FORALL在資料批量處理中的使用
在PLSQL中,PLSQL塊/子程式由PLSQL引擎處理,而其中的SQL語句則由PLSQL引擎傳送至SQL引擎處理,後者處理完畢後再向前者返回資料,兩者之間的通訊稱為上下文切換。過多的上下文切換將帶來過量的效能負載,FORALL和BULK COLLECT子句則可批量處理資料,從而減少這方面的效能負載。
一、FORALL與DML語句的簡單結合
當PLSQL中的DML語句加上FORALL子句就可以一次性將語句和資料傳送至SQL引擎處理,處理結果也會一次性反饋給PLSQL引擎。
CREATE TABLE cux_employees(empno NUMBER, ename VARCHAR2(40)); / DECLARE TYPE empno_tbl_type IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; TYPE ename_tbl_type IS TABLE OF VARCHAR2(40) INDEX BY BINARY_INTEGER; t_empno empno_tbl_type; t_ename ename_tbl_type; l_limit NUMBER := 5000; l_len NUMBER := length(l_limit); l_sql VARCHAR2(240); BEGIN --模擬出現有兩個大量資料的表變數 FOR k IN 1 .. l_limit LOOP t_empno(k) := k; t_ename(k) := 'EMP' || lpad(k, l_len, '0'); END LOOP; --INSERT語句搭配FORALL FORALL k IN 1 .. l_limit INSERT INTO cux_employees (empno ,ename) VALUES (t_empno(k) ,t_ename(k)); --UPDATE語句搭配FORALL --這裡要注意:SET節中不允許使用迴圈變數! FORALL k IN 1 .. l_limit UPDATE cux_employees SET ename = regexp_replace(ename, '(\d{' || l_len || '})', '_No.\1') WHERE empno = t_empno(k); --DELETE語句搭配FORALL FORALL k IN floor(l_limit / 2) + 1 .. l_limit DELETE FROM cux_employees WHERE empno = t_empno(k); --FORALL語句也可以搭配動態SQL實現批量DML操作,例如: l_sql := 'INSERT INTO cux_employees(empno, ename) VALUES(:1, :2)'; FORALL k IN floor(l_limit / 2) + 1 .. l_limit EXECUTE IMMEDIATE l_sql USING t_empno(k), t_ename(k); END;
二、SAVE EXCEPTIONS和SQL%BULK_EXCEPTIONS屬性
批量DML雖然是一次性將指令和資料傳送至SQL引擎,但在SQL引擎中仍然是一條條執行的,如果在處理過程中發生異常,則整個批量處理會中斷,同時丟擲這個異常。使用SAVE EXCEPTIONS關鍵字可以使在過程中即便發生異常,也能繞過異常繼續,以保證整個批量處理中沒有異常的處理全部執行,最終丟擲一個異常,程式碼-24381。顧名思義,記錄下來的異常則可以通過SQL%BULK_EXCEPTIONS屬性查詢:SQL%BULK_EXCEPTIONS是一個記錄集合,每條記錄都由ERROR_INDEX和ERROR_CODE兩個欄位組成,前者是批量處理中發生異常的迭代編號(對應著FORALL的迴圈變數),後者是對應異常的ORACLE錯誤程式碼;而SQL%BULK_EXCEPTIONS.COUNT則是批量處理中的異常個數了。
TRUNCATE TABLE cux_employees; ALTER TABLE cux_employees ADD CONSTRAINT cux_employees_u1 UNIQUE(empno); ALTER TABLE cux_employees MODIFY(empno NOT NULL); / DECLARE TYPE empno_tbl_type IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; TYPE ename_tbl_type IS TABLE OF VARCHAR2(240) INDEX BY BINARY_INTEGER; t_empno empno_tbl_type; t_ename ename_tbl_type; errors_in_array_dml EXCEPTION; PRAGMA EXCEPTION_INIT(errors_in_array_dml, -24381); BEGIN FOR k IN 1 .. 10 LOOP t_empno(k) := k; t_ename(k) := 'EMP' || lpad(k, 3, '0'); END LOOP; --製造一些問題資料 t_empno(3) := NULL; t_empno(5) := 10; t_ename(7) := rpad(t_ename(7), 41, '.'); --將資料批量插入表中 FORALL k IN 1 .. 10 SAVE EXCEPTIONS INSERT INTO cux_employees (empno, ename) VALUES (t_empno(k), t_ename(k)); COMMIT; EXCEPTION WHEN errors_in_array_dml THEN dbms_output.put_line('批量DML中發生了' || SQL%bulk_exceptions.count || '個錯誤'); FOR k IN 1 .. SQL%bulk_exceptions.count LOOP dbms_output.put_line('第' || k || '個錯誤發生在第' || SQL%BULK_EXCEPTIONS(k) .error_index || '行DML:' || SQLERRM(-sql%BULK_EXCEPTIONS(k).error_code)); --注意%BULK_EXCEPTIONS中的error_code不帶負號 END LOOP; END;
上例的執行結果是:
批量DML中發生了3個錯誤 第1個錯誤發生在第3行DML:ORA-01400: 無法將 NULL 插入 () 第2個錯誤發生在第7行DML:ORA-12899: 列 的值太大 (實際值: , 最大值: ) 第3個錯誤發生在第10行DML:ORA-00001: 違反唯一約束條件 (.)
三、SQL%BULK_ROWCOUNT屬性
SQL%BULK_ROWCOUNT也是為FORALL設計的,SQL%BULK_ROWCOUNT是一個數字集合,用於儲存FORALL中第N次DML所產生影響的實際行數,沒有產生影響就返回0,若產生影響,影響了幾行就返回幾;SQL%BULK_ROWCOUNT的索引和FORALL的迴圈變數是一一對應的。
DECLARE
TYPE deptno_tbl_type IS TABLE OF NUMBER;
t_deptno deptno_tbl_type := deptno_tbl_type(10, 40);
BEGIN
FORALL k IN 1 .. t_deptno.count
UPDATE emp SET sal = sal * 1.5 WHERE deptno = t_deptno(k);
FOR i IN 1 .. t_deptno.count LOOP
dbms_output.put_line('第' || i || '次更新實際影響了' || SQL%BULK_ROWCOUNT(i) ||
'行資料.');
END LOOP;
END;
上例的執行結果是
第1次更新實際影響了3行資料. 第2次更新實際影響了0行資料.
四、INDICES OF選項
如果使用FORALL操作一個索引不連續的陣列,那麼迴圈變數的上下限則無法確定,此時需要使用INDICES OF選項,可使迴圈變數直接在存在的索引當中遍歷。
DECLARE
TYPE deptno_tbl_type IS TABLE OF NUMBER INDEX BY BINARY_INTEGER;
t_deptno deptno_tbl_type;
BEGIN
t_deptno(3) := 10;
t_deptno(8) := 30;
t_deptno(10) := 40;
FORALL k IN INDICES OF t_deptno
UPDATE emp SET sal = sal * 1.5 WHERE deptno = t_deptno(k);
FOR i IN t_deptno.first .. t_deptno.last LOOP
IF t_deptno.exists(i)
THEN
dbms_output.put_line('第' || i || '次更新實際影響了' || SQL%BULK_ROWCOUNT(i) ||
'行資料.');
END IF;
END LOOP;
END;
上例的執行結果是
第3次更新實際影響了3行資料. 第8次更新實際影響了6行資料. 第10次更新實際影響了0行資料.
五、VALUES OF選項
VALUES OF選項可以讓我們指定FORALL迴圈變數遍歷的資料,不僅可以無序,甚至可以反覆。簡單來說就是在一個數組中按照我們希望的遍歷順序將索引數存入,VALUES OF就可以將陣列中的資料作為迴圈變數遍歷的範圍。由於資料相當於賦值給了迴圈變數,所以這個陣列應當是PLS_INTEGER或BINARY_INTEGER元素的陣列,而且要保證這個陣列中不能有NULL值,否則會引起FORALL報錯ORA-22160,即便使用SAVE EXCEPTIONS也會使整個FORALL不執行,因為這不是到SQL引擎才丟擲的錯誤。
CREATE TABLE cux_male_employees (empno NUMBER, ename VARCHAR2(40));
CREATE TABLE cux_female_employees (empno NUMBER, ename VARCHAR2(40));
/
DECLARE
TYPE emp_rcd_type IS RECORD(
empno NUMBER
,ename VARCHAR2(40)
,gender CHAR(1));
TYPE emp_tbl_type IS TABLE OF emp_rcd_type INDEX BY BINARY_INTEGER;
TYPE index_tbl_type IS TABLE OF BINARY_INTEGER;
t_emp emp_tbl_type;
t_male_emp index_tbl_type := index_tbl_type();
t_female_emp index_tbl_type := index_tbl_type();
BEGIN
--模擬出t_emp中儲存了不同的員工資訊
t_emp(1).empno := 1;
t_emp(1).ename := 'YUSUF';
t_emp(1).gender := 'M';
t_emp(2).empno := 2;
t_emp(2).ename := 'FATIMAH';
t_emp(2).gender := 'F';
t_emp(3).empno := 3;
t_emp(3).ename := 'HAMUZA';
t_emp(3).gender := 'M';
--將男性和女性員工的索引分開加入對應的關聯陣列中
FOR k IN t_emp.first .. t_emp.last LOOP
IF t_emp(k).gender = 'M'
THEN
t_male_emp.extend;
t_male_emp(t_male_emp.last) := k;
ELSIF t_emp(k).gender = 'F'
THEN
t_female_emp.extend;
t_female_emp(t_female_emp.last) := k;
END IF;
END LOOP;
--將男性和女性員工的資訊分別存入表中
FORALL m IN VALUES OF t_male_emp
INSERT INTO cux_male_employees
(empno, ename)
VALUES
(t_emp(m).empno, t_emp(m).ename);
FORALL f IN VALUES OF t_female_emp
INSERT INTO cux_female_employees
(empno, ename)
VALUES
(t_emp(f).empno, t_emp(f).ename);
COMMIT;
END;
上例的執行結果是
SQL> SELECT * FROM cux_male_employees;
EMPNO ENAME
---------- ----------------------------------------
1 YUSUF
3 HAMUZA
SQL> SELECT * FROM cux_female_employees;
EMPNO ENAME
---------- ----------------------------------------
2 FATIMAH