1. 程式人生 > >使用bulk collect 和 forall 提高遊標效能

使用bulk collect 和 forall 提高遊標效能

           當執行一個pl/sql程式時, pl/sql語句引擎會執行pl/sql語句。但如果在這個過程中引擎遇到sql語句,它會把這個語句傳給sql引擎(後臺發生上下文切換)。

PL/SQLSQL引擎(engines)中,太多的上下文切換(context switches)會影響效能。這個會發生在當一個迴圈為集合中的每個元素執行一個單個SQL語句時。而使用批挷定能顯著提高效能。

SQL語句中,為PL/SQL變數指定值稱為挷定(binding)DML語句能傳遞所有集合元素到一個單個操作中,這過程稱為批挷定(bulk binding)。  如果集合有20個元素,批挷定讓你用單個操作等效於執行與

20SELECTINSERT   UPDATEDELETE語句。這個技術通過減少在PL/SQLSQL引擎(engines)間的上下文切換來提高效能。批挷定包括:1.帶INSERT, UPDATE, and DELETE語句的批挷定:在FORALL語句中嵌入SQL語句2.帶SELECT語句的批挷定:在SELECT語句中用BULK COLLECT 語句代替INTO 。

作用:BULK COLLECT提供對資料的高速檢索。

優點:可以將多個行引入一個或多個集合中,而不是單獨變數或記錄中,減少了上下文切換,效能高。

缺點:1.消耗更多的記憶體(PGA);由於該數集合據儲存在每個會話中,假設一個會話多消耗

5M,記憶體,那麼1000個就消耗約5G記憶體。

2.BULK COLLECT INTO的目標物件必須是集合型別。

3.不能對使用字串型別作鍵的關聯陣列使用BULK COLLECT子句。

4.如果有一個隱式的資料型別轉換,複合目標的集合(如物件型別集合)就不能用於BULK COLLECTINTO 子句中。

bulk collect語句可以使用三種方式:

1select into語句中使用bulk collect

2fetch into中使用bulk collect

3returning into中使用bulk collect

create table empl_tbl(last_name varchar2(20),
first_name varchar2(10),
salary number(10));

--建立表
create table demo_t(
  id number(5),
  name varchar2(50),
  sales number
);
--在select into語句中使用bulk collect
DECLARE
TYPE contractList IS TABLE OF sad.cm_contracts_t.contract_number%TYPE;
contracts contractList;
BEGIN
-- Limit the number of rows to 100.
SELECT contract_number BULK COLLECT INTO contracts FROM sad.cm_contracts_t
WHERE ROWNUM <= 100;


--在fetch into中使用bulk collect
DECLARE
TYPE contracts_list IS TABLE OF sad.cm_contracts_t%ROWTYPE;
contracts_l contracts_list;
CURSOR c1 IS
SELECT contract_number,contract_id FROM contracts_l WHERE deptno > 10;
BEGIN
OPEN c1;
FETCH c1 BULK COLLECT INTO dept_recs;
END;

--在returning into中使用bulk collect
--CREATE TABLE item AS SELECT * FROM sad.sad_prm_item_ti;
declare 
  -- Local variables here
TYPE lineid IS TABLE OF item.lineid%TYPE;
line_id lineid;
begin
DELETE FROM item t WHERE t.lineid='1'
RETURNING lineid  BULK COLLECT INTO line_id;
dbms_output.put_line('Deleted ' || SQL%ROWCOUNT || ' rows:');
FOR i IN line_id.FIRST .. line_id.LAST
 LOOP
    dbms_output.put_line('item : '||line_id(i));
 END LOOP;
end;
-- Created on 2013/12/4 by ZWX190516 
DECLARE
  -- Local variables here
  all_row       NUMBER(10);
 all_rows_bluk       NUMBER(10);
  temp_last_name empl_tbl.last_name%TYPE;
  --首先,定義一個Index-by表資料型別
  TYPE last_name_tab IS TABLE OF empl_tbl.last_name%TYPE INDEX BY BINARY_INTEGER;
  last_name_arr last_name_tab;
  --定義一個Index-by表集合變數
  dis_count NUMBER;
  
  --資料量
   data_count NUMBER :=100000;
  --記錄時間
  t1 NUMBER;
  t2 NUMBER;
  t3 NUMBER;
  t4 NUMBER;
BEGIN
  all_row       := 0;
  all_rows_bluk       := 0;
  temp_last_name := ' ';


  --寫入1W筆資料
  FOR i IN 1 .. data_count LOOP
    INSERT INTO empl_tbl
      (last_name, first_name, salary)
    VALUES
      ('carl' || (i), 'wu' || (data_count-1), i);
  END LOOP;

  COMMIT;
  
  
--記錄時間
  SELECT DBMS_UTILITY.get_time INTO t1 FROM DUAL;
  
  --查詢不相同資料使用時間
  SELECT COUNT(DISTINCT last_name) "Distinct Last Name"
    INTO dis_count
    FROM empl_tbl; 
    
--記錄時間
  SELECT DBMS_UTILITY.get_time INTO t2 FROM DUAL;
  
  --使用簡單遊標實現
  BEGIN
    FOR cur IN (SELECT last_name FROM empl_tbl ORDER BY last_name) LOOP
      IF cur.last_name != temp_last_name THEN
        all_row := all_row + 1;
      END IF;
      temp_last_name := cur.last_name;
    END LOOP;
    dbms_output.put_line('all_rows are ' || all_row);
  END;

--記錄時間
  SELECT DBMS_UTILITY.get_time INTO t3 FROM DUAL;
  --使用Bulk Collect來實現
BEGIN
  SELECT last_name BULK COLLECT INTO last_name_arr FROM empl_tbl;
  FOR i IN 1 .. last_name_arr.count LOOP
    IF temp_last_name != last_name_arr(i) THEN
      all_rows_bluk := all_rows_bluk + 1;
    END IF;
    temp_last_name := last_name_arr(i);
  END LOOP;
  dbms_output.put_line(' BULK COLLECT  all_rows are ' || all_rows_bluk);
  END;
--記錄時間
  SELECT DBMS_UTILITY.get_time INTO t4 FROM DUAL;
  
  
    DBMS_OUTPUT.put_line('Execution Time (hsecs)');
  DBMS_OUTPUT.put_line('---------------------');
  DBMS_OUTPUT.put_line('distince: ' || TO_CHAR(t2 - t1));
  DBMS_OUTPUT.put_line('cursor one by one :   ' || TO_CHAR(t3 - t2));
    DBMS_OUTPUT.put_line('cursor BULK COLLECTe :   ' || TO_CHAR(t4 - t3));

END;


FORALL

作用:FORALL可大大改進INSERT、UPDATEDELETE操作的效能。

在傳送語句到SQL引擎前,FORALL語句告知PL/SQL引擎批挷定輸入集合。儘管FORALL語句包含一個迭代(iteration)模式,它並不一是個FOR迴圈

優點:一次性繫結資料寫進行操作。減少上下文切換,提高資料庫效能。

缺點:1.操作邏輯單一,只能寫一個sql

    2.若出現異常,必須捕獲異常後提交才能保證資料正常操作。

forall語句使用三種方式(只允許一條sql)

1.FORALL 下標變數(只能當作下標被引用) IN下限..上限。

2.INDICES OF collection_name (引用特定集合元素的下標(該集合可能為稀疏))

3.VALUES OF colletion_name(把該集合中的值當作下標,且該集合值的型別只能是PLS_INTEGER BINARY_INTEGER)

)。

--批量插入演示簡單;必須順序
declare
  type tb_table_type is table of demo_t%rowtype
    index by binary_integer;
  tb_table tb_table_type;
begin
  for i in 1..10 loop
    tb_table(i).id:=i;
    tb_table(i).name:='NAME'||i;
  end loop;
  forall i in 1..tb_table.count
    insert into demo_t values tb_table(i);
end;

--批量修改演示
declare
  type demo_t_type is table of demo_t%rowtype
  index by binary_integer;
  demo_t demo_t_type;
begin
  for i in 1..10 loop
    demo_t(i).id:=i;
    demo_t(i).name:='NAMES'||i;
  end loop;
  forall i in 1..demo_t.count
    update tb1 t set row = demo_t(i) where t.id = demo_t(i).id;
end;

--批量刪除演示
declare
  type demo_t_type is table of demo_t%rowtype
  index by binary_integer;
  demo_t demo_t_type;
begin
  for i in 1..10 loop
    demo_t(i).id:=i;
    demo_t(i).name:='NAMES'||i;
  end loop;
  forall i in 1..demo_t.count
    delete tb1 where id = demo_t(i).id;
end;


select * from demo_t

--批量寫入不連續的陣列

declare
  type demo_t_table_type is table of demo_t%rowtype
    index by binary_integer;
  demo_t_table demo_t_table_type;
begin
  for i in 1..10 loop
    demo_t_table(i).id:=i;
    demo_t_table(i).name:='NAME'||i;
  end loop;
  demo_t_table.delete(3);
  demo_t_table.delete(6);
  demo_t_table.delete(9);
  forall i in indices of demo_t_table
    insert into demo_t values demo_t_table(i);
end;



--按照下標寫入陣列資料
declare
  type index_poniter_type is table of pls_integer;
  index_poniter index_poniter_type;
  type demo_t_table_type is table of demo_t%rowtype
    index by binary_integer;
  demo_t_table demo_t_table_type;
begin
  index_poniter:=index_poniter_type(1,3,5,7);
  for i in 1..10 loop
    demo_t_table(i).id:=i;
    demo_t_table(i).name:='NAME'||i;
  end loop;
  forall i in values of index_poniter
    insert into demo_t values demo_t_table(i);
end;

-- Created on 2013/12/5 by ZWX190516 
DECLARE
  -- Local variables here
  TYPE numtab IS TABLE OF NUMBER(20) INDEX BY BINARY_INTEGER;

  TYPE nametab IS TABLE OF VARCHAR2(50) INDEX BY BINARY_INTEGER;

  pnums  numtab;
  pnames nametab;
  t1     NUMBER;
  t2     NUMBER;
  t3     NUMBER;
BEGIN
  FOR j IN 1 .. 1000000 LOOP
    pnums(j) := j;
    pnames(j) := 'Seq No. ' || TO_CHAR(j);
  END LOOP;

--記錄時間
  SELECT DBMS_UTILITY.get_time INTO t1 FROM DUAL;
--普通遊標寫入
  FOR i IN 1 .. 1000000 LOOP
    INSERT INTO blktest VALUES (pnums(i), pnames(i));
  END LOOP;
  
--記錄時間
  SELECT DBMS_UTILITY.get_time INTO t2 FROM DUAL;

--forall寫入
  FORALL i IN 1 .. 1000000
    INSERT INTO blktest VALUES (pnums(i), pnames(i));

--記錄時間
  SELECT DBMS_UTILITY.get_time INTO t3 FROM DUAL;

  DBMS_OUTPUT.put_line('Execution Time (hsecs)');
  DBMS_OUTPUT.put_line('---------------------');
  DBMS_OUTPUT.put_line('FOR loop: ' || TO_CHAR(t2 - t1));
  DBMS_OUTPUT.put_line('FORALL:   ' || TO_CHAR(t3 - t2));

END;

異常:Forall在出現異常情況後,捕獲到異常不進行事物處理,那麼將會自動回滾。

其他用法:

1.%BULK_ROWCOUNT屬性計算FORALL迭代影響行數

1.2%BULK_EXCEPTIONS屬性處理FORALL異常

CREATE TABLE emp2 (deptno NUMBER(2), job VARCHAR2(15));
INSERT INTO emp2 VALUES(10, 'Clerk');
INSERT INTO emp2 VALUES(10, 'Clerk');
INSERT INTO emp2 VALUES(20, 'Bookkeeper'); -- 10-char job title
INSERT INTO emp2 VALUES(30, 'Analyst');
INSERT INTO emp2 VALUES(30, 'Analyst');
Comit;
DECLARE
TYPE NumList IS TABLE OF NUMBER;
depts NumList := NumList(10, 20, 30);
BEGIN
FORALL j IN depts.FIRST..depts.LAST
UPDATE emp2 SET job = job || ' (temp)'
WHERE deptno = depts(j);
-- raises a "value too large" exception
EXCEPTION
WHEN OTHERS THEN
COMMIT;
END;

PL/SQL Developer Test script 3.0
12
DECLARE 
TYPE NumList IS TABLE OF NUMBER;
depts NumList := NumList(10, 20, 50); 
BEGIN 
FORALL j IN depts.FIRST..depts.LAST 
UPDATE demo_t t SET sales = sales * 1.10 WHERE t.id=depts(j);
dbms_output.put_line( SQL%BULK_ROWCOUNT(3)  );
END;


在進行SQL資料操作語句時,SQL引擎開啟一個隱式遊標(命名為SQL),該遊標的標量屬性(scalar attribute)有 %FOUND, %ISOPEN, %NOTFOUND, and %ROWCOUNT。 
  FORALL語句除具有上邊的標量屬性外,還有個複合屬性(composite attribute):%BULK_ROWCOUNT,該屬性具有索引表(index-by table)語法。它的第i個元素存貯SQL語句(INSERT, UPDATE或DELETE)第i個執行的處理行數。如果第i個執行未影響行,%bulk_rowcount (i),返回0。FORALL與%bulk_rowcount屬性使用相同下標。
0
0

PL/SQL Developer Test script 3.0
27
DECLARE 
TYPE NumList IS TABLE OF NUMBER; 
num_tab NumList := NumList(10,0,11,12,30,0,20,199,2,0,9,1); 
errors NUMBER; 
dml_errors EXCEPTION; 
PRAGMA exception_init(dml_errors, -24381); 
BEGIN 
FORALL i IN num_tab.FIRST..num_tab.LAST SAVE EXCEPTIONS 
DELETE FROM demo_t  WHERE sales > 500000/num_tab(i); 
EXCEPTION 
WHEN dml_errors THEN 
errors := SQL%BULK_EXCEPTIONS.COUNT; 
dbms_output.put_line('Number of errors is ' || errors); 
FOR i IN 1..errors LOOP 
dbms_output.put_line('Error ' || i || ' occurred during '|| 
'iteration ' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX); 
dbms_output.put_line('Oracle error is ' ||
SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE)); 
END LOOP; 
END;


執行期間引發的所有異常都被儲存遊標屬性 %BULK_EXCEPTIONS中,它存貯一個集合記錄,每記錄有兩個欄位: 
  %BULK_EXCEPTIONS(i).ERROR_INDEX:存貯在引發異常期間FORALL語句迭代(重複:iteration) 
  %BULK_EXCEPTIONS(i).ERROR_CODE:存貯相應的Oracle錯誤程式碼 
  %BULK_EXCEPTIONS.COUNT存貯異常的數量。(該屬性不是%BULK_EXCEPTIONS集合記錄的欄位)。如果忽略SAVE EXCEPTIONS,當引發異常時,FORALL語句停止執行。此時,SQL%BULK_EXCEPTIONS.COUNT 返回1, 且SQL%BULK_EXCEPTIONS只包含一條記錄。如果執行期間無異常 SQL%BULK_EXCEPTIONS.COUNT 返回 0


注意: 1.bulk collect 消耗的是PGA記憶體。每個會話都會佔記憶體,所以儘可能使用limit限制。

   2.pl/sql優化等級為2(預設)或更高,把遊標內容直接寫在for迴圈之中有利於提高效能,除非其他特殊情況。

3.注意cursor%notfond的正確使用

PL/SQL Developer Test script 3.0
26
-- Created on 2013/11/25 by ZWX190516 
declare 
  -- Local variables here
  i integer;
  TYPE type_t IS TABLE OF sad.cm_contracts_t%ROWTYPE;
  type_table type_t;  
  CURSOR cur_test IS
  SELECT * FROM sad.cm_contracts_t t
  WHERE rownum=1;
begin
  -- Test statements here
  OPEN cur_test;
  LOOP
      FETCH cur_test BULK COLLECT   INTO type_table  LIMIT 3;
   EXIT WHEN cur_test%NOTFOUND; --該句判斷放在前邊,導致漏掉資料。
    FOR i IN type_table.first..type_table.last
      LOOP
        dbms_output.put_line(type_table(i).contract_id);
        END LOOP;
        

   END LOOP;
   
   CLOSE cur_test;
  
end;
0
0