1. 程式人生 > >FORALL使用--insert/delete/update操作的批繫結Bulk Binding

FORALL使用--insert/delete/update操作的批繫結Bulk Binding

       從 Oracle8i 開始,出現了 FORALL 語句,可以幫助我們更快地執行 DML 語句。FORALL是Oracle在PL/SQL中提供的一種批量處理語法。它提供了比傳統for loop更好的處理效能優勢。兩者的差異主要體現在處理引擎上下文切換上的效能損耗優勢。在PL/SQL語句中出現語句,PL/SQL引擎會將SQL語句傳遞轉給SQL引擎進行處理。SQL引擎處理後再將結果返回給PL/SQL引擎。這個過程我們稱之為上下文切換(context switch)。在for語句執行的時候,伴隨著頻繁的上下文切換動作。使用FORALL,就可以避免出現這種情況。

      在使用FORALL的時候,PL/SQL與SQL引擎的互動只有一次。所有的語句引數都是一次性的傳遞給SQL進行執行。這樣,上下文切換動作的損耗就能得到節省。

      當要在 Oracle 中進行批量 INSERT、UPDATE 和 DELETE 操作時,可以使用 FORALL 語句。與for語句語法格式不同的是:FORALL沒有loop和end loop關鍵字配對。也就意味著forall語句後面只能跟一個SQL語句呼叫。 

      提醒:因為FORALL功能是隨著Oracle版本的演進不斷髮展的,不同Oracle版本該功能會有所變化,所以本文除無特別說明外,舉例的程式碼都是在Oracle 11g的環境下正常執行的。

1、FORALL使用語法

      下面介紹FORALL三種語法的介紹,其中INDICES OF和VALUES OF是Oracle 10g引入的新特性,就是說在Oracle 10g之前,FORALL只能遍歷下標連續的陣列。

語法1:

FORALL 變數 IN 下限..上限
  sql 語句;
說明:
    1. 變數:是被遍歷的的陣列元素的下標。
    2. 下標必須是連續的,否則執行會報錯。
    3. 執行的sql語句只能有一個。
    4. 在oracle 11g之前,陣列是不能使用ROWTYPE和RECORD型別的,只是使用基本型別的陣列。

語法2:

FORALL 變數 IN INDICES OF(跳過沒有賦值的元素,例如被 DELETE 的元素,NULL 也算值) 集合
  [BETWEEN 下限 AND 上限]
  sql 語句;
說明:
    1. 變數:是被遍歷的的陣列元素的下標。
    2. INDICES OF:可以是迴圈跳過沒有賦值的元素,注意:被賦予NULL也算是有值。     3. BETWEEN 下限 AND 上限:該子句是可選的,作用是把子句的'下限'到‘上限’範圍之間的數值與陣列元素下標做個交集,並遍歷。這個交集可能是陣列的全部元素,也可能是部分元素,或是空。如果不指定,就遍歷陣列全部元素。     4. 執行的sql語句只能是一個。

語法3:

FORALL 變數 IN VALUES OF 集合
  sql 語句;
說明:
     1.  變數:被遍歷元素的值,且該集合值的型別只能是PLS_INTEGER或是BINARY_INTEGER。
     2.  執行的sql只能是一個。

2、語法1使用說明

  通過 語法1的方式遍歷陣列,陣列下標的上限和下限之間的數字必須是連續的,否則會報錯。
  下面以一個例子來說明FORALL語法1的使用:

   1.    建立t_student表
create table t_student(
  gid number(38),
  name varchar2(100)
);
   2.   批量插入資料
declare
  type stu_table_type is table of t_student%rowtype
    index by binary_integer;
  stu_table stu_table_type;
begin
  for i in 1..10 loop
    stu_table(i).gid:=i;
    stu_table(i).name:='NAME'||i;
  end loop;
  forall i in stu_table.first..stu_table.last
    insert into t_student values stu_table(i);
  commit;  
end;

     如果stu_table陣列中的資料下標不連續,比如下面的程式碼:

declare
  type stu_table_type is table of t_student%rowtype
    index by binary_integer;
  stu_table stu_table_type;
begin
  for i in 1..10 loop
    stu_table(i).gid:=i;
    stu_table(i).name:='NAME'||i;
  end loop;
  stu_table.delete(2);--刪除陣列第二個元素
  forall i in stu_table.first..stu_table.last
    insert into t_student values stu_table(i);
  commit;  
end;

     執行上面程式碼會發生如下錯誤:ORA-22160:element at index [2] does not exist。

     在oracle 10g之前,Oracle一致有這種限制,自從oracle 10g開始,資料庫增加了indices of和values of兩個子句,成功的解決了這個問題。下面分別介紹下這個兩個子句語法的使用。

3、語法2使用說明

declare
  type student_tbl_type is table of t_student%rowtype
    index by binary_integer;
  student_tbl student_tbl_type;
begin
  for i in 1..10 loop
    student_tbl(i).gid:=i;
    student_tbl(i).name:='NAME'||i;
  end loop;
  student_tbl.delete(3);
  student_tbl.delete(6);
  student_tbl.delete(9);--刪除3,6,9三個元素
  forall i in indices of student_tbl
    insert into t_student values student_tbl(i);
  commit;  
end;

檢視執行結果:

SQL> select t.gid, t.name from T_STUDENT t;
                                    GID NAME
--------------------------------------- --------------------------------------------------------------------------------
                                      1 NAME1
                                      2 NAME2
                                      4 NAME4
                                      5 NAME5
                                      7 NAME7
                                      8 NAME8
                                     10 NAME10
7 rows selected

從執行結果可見:插入的元素雖然沒有3,6,9三個元素,即陣列是不連續的,通過indices of 子句,可以實現不連續陣列的FORALL迴圈操作。

下面通過使用"between 下限 and 上限"子句舉例:

declare
  type student_tbl_type is table of t_student%rowtype
    index by binary_integer;
  student_tbl student_tbl_type;
begin
  for i in 1..10 loop
    student_tbl(i).gid:=i;
    student_tbl(i).name:='NAME'||i;
  end loop;
  student_tbl.delete(3);
  student_tbl.delete(6);
  student_tbl.delete(9);--刪除3,6,9三個元素
  forall i in indices of student_tbl between 5 and 18  --指定5到18範圍內下標陣列元素
    insert into t_student values student_tbl(i);
  commit;  
end;

檢視執行結果:

SQL> select t.gid, t.name from T_STUDENT t;
                                    GID NAME
--------------------------------------- --------------------------------------------------------------------------------
                                      5 NAME5
                                      7 NAME7
                                      8 NAME8
                                     10 NAME10

從上面可以看出插入資料的陣列是between and子句和陣列元素的交集。

4、 語法3使用說明

把該集合中的值當作下標,且該集合值的型別只能是PLS_INTEGER或BINARY_INTEGER。

declare
  type index_poniter_type is table of pls_integer;
  index_poniter index_poniter_type;
  type student_tbl_type is table of t_student%rowtype
    index by binary_integer;
  student_tbl student_tbl_type;
begin
  index_poniter:=index_poniter_type(1,3,5,7);
  for i in 1..10 loop
    student_tbl(i).gid:=i;
    student_tbl(i).name:='NAME'||i;
  end loop;
  forall i in values of index_poniter
    insert into t_student values student_tbl(i);
  commit;
end;

檢視執行結果:

SQL> select t.gid, t.name from T_STUDENT t;
                                    GID NAME
--------------------------------------- --------------------------------------------------------------------------------
                                      1 NAME1
                                      3 NAME3
                                      5 NAME5
                                      7 NAME7

5、FORALL中的SQL語句批繫結

      在介紹內容之前先舉一個例子:

declare
  v_gid t_student.gid%TYPE;
  v_name t_student.name%TYPE;
begin
  v_name := 'CHENZHEN';
  forall i in 1..10
    update t_student set name = v_name where gid = i;
  commit;  
end;
    執行這段程式碼會報錯,錯誤資訊有兩個:
    1)PLS-00430:FORALL interation variable I is not allowed in this context
    2)PLS-00435:DML statement without BULK In-BIND cannot be used inside FORALL。
    從錯誤資訊可知:
     1) 在上下文中不允許使用 FORALL 迴圈變數i,迴圈變數i只能作為被遍歷陣列的下標來使用。     2)沒有批繫結的的SQL語句是不能放在FORALL中的。那麼什麼是繫結,什麼又是批繫結?  
    在SQL語句中,為PL/SQL變數指定值稱為挷定(binding);在DML語句中運算元組,這個過程稱為批挷定(bulk binding)。 
    批挷定包括: 
   1.帶INSERT,UPDATE和DELETE語句的批挷定:在FORALL語句中嵌入SQL語句;
   2.帶SELECT語句的批挷定:在SELECT語句中用BULK COLLECT 語句代替INTO。 

     在FORALL中的SQL語句一定要有對陣列的操作,而且迴圈變數只能作為陣列的下標來使用,如下面的語句:
declare
  TYPE name_type is table of t_student.name%TYPE index by binary_integer;
  TYPE gid_type is table of t_student.gid%TYPE index by binary_integer;
  v_name name_type;
  v_gid gid_type;
begin
  for i in 1..10 loop
    v_name(i) := 'CHENZHEN';
    v_gid(i) := i;
  end loop;

  forall i in 1..10
    update t_student set name = v_name(i) where gid = v_gid(i);
  commit;  
end;

6、FORALL 中的 RETURNING

    FORALL 中的 RETURNING 必須使用 BULK COLLECT INTO

    如下程式碼所示:

declare
  type student_tbl_type is table of t_student%rowtype
    index by binary_integer;
  type gid_tbl_type is table of number
    index by binary_integer;
  student_tbl student_tbl_type;
  gid_tbl  gid_tbl_type;
  
begin
  for i in 1..10 loop
    student_tbl(i).gid:=i;
    student_tbl(i).name:='NAME'||i;
  end loop;
  forall i in 1..10
    delete from t_student where gid = student_tbl(i).gid
       returning gid bulk collect into gid_tbl;
  commit;
  
 for i in 1..gid_tbl.count loop
    dbms_output.put_line(gid_tbl(i));
  end loop;  
end;

7、Oracle不同版本FORALL的使用差異

       在oracle 11g之前,Bulk Binding與RECORD、%ROWTYPE是不能在一塊使用的,也就是說,BULK In-BIND只能與簡單型別的陣列一塊使用,這樣導致如果有多個欄位需要用BULK In-BIND來處理。雖然oracle 11g已經解決了這個問題,但是如果使用的是11g之前的版本,一定要注意,否則程式將會報錯。

    如下面這段程式碼在oracle 11g下沒有任何問題,但是在oracle 10g即之前版本,將無法編譯通過。

declare
  type stu_table_type is table of t_student%rowtype   --%ROWTYPE型別的陣列
    index by binary_integer;
  stu_table stu_table_type;
begin
  for i in 1..10 loop
    stu_table(i).gid:=i;
    stu_table(i).name:='NAME'||i;
  end loop;
  forall i in stu_table.first..stu_table.last
    insert into t_student values stu_table(i);
  commit;  
end;
    錯誤資訊是:PLS-00436: implementation restriction: cannot reference fields of BULK In-BIND table of records(PLS-00436:實施限制:不能引用記錄的BULK In-BIND表的欄位)。
    雖然oracle 10g及之前的版本,Bulk Binding與RECORD、%ROWTYPE是不能在一塊使用,我們可以通過下面實現來解決,程式碼如下:
declare
  type name_table_type is table of t_student.name%type index by binary_integer; --基本型別的陣列
  type gid_table_type is table of t_student.gid%type index by binary_integer; --基本型別的陣列

  v_name_table name_table_type;
  v_gid_table gid_table_type;
begin
  for i in 1..10 loop
    v_gid_table(i) :=i;
    v_name_table(i) :='NAME'||i;
  end loop;
  forall i in v_gid_table.first..v_gid_table.last
    insert into t_student(gid, name) values(v_gid_table(i), v_name_table(i));
  commit;  
end;

8、FOR和FORALL效能比較

上面通過理論分析知道FORALL執行效率比FOR高,究竟高多少,我們還沒有見識,下面就通過一段程式碼對他們做個比較:

DECLARE
  TYPE gid_tbl_type IS TABLE OF t_student.gid%TYPE INDEX BY PLS_INTEGER;
  TYPE name_tbl_type IS TABLE OF t_student.name%TYPE INDEX BY PLS_INTEGER;
  gid_tbl gid_tbl_type;
  name_tbl name_tbl_type;
  t1 INTEGER;
  t2 INTEGER;
  t3 INTEGER;
BEGIN
  FOR j IN 1..100000 LOOP  -- load index-by tables
     gid_tbl(j) := j;
     name_tbl(j) := 'Part No. ' || TO_CHAR(j);
  END LOOP;
  t1 := DBMS_UTILITY.get_time;
  FOR i IN 1..50000 LOOP  -- use FOR loop
     INSERT INTO t_student(gid, name) VALUES (gid_tbl(i), name_tbl(i));
  END LOOP;
  COMMIT;
  t2 := DBMS_UTILITY.get_time;
  FORALL i IN 50001..100000  -- use FORALL statement
     INSERT INTO t_student(gid, name) VALUES (gid_tbl(i), name_tbl(i));
  COMMIT;   
  t3 := DBMS_UTILITY.get_time;
  DBMS_OUTPUT.PUT_LINE('Execution Time (secs)');
  DBMS_OUTPUT.PUT_LINE('---------------------');
  DBMS_OUTPUT.PUT_LINE('FOR loop: ' || TO_CHAR((t2 - t1)/100));
  DBMS_OUTPUT.PUT_LINE('FORALL:   ' || TO_CHAR((t3 - t2)/100));
END;

       執行結果:

Execution Time (secs)
---------------------
FOR loop: 2.21
FORALL:   .3

      同樣執行50000條insert語句,FOR花了2.21秒,FORALL花了0.3秒,也就是FORALL的執行效率是FOR的約7.4倍。這種效率上的優越性還是很高的。

9、FORALL的Sql%rowcount和sql%bulk_rowcount屬性

         在forall操作中,Sql%rowcount表示commit之前(所以該屬性要執行在commit之前,否則獲得將是改變0條資料)被影響的記錄總行數,而sql%bulk_rowcount(i)則表示FORALL遍歷每一個元素,DML操作所影響的行數,該值是存在一個集合裡,FORALL中的第n條dml語句處理的行數儲存在該集合的第n個元素中。

declare
  TYPE name_type is table of t_student.name%TYPE index by binary_integer;
  TYPE gid_type is table of t_student.gid%TYPE index by binary_integer;
  v_name name_type;
  v_gid gid_type;
begin
  for i in 1..10 loop
    v_name(i) := 'CHENZHEN'||i;
    v_gid(i) := i;
  end loop;
  forall i in 1..10
    update t_student set name = v_name(i) where gid = v_gid(i); 
  dbms_output.put_line(sql%rowcount||'行記錄被更新!');
  for i in 1 .. v_name.count loop
    dbms_output.put_line('名稱為'||v_name(i)||'的記錄共有'||sql%bulk_rowcount(i)||'條被更新!');
  end loop;
  commit; 
end;
執行結果如下:
10行記錄被更新
名稱為CHENZHEN1的記錄共有1條被更新
名稱為CHENZHEN2的記錄共有1條被更新
名稱為CHENZHEN3的記錄共有1條被更新
名稱為CHENZHEN4的記錄共有1條被更新
名稱為CHENZHEN5的記錄共有1條被更新
名稱為CHENZHEN6的記錄共有1條被更新
名稱為CHENZHEN7的記錄共有1條被更新
名稱為CHENZHEN8的記錄共有1條被更新
名稱為CHENZHEN9的記錄共有1條被更新
名稱為CHENZHEN10的記錄共有1條被更新