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的使用:
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條被更新