批量 SQL 之 FORALL 語句

來源:互聯網
上載者:User
    對PL/SQL而言,任何的PL/SQL塊或者子程式都是PL/SQL引擎來處理,而其中包含的SQL語句則由PL/SQL引擎發送SQL語句轉交到SQL引擎來處
理,SQL引擎處理完畢後向PL/SQL引擎返回資料。Pl/SQL與SQL引擎之間的通訊則稱之為環境切換。過多的環境切換將帶來過量的效能負載。
因此為減少效能的FORALL與BULK COLLECT的子句應運而生。即僅僅使用一次切換多次執行來降低環境切換次數。本文主要描述FORALL子句。

一、FORALL文法描述

    FORALL loop_counter IN bounds_clause            -->注意FORALL塊內不需要使用loop, end loop
    SQL_STATEMENT [SAVE EXCEPTIONS];
    
    bounds_clause的形式
    lower_limit .. upper_limit                                     -->指明迴圈計數器的上限和下限,與for迴圈類似
    INDICES OF collection_name BETWEEN lower_limit .. upper_limit  -->引用特定集合元素的下標(該集合可能為稀疏)
    VALUES OF colletion_name                                       -->引用特定集合元素的值
    
    SQL_STATEMENT部分:SQL_STATEMENT部分必須是一個或者多個集合的靜態或者動態DML(insert,update,delete)語句。
    SAVE EXCEPTIONS部分:對於SQL_STATEMENT部分導致的異常使用SAVE EXCEPTIONS來保證異常存在時語句仍然能夠繼續執行。

二、使用 FORALL 代替 FOR 迴圈提高效能

-->下面的樣本使用了FOR迴圈與FORALL迴圈操作進行對比,使用FORALL完成同樣的功能,效能明顯提高CREATE TABLE t(   col_num   NUMBER  ,col_var   VARCHAR2( 10 ));DECLARE   TYPE col_num_type IS TABLE OF NUMBER            -->聲明了兩個聯合數組                           INDEX BY PLS_INTEGER;   TYPE col_var_type IS TABLE OF VARCHAR2( 10 )                           INDEX BY PLS_INTEGER;   col_num_tab    col_num_type;   col_var_tab    col_var_type;   v_start_time   INTEGER;   v_end_time     INTEGER;BEGIN   FOR i IN 1 .. 5000                    -->使用FOR迴圈向數組填充元素   LOOP      col_num_tab( i ) := i;      col_var_tab( i ) := 'var_' || i;   END LOOP;   v_start_time := DBMS_UTILITY.get_time;  -->獲得FOR迴圈向表t插入資料前的初始時間   FOR i IN 1 .. 5000                   -->使用FOR迴圈向表t插入資料   LOOP      INSERT INTO t      VALUES ( col_num_tab( i ), col_var_tab( i ) );   END LOOP;   v_end_time  := DBMS_UTILITY.get_time;   DBMS_OUTPUT.put_line( 'Duration of the FOR LOOP: ' || ( v_end_time - v_start_time ) );   v_start_time := DBMS_UTILITY.get_time;   FORALL i IN 1 .. 5000          -->使用FORALL迴圈向表t插入資料      INSERT INTO t      VALUES ( col_num_tab( i ), col_var_tab( i ) );   v_end_time  := DBMS_UTILITY.get_time;   DBMS_OUTPUT.put_line( 'Duration of the FORALL STATEMENT: ' || ( v_end_time - v_start_time ) );   COMMIT;END;Duration of the FOR LOOP: 68           -->此處的計時單位為百分之一秒,即0.68s,下同Duration of the FORALL STATEMENT: 18PL/SQL procedure successfully completed.

三、SAVE EXCEPTIONS
         對於任意的SQL語句執行失敗,將導致整個語句或整個事務會滾。而使用SAVE EXCEPTIONS可以使得在對應的SQL語句異常的情形下,FORALL
仍然可以繼續執行。如果忽略了SAVE EXCEPTIONS時,當異常發生,FORALL語句就會停止執行。因此SAVE EXCEPTIONS使得FORALL子句中的DML下
產生的所有異常都將記錄在SQL%BULK_EXCEPTIONS的遊標屬性中。SQL%BULK_EXCEPTIONS屬性是個記錄集合,其中的每條記錄由兩個欄位組成,

ERROR_INDEX和ERROR_CODE。ERROR_INDEX欄位會儲存發生異常的FORALL語句的迭代編號,而ERROR_CODE則儲存對應異常的ORACLE錯誤碼。類似於這樣:(2,01400),(6,1476)和(10,12899)。存放在%BULK_EXCEPTIONS中的值總是與最近一次FORALL語句執行的結果相關,異常的個數存放在%BULK_EXCEPTIONS的COUNT屬性中,%BULK_EXCEPTIONS有效下標索引範圍在1到%BULK_EXCEPTIONS.COUNT之間。

1、%BULK_EXCEPTIONS的用法CREATE TABLE tb_emp AS              -->建立表tb_emp   SELECT empno, ename, hiredate   FROM   emp   WHERE  1 = 2;ALTER TABLE tb_emp MODIFY(empno NOT NULL);   -->為表添加約束DECLARE   TYPE col_num_type IS TABLE OF NUMBER            -->一共定義了3個聯合數群組類型                           INDEX BY PLS_INTEGER;   TYPE col_var_type IS TABLE OF VARCHAR2( 100 )                           INDEX BY PLS_INTEGER;   TYPE col_date_type IS TABLE OF DATE                            INDEX BY PLS_INTEGER;   empno_tab      col_num_type;   ename_tab      col_var_type;   hiredate_tab   col_date_type;   v_counter      PLS_INTEGER := 0;   v_total        INTEGER := 0;   errors         EXCEPTION;                      -->聲明異常   PRAGMA EXCEPTION_INIT( errors, -24381 );BEGIN   FOR rec IN ( SELECT empno, ename, hiredate FROM emp )   -->使用for迴圈將資料填充到聯合數組   LOOP      v_counter   := v_counter + 1;      empno_tab( v_counter ) := rec.empno;      ename_tab( v_counter ) := rec.ename;      hiredate_tab( v_counter ) := rec.hiredate;   END LOOP;   empno_tab( 2 ) := NULL;                                -->對部分資料進行處理以產生異常   ename_tab( 5 ) := RPAD( ename_tab( 5 ), 15, '*' );   empno_tab( 10 ) := NULL;   FORALL i IN 1 .. empno_tab.COUNT                      -->使用forall將聯合數組中的資料插入到表tb_emp   SAVE EXCEPTIONS      INSERT INTO tb_emp      VALUES ( empno_tab( i ), ename_tab( i ), hiredate_tab( i ) );   COMMIT;   SELECT COUNT( * ) INTO v_total FROM tb_emp;   DBMS_OUTPUT.put_line( v_total || ' rows were inserted to tb_emp' );EXCEPTION   WHEN errors THEN      DBMS_OUTPUT.put_line( 'There are ' || SQL%bulk_exceptions.COUNT || ' exceptions' );      FOR i IN 1 .. SQL%bulk_exceptions.COUNT            -->SQL%bulk_exceptions.COUNT記錄異常個數來控制迭代      LOOP         DBMS_OUTPUT.          put_line(                       'Record '                    || SQL%bulk_exceptions( i ).error_index                    || ' caused error '                    || i                    || ': '                    || SQL%bulk_exceptions( i ).error_code                    || ' '                    || SQLERRM( -SQL%bulk_exceptions( i ).error_code ) );   -->使用SQLERRM根據錯誤號碼拋出具體的錯誤資訊      END LOOP;END;There are 3 exceptionsRecord 2 caused error 1: 1400 ORA-01400: cannot insert NULL into ()Record 5 caused error 2: 12899 ORA-12899: value too large for column  (actual: , maximum: )Record 10 caused error 3: 1400 ORA-01400: cannot insert NULL into ()PL/SQL procedure successfully completed.2、%BULK_ROWCOUNT%BULK_ROWCOUNT也是專門為FORALL設計的,用於儲存第i個元素第i次insert或update或delete所影響到的行數。如果第i次操作沒有行被影響,則%BULK_ROWCOUNT返回為零值。FORALL語句和%BULK_ROWCOUNT屬性使用同樣的下標索引。如果FORALL使用下標索引的範圍在5到8的話,那麼%BULK_ROWCOUNT的也是5到8。需要注意的是一般情況下,對於insert .. values而言,所影響的行數為1,即%BULK_ROWCOUNT的值為1。而對於insert .. select方式而言,%BULK_ROWCOUNT的值就有可能大於1。update與delete語句存在0,1,以及大於1的情形。DECLARE   TYPE dept_tab_type IS TABLE OF NUMBER;   dept_tab   dept_tab_type := dept_tab_type( 10, 20, 50 );    -->聲明及初始化巢狀表格BEGIN   FORALL i IN dept_tab.FIRST .. dept_tab.LAST                 -->使用FORALL更新      UPDATE emp      SET    sal          = sal * 1.10      WHERE  deptno = dept_tab( i );   -- COMMIT;   FOR i IN 1 .. dept_tab.COUNT                               -->迴圈輸出每次執行SQL語句影響的行數   LOOP      DBMS_OUTPUT.put_line( 'Dept no ' || dept_tab( i ) || ' has ' || SQL%bulk_rowcount (i) || ' rows been updated' );   END LOOP;   -- Did the 3rd UPDATE statement affect any rows?   IF SQL%bulk_rowcount (3) = 0 THEN      DBMS_OUTPUT.put_line( 'The deptno 50 has not child record' );   END IF;END;Dept no 10 has 3 rows been updatedDept no 20 has 5 rows been updatedDept no 50 has 0 rows been updatedThe deptno 50 has not child recordPL/SQL procedure successfully completed.

四、INDICES OF 選項
    INDICES OF 選項用於處理稀疏集合類型。即當集合(巢狀表格或聯合數組)中的元素被刪除之後,對稀疏集合實現迭代。

-->下面的指令碼同前面的樣本基本相似,所不同的是使用了delete方式刪除其中的部分記錄,導致集合變得稀疏。-->其次在forall子句處使用indices OF方式來控制迴圈。TRUNCATE TABLE tb_emp;DECLARE   TYPE col_num_type IS TABLE OF NUMBER                           INDEX BY PLS_INTEGER;   TYPE col_var_type IS TABLE OF VARCHAR2( 100 )                           INDEX BY PLS_INTEGER;   TYPE col_date_type IS TABLE OF DATE                            INDEX BY PLS_INTEGER;   empno_tab      col_num_type;   ename_tab      col_var_type;   hiredate_tab   col_date_type;   v_counter      PLS_INTEGER := 0;   v_total        INTEGER := 0;BEGIN   FOR rec IN ( SELECT empno, ename, hiredate FROM emp )   LOOP      v_counter   := v_counter + 1;      empno_tab( v_counter ) := rec.empno;      ename_tab( v_counter ) := rec.ename;      hiredate_tab( v_counter ) := rec.hiredate;   END LOOP;   empno_tab.delete( 2 );       -->此處刪除了數組中的第二個元素,導致數組變為稀疏型   ename_tab.delete( 2 );   hiredate_tab.delete( 2 );   FORALL i IN indices OF empno_tab   -->此處使用了indices OF empno_tab,則所有未被delete的元素都將進入迴圈      INSERT INTO tb_emp      VALUES ( empno_tab( i ), ename_tab( i ), hiredate_tab( i ) );   COMMIT;   SELECT COUNT( * ) INTO v_total FROM tb_emp;   DBMS_OUTPUT.put_line( v_total || ' rows were inserted to tb_emp' );END;13 rows were inserted to tb_empPL/SQL procedure successfully completed.

五、VALUES OF 選項
    VALUES OF選項可以指定FORALL語句中迴圈計數器的值來自於指定集合中元素的值。
    VALUES OF選項使用時有一些限制
          如果VALUES OF子句中所使用的集合是聯合數組,則必須使用PLS_INTEGER和BINARY_INTEGER進行索引
          VALUES OF 子句中所使用的元素必須是PLS_INTEGER或BINARY_INTEGER
          當VALUES OF 子句所引用的集合為空白,則FORALL語句會導致異常               

TRUNCATE TABLE tb_emp;CREATE TABLE tb_emp_ins_log AS                    -->建立一張與tb_emp結構類似的表tb_emp_ins_log   SELECT *   FROM   tb_emp   WHERE  1 = 0;ALTER TABLE tb_emp_ins_log MODIFY(ename VARCHAR2(50));   -->修改列ename的長度DECLARE   TYPE col_num_type IS TABLE OF tb_emp.empno%TYPE                           INDEX BY PLS_INTEGER;   TYPE col_var_type IS TABLE OF VARCHAR2( 100 )                           INDEX BY PLS_INTEGER;   TYPE col_date_type IS TABLE OF tb_emp.hiredate%TYPE                            INDEX BY PLS_INTEGER;   TYPE ins_log_type IS TABLE OF PLS_INTEGER         -->此處較之前的樣本多聲明了一個聯合數組                           INDEX BY PLS_INTEGER;     -->用於填充異常記錄的元素值   empno_tab      col_num_type;   ename_tab      col_var_type;   hiredate_tab   col_date_type;   ins_log_tab    ins_log_type;   v_counter      PLS_INTEGER := 0;   v_total        INTEGER := 0;   errors         EXCEPTION;   PRAGMA EXCEPTION_INIT( errors, -24381 );BEGIN   FOR rec IN ( SELECT empno, ename, hiredate FROM emp )   LOOP      v_counter   := v_counter + 1;      empno_tab( v_counter ) := rec.empno;      ename_tab( v_counter ) := rec.ename;      hiredate_tab( v_counter ) := rec.hiredate;   END LOOP;   ename_tab( 2 ) := RPAD( ename_tab( 2 ), 15, '*' );    -->使記錄2與記錄5的ename列長度變長而產生異常   ename_tab( 5 ) := RPAD( ename_tab( 5 ), 15, '*' );   empno_tab( 6 ) := NULL;                          -->使第6條記錄的empno為NULL值,由於表tb_emp的empno不允許為NULL而產生異常   FORALL i IN 1 .. empno_tab.COUNT   SAVE EXCEPTIONS      INSERT INTO tb_emp      VALUES ( empno_tab( i ), ename_tab( i ), hiredate_tab( i ) );   COMMIT;EXCEPTION   WHEN errors THEN      FOR i IN 1 .. SQL%bulk_exceptions.COUNT      LOOP         ins_log_tab( i ) := SQL%bulk_exceptions( i ).error_index;   -->異常記錄的索引值將填充ins_log_type聯合數組      END LOOP;                                    -->此處的結果是ins_log_tab(1)=2,  ins_log_tab(2)=5,  ins_log_tab(2)=6      FORALL i IN VALUES OF ins_log_tab   -->使用VALUES OF子句為ins_log_type聯合數組中的元素值         INSERT INTO tb_emp_ins_log         VALUES ( empno_tab( i ), ename_tab( i ), hiredate_tab( i ) );  -->因此values中的i分別為2和5      COMMIT;END;PL/SQL procedure successfully completed.-->異常的記錄被插入到表tb_emp_ins_logselect * from tb_emp_ins_log;     EMPNO ENAME                                              HIREDATE---------- -------------------------------------------------- ---------      7369 Henry**********                                    17-DEC-80      7566 JONES**********                                    02-APR-81           MARTIN                                             28-SEP-81

六、INDICES OF 與 VALUES OF 的綜合運用

-->下面的例子來自Oraclehttp://docs.oracle.com/cd/B19306_01/appdev.102/b14261/tuning.htm-- Create empty tables to hold order detailsCREATE TABLE valid_orders(   cust_name   VARCHAR2( 32 )  ,amount      NUMBER( 10, 2 ));CREATE TABLE big_orders AS   SELECT *   FROM   valid_orders   WHERE  1 = 0;CREATE TABLE rejected_orders AS   SELECT *   FROM   valid_orders   WHERE  1 = 0;DECLARE   -- Make collections to hold a set of customer names and order amounts.   SUBTYPE cust_name IS valid_orders.cust_name%TYPE;   TYPE cust_typ IS TABLE OF cust_name;   cust_tab             cust_typ;   SUBTYPE order_amount IS valid_orders.amount%TYPE;   TYPE amount_typ IS TABLE OF NUMBER;   amount_tab           amount_typ;   -- Make other collections to point into the CUST_TAB collection.   TYPE index_pointer_t IS TABLE OF PLS_INTEGER;   big_order_tab        index_pointer_t := index_pointer_t( );   rejected_order_tab   index_pointer_t := index_pointer_t( );   PROCEDURE setup_data IS   BEGIN      -- Set up sample order data, including some invalid orders and some 'big' orders.      cust_tab    :=         cust_typ( 'Company1','Company2','Company3','Company4','Company5' );      amount_tab  :=         amount_typ( 5000.01,0,150.25,4000.00,NULL );   END;BEGIN   setup_data( );   DBMS_OUTPUT.put_line( '--- Original order data ---' );   FOR i IN 1 .. cust_tab.LAST   LOOP      DBMS_OUTPUT.put_line( 'Customer #' || i || ', ' || cust_tab( i ) || ': $' || amount_tab( i ) );   END LOOP;   -- Delete invalid orders (where amount is null or 0).   FOR i IN 1 .. cust_tab.LAST   LOOP      IF amount_tab( i ) IS NULL OR amount_tab( i ) = 0 THEN         cust_tab.delete( i );         amount_tab.delete( i );      END IF;   END LOOP;   DBMS_OUTPUT.put_line( '--- Data with invalid orders deleted ---' );   FOR i IN 1 .. cust_tab.LAST   LOOP      IF cust_tab.EXISTS( i ) THEN         DBMS_OUTPUT.put_line( 'Customer #' || i || ', ' || cust_tab( i ) || ': $' || amount_tab( i ) );      END IF;   END LOOP;   -- Because the subscripts of the collections are not consecutive, use   -- FORALL...INDICES OF to iterate through the actual subscripts,   -- rather than 1..COUNT   FORALL i IN indices OF cust_tab      INSERT INTO valid_orders( cust_name, amount )      VALUES ( cust_tab( i ), amount_tab( i ) );   -- Now process the order data differently   -- Extract 2 subsets and store each subset in a different table   setup_data( );                    -- Initialize the CUST_TAB and AMOUNT_TAB collections again.   FOR i IN cust_tab.FIRST .. cust_tab.LAST   LOOP      IF amount_tab( i ) IS NULL OR amount_tab( i ) = 0 THEN         rejected_order_tab.EXTEND;                          -- Add a new element to this collection         -- Record the subscript from the original collection         rejected_order_tab( rejected_order_tab.LAST ) := i;      END IF;      IF amount_tab( i ) > 2000 THEN         big_order_tab.EXTEND;                            -- Add a new element to this collection         -- Record the subscript from the original collection         big_order_tab( big_order_tab.LAST ) := i;      END IF;   END LOOP;   -- Now it's easy to run one DML statement on one subset of elements,   -- and another DML statement on a different subset.   FORALL i IN VALUES OF rejected_order_tab      INSERT INTO rejected_orders      VALUES ( cust_tab( i ), amount_tab( i ) );   FORALL i IN VALUES OF big_order_tab      INSERT INTO big_orders      VALUES ( cust_tab( i ), amount_tab( i ) );   COMMIT;END;--- Original order data ---Customer #1, Company1: $5000.01Customer #2, Company2: $0Customer #3, Company3: $150.25Customer #4, Company4: $4000Customer #5, Company5: $--- Data with invalid orders deleted ---Customer #1, Company1: $5000.01Customer #3, Company3: $150.25Customer #4, Company4: $4000PL/SQL procedure successfully completed.SELECT cust_name "Customer", amount "Valid order amount" FROM valid_orders;Customer                         Valid order amount-------------------------------- ------------------Company1                                    5000.01Company3                                     150.25Company4                                       4000SELECT cust_name "Customer", amount "Big order amount" FROM big_orders;Customer                         Big order amount-------------------------------- ----------------Company1                                  5000.01Company4                                     4000SELECT cust_name "Customer", amount "Rejected order amount" FROM rejected_orders;Customer                         Rejected order amount-------------------------------- ---------------------Company2                                             0Company5--Author: Robinson Cheng--Blog : http://blog.csdn.net/robinson_0612     --上面的例子對訂單進行分類,並將其儲存到三張不同類型的表中。--1、首先定義了兩個巢狀表格cust_tab,amount_tab用於儲存未經處理資料,setup_data( )則用來初始化資料。--2、第一個for迴圈用於輸出所有的訂單,第二個for迴圈則用來將刪除amount_tab中為NULL或0值的記錄。--3、第三個for迴圈則用來輸出經過刪除之後剩餘的記錄,使用exists方法判斷。--4、使用forall子句將所有有效記錄插入到valid_orders,注意此時使用了indices of,因此此時的兩個巢狀表格已為稀疏表。--5、在這之後,使用setup_data( )重新初始化資料。--6、將無效訂單的下標記錄到rejected_order_tab巢狀表格,將amount > 2000訂單的下標記錄到big_order_tab。--7、使用VALUES OF 子句將兩個巢狀表格中對應下表的記錄插入到對應的表中。

七、更多參考

批量 SQL 之 BULK COLLECT 子句

PL/SQL 集合的初始化與賦值
PL/SQL 聯合數組與巢狀表格
PL/SQL 變長數組
PL/SQL --> PL/SQL記錄

SQL tuning 步驟

高效SQL語句必殺技

父遊標、子遊標及共用遊標

綁定變數及其優缺點

dbms_xplan之display_cursor函數的使用

dbms_xplan之display函數的使用

執行計畫中各欄位各模組描述

使用 EXPLAIN PLAN 擷取SQL語句執行計畫

啟用 AUTOTRACE 功能

函數使得索引列失效

Oracle 綁定變數窺探

Oracle 自適應共用遊標                     

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.