Oracle數據庫之FORALL與BULK COLLECT語句

本文原發佈於簡書,地址爲: Oracle數據庫之FORALL與BULK COLLECT語句
更多數據庫資訊請參看github: 數據庫知識彙總

1 PL/SQL塊的執行過程

當PL/SQL運行時引擎處理一塊代碼時,它使用PL/SQL引擎來執行過程化的代碼,而將SQL語句發送給SQL引擎來執行;
SQL引擎執行完畢後,將結果再返回給PL/SQL引擎。這種在PL/SQL引擎和SQL引擎之間的交互,稱爲上下文交換(context switch)。
每發生一次交換,就會帶來必定的額外開銷。git

2 FORALL和BULK COLLECT特色

這兩個語句在PL/SQL內部進行一種數組處理,BULK COLLECT提供對數據的高速檢索,FORALL可大大改進INSERT、UPDATE和DELETE操做的性能。Oracle數據庫使用這些語句大大減小了PL/SQL與SQL語句執行引擎的環境切換次數,從而使其性能有了顯著提升。github

  1. FORALL,用於加強PL/SQL引擎到SQL引擎的交換。
  2. BULK COLLECT,用於加強SQL引擎到PL/SQL引擎的交換。
若是你要插入5000條數據,通常狀況下,在pl/sql中用for循環,循環插入5000次,而用forall一次就能夠插入5000條,提升了性能和速度。

3 FORALL介紹

使用FORALL,能夠將多個DML批量發送給SQL引擎來執行,最大限度地減小上下文交互所帶來的開銷。sql

3.1 FORALL語法

FORALL index_name IN
    { lower_bound .. upper_bound
     | INDICES OF collection_name [ BETWEEN lower_bound AND upper_bound ]
     | VALUES OF index_collection
    }
 [ SAVE EXCEPTIONS ] dml_statement;

說明:數據庫

  1. index_name:一個無需聲明的標識符,做爲集合下標使用。
  2. lower_bound .. upper_bound:數字表達式,來指定一組連續有效的索引數字下限和上限。該表達式只需解析一次。
  3. INDICES OF collection_name:用於指向稀疏數組的實際下標。跳過沒有賦值的元素,例如被 DELETE 的元素,NULL 也算值。
  4. VALUES OF index_collection_name:把該集合中的值看成下標,且該集合值的類型只能是 PLS_INTEGER/BINARY_INTEGER。
  5. SAVE EXCEPTIONS:可選關鍵字,表示即便一些DML語句失敗,直到FORALL LOOP執行完畢才拋出異常。可使用SQL%BULK_EXCEPTIONS 查看異常信息。
  6. dml_statement:靜態語句,例如:UPDATE或者DELETE;或者動態(EXECUTE IMMEDIATE)DML語句。

3.2 FORALL案例

見sqlscripts/forall-bulkcollect包下的sql腳本事例數組

3.3 FORALL注意事項

使用FORALL時,應該遵循以下規則:服務器

  1. FORALL語句的執行體,必須是一個單獨的DML語句,好比INSERT,UPDATE或DELETE。
  2. 不要顯式定義index_row,它被PL/SQL引擎隱式定義爲PLS_INTEGER類型,而且它的做用域也僅僅是FORALL。
  3. 這個DML語句必須與一個集合的元素相關,而且使用FORALL中的index_row來索引。注意不要由於index_row致使集合下標越界。
  4. lower_bound和upper_bound之間是按照步進 1 來遞增的。
  5. 在sql_statement中,不能單獨地引用集合中的元素,只能批量地使用集合。
  6. 在sql_statement中使用的集合,下標不能使用表達式。
--error statement
--1.insert into test2 values dr_table(i);dbms_output.put_line(i);不正確,找不到i,由於forall中只能使用單條語句能夠引用索引變量
--2.insert into test2 values(dr_table(i).id,dr_table(i).name);集合的field不能夠在forall中使用,必須是總體使用
--3.insert into test2 values dr_table(i+1);錯誤,不能夠對索引變量進行運算
--4.insert into test2 values(dr_table(i));報沒有足夠的值錯誤,此處外面不能夠加括號,當有多個字段的時候,單個字段能夠加括號

4 BULK COLLECT的使用

4.1 在SELECT INTO中使用BULK COLLECT

DECLARE
  -- 定義記錄類型
  TYPE EMP_REC_TYPE IS RECORD(
    EMPNO    EMP.EMPNO%TYPE,
    ENAME    EMP.ENAME%TYPE,
    HIREDATE EMP.HIREDATE%TYPE);
  -- 定義基於記錄的嵌套表
  TYPE NESTED_EMP_TYPE IS TABLE OF EMP_REC_TYPE;
  -- 聲明變量
  EMP_TAB NESTED_EMP_TYPE;
BEGIN
  -- 使用BULK COLLECT將所得的結果集一次性綁定到記錄變量emp_tab中
  SELECT EMPNO, ENAME, HIREDATE BULK COLLECT INTO EMP_TAB FROM EMP;

  FOR I IN EMP_TAB.FIRST .. EMP_TAB.LAST LOOP
    DBMS_OUTPUT.PUT_LINE('當前記錄: ' || EMP_TAB(I)
                         .EMPNO || CHR(9) || EMP_TAB(I)
                         .ENAME || CHR(9) || EMP_TAB(I).HIREDATE);
  END LOOP;
END;

說明:使用BULK COLLECT一次便可提取全部行並綁定到記錄變量,這就是所謂的批量綁定。網絡

4.2 在FETCH INTO中使用BULK COLLECT

在遊標中可使用BLUK COLLECT一次取出一個數據集合,比用遊標單條取數據效率高,尤爲是在網絡不大好的狀況下。oop

語法:性能

FETCH ... BULK COLLECT INTO ...[LIMIT row_number];

注意:fetch

  1. 在使用BULK COLLECT子句時,對於集合類型會自動對其進行初始化以及擴展。所以若是使用BULK COLLECT子句操做集合,則無需對集合進行初始化以及擴展。
  2. 因爲BULK COLLECT的批量特性,若是數據量較大,而集合在此時又自動擴展,爲避免過大的數據集形成性能降低,所以可使用LIMIT子句來限制一次提取的數據量。
    LIMIT子句只容許出如今FETCH操做語句的批量中.
DECLARE
  CURSOR EMP_CUR IS
    SELECT EMPNO, ENAME, HIREDATE FROM EMP;

  TYPE EMP_REC_TYPE IS RECORD(
    EMPNO    EMP.EMPNO%TYPE,
    ENAME    EMP.ENAME%TYPE,
    HIREDATE EMP.HIREDATE%TYPE);
  -- 定義基於記錄的嵌套表
  TYPE NESTED_EMP_TYPE IS TABLE OF EMP_REC_TYPE;
  -- 聲明集合變量
  EMP_TAB NESTED_EMP_TYPE;
  -- 定義了一個變量來做爲limit的值
  V_LIMIT PLS_INTEGER := 5;
  -- 定義變量來記錄FETCH次數
  V_COUNTER PLS_INTEGER := 0;
BEGIN
  OPEN EMP_CUR;

  LOOP
    -- fetch時使用了BULK COLLECT子句
    FETCH EMP_CUR BULK COLLECT
      INTO EMP_TAB LIMIT V_LIMIT; -- 使用limit子句限制提取數據量

    EXIT WHEN EMP_TAB.COUNT = 0; -- 注意此時遊標退出使用了emp_tab.COUNT,而不是emp_cur%notfound
    V_COUNTER := V_COUNTER + 1; -- 記錄使用LIMIT以後fetch的次數

    FOR I IN EMP_TAB.FIRST .. EMP_TAB.LAST LOOP
      DBMS_OUTPUT.PUT_LINE('當前記錄: ' || EMP_TAB(I)
                           .EMPNO || CHR(9) || EMP_TAB(I)
                           .ENAME || CHR(9) || EMP_TAB(I).HIREDATE);
    END LOOP;
  END LOOP;

  CLOSE EMP_CUR;

  DBMS_OUTPUT.PUT_LINE('總共獲取次數爲:' || V_COUNTER);
END;

4.3 在RETURNING INTO中使用BULK COLLECT

BULK COLLECT除了與SELECT,FETCH進行批量綁定以外,還能夠與INSERT,DELETE,UPDATE語句結合使用。
當與這幾個DML語句結合時,須要使用RETURNING子句來實現批量綁定。

DECLARE
  TYPE EMP_REC_TYPE IS RECORD(
    EMPNO    EMP.EMPNO%TYPE,
    ENAME    EMP.ENAME%TYPE,
    HIREDATE EMP.HIREDATE%TYPE);
  TYPE NESTED_EMP_TYPE IS TABLE OF EMP_REC_TYPE;
  EMP_TAB NESTED_EMP_TYPE;
BEGIN
  DELETE FROM EMP
   WHERE DEPTNO = 20 RETURNING EMPNO, ENAME, HIREDATE -- 使用returning 返回這幾個列
   BULK COLLECT INTO EMP_TAB; -- 將返回的列的數據批量插入到集合變量

  DBMS_OUTPUT.PUT_LINE('刪除 ' || SQL%ROWCOUNT || ' 行記錄');
  COMMIT;

  IF EMP_TAB.COUNT > 0 THEN
    -- 當集合變量不爲空時,輸出全部被刪除的元素
    FOR I IN EMP_TAB.FIRST .. EMP_TAB.LAST LOOP
      DBMS_OUTPUT.PUT_LINE('當前記錄:' || EMP_TAB(I)
                           .EMPNO || CHR(9) || EMP_TAB(I)
                           .ENAME || CHR(9) || EMP_TAB(I)
                           .HIREDATE || ' 已被刪除');
    END LOOP;
  END IF;
END;

4.4 BULK COLLECT的注意事項

  1. BULK COLLECT INTO 的目標對象必須是集合類型。
  2. 只能在服務器端的程序中使用BULK COLLECT,若是在客戶端使用,就會產生一個不支持這個特性的錯誤。
  3. 不能對使用字符串類型做鍵的關聯數組使用BULK COLLECT子句。
  4. 複合目標(如對象類型)不能在RETURNING INTO子句中使用。
  5. 若是有多個隱式的數據類型轉換的狀況存在,多重複合目標就不能在BULK COLLECT INTO子句中使用。
  6. 若是有一個隱式的數據類型轉換,複合目標的集合(如對象類型集合)就不能用於BULK COLLECTINTO子句中

5 FORALL與BULK COLLECT綜合運用

FORALL與BULK COLLECT是實現批量SQL的兩個重要方式,咱們能夠將其結合使用以提升性能.

-- create tb_emp_test
 CREATE TABLE tb_emp_test AS
    SELECT empno, ename, hiredate
   FROM   EMP_TEST
   WHERE  1 = 0;

 DECLARE
   -- declare cursor
   CURSOR EMP_CUR IS
     SELECT EMPNO, ENAME, HIREDATE FROM EMP_TEST;
   -- 基於遊標的嵌套表類型
   TYPE NESTED_EMP_TYPE IS TABLE OF EMP_CUR%ROWTYPE;
   -- 聲明變量
   EMP_TAB NESTED_EMP_TYPE;
 BEGIN
   SELECT EMPNO, ENAME, HIREDATE BULK COLLECT
     INTO EMP_TAB
     FROM EMP_TEST
    WHERE SAL > 1000;

   -- 使用FORALL語句將變量中的數據插入到表tb_emp
   FORALL I IN 1 .. EMP_TAB.COUNT
     INSERT INTO
       (SELECT EMPNO, ENAME, HIREDATE FROM TB_EMP_TEST)
     VALUES EMP_TAB
       (I);

   COMMIT;
   DBMS_OUTPUT.PUT_LINE('總共向 tb_emp 表中插入記錄數: ' || EMP_TAB.COUNT);
 END;

6 總結

  1. limit減小內存佔用,若是數據量較大一次性所有加載到內存中,對PGA來講壓力太大,可採用limit的方法一次加載必定數量的數據,建議值一般爲1000。使用limit時注意,循環的時候若是用while cursor_name%found loop,對於最後一次fetch的數據量不足設定值1000,%found條件就會不成立。示例使用v_oid_lst.count > 0做爲判斷條件。
  2. 在寫plsql代碼塊,定義數值變量時,建議採用pls_integer類型,或者simple_integer類型。二者的區別:
Oracle9i以前有binary_integer類型,和11g中引入的pls_integer數值範圍相同:-2147483647~+2147483647,但pls_integer有更高的性能。二者性能均優於number類型。
Oracle中也引入了simple_integer類型,不過不能包含null值,範圍:-2147483648~2147483647,性能優於pls_integer。
  1. 使用ref cursor。
  2. 使用綁定變量。
  3. 自定義table類型。
  4. Bulk collect into加載到內存中,處理完業務邏輯後forall批量插入到數據表中。
  5. Forall可使用returning bulk collect into,且可以使用sql%rowcount返回其更新行數。
  6. type numbers is table of number index by binary_integer/pls_integer/simple_integer; 其做用是:
1. 加了"index by binary_integer "後,numbers類型的下標就是自增加,numbers類型在插入元素時,不須要初始化,不須要每次extend增長一個空間。
2. 若是沒有這句話"index by binary_integer",那就得要顯示對初始化,且每插入一個元素到numbers類型的table中時,都須要先extend。
相關文章
相關標籤/搜索