1. 程式人生 > >FORALL在資料批量處理中的使用

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