Reducing Loop Overhead for DML Statements and Queries with Bulk SQL(10gr2)

PL/SQL运行SQL的机制是把SQL语句,比如DML,SELECT,发送给SQL 引擎。然后SQL引擎把SQL语句的结果返回给PL/SQL。想象一下,下面这样的PL/SQL块。

FOR j IN 1..99999999 LOOP
  VAR1[i]=i;
  INSERT INTO TEST VALUES(VAR[i]);
END LOOP;

这个PL/SQL块非常简洁明了,它循环9999999次,每一次循环都把 i 赋值到 VAR1[i]中,然后把这个VAR1[ i ] insert到表TEST中。这么简单的SQL却有着非常严重的性能问题。因为他要循环99999999次,每一次循环PL/SQL引擎都像SQL引擎发送一条INSERT 语句,这期间的通讯开销会导致运行很慢。 Oracle对这种现象的解决方案是使用批量SQL,比如,FORALL,BULK COLLECT。

首先,我们看一下FORALL。 这里我们分为2部分讲,

普通的FORALL

 这里我们做一个比较,先是用普通的FOR把一万条数据插入一个表A中,记录一下时间。然后把相同的一万条数据插入同一个表A中,但这次用FORALL。我们看一下哪个快。

 1 CREATE OR REPLACE PROCEDURE for_vs_forall
 2 AS
 3   timer1 INTEGER;
 4   timer2 INTEGER;
 5   
 6   TYPE num_tab_type IS TABLE OF NUMBER INDEX BY  binary_integer ;
 7   num_table num_tab_type;
 8 BEGIN
 9   /*初始化 num_table*/
10   FOR i IN 1..9999 LOOP
11     num_table(i) := i;
12   END LOOP;
13   
14   /*在FOR循环里向A中insert数据,并在循环开始前和循环结束后记录时间,算出循环一共花的时间*/
15   
16   timer1 := DBMS_UTILITY.get_time;
17   FOR i IN 1..9999 LOOP
18     num_table(i) := i;
19     insert into A values(num_table(i));
20   END LOOP;
21   timer2 := DBMS_UTILITY.get_time;
22   DBMS_OUTPUT.PUT_LINE('Execution Time (secs)');
23   DBMS_OUTPUT.PUT_LINE('FOR loop: ' || TO_CHAR((timer2 - timer1)/100));
24   
25   
26   /*清空A表*/
27   
28   execute immediate 'delete from A';
29   
30   
31   /*在FORALL循环里向A中insert数据,并在循环开始前和循环结束后记录时间,算出循环一共花的时间*/
32   
33   timer1 := DBMS_UTILITY.get_time;
34   FORALL i IN 1..9999 
35     INSERT INTO A VALUES(num_table(i));
36   timer2 := DBMS_UTILITY.get_time;
37   DBMS_OUTPUT.PUT_LINE('Execution Time (secs)');
38   DBMS_OUTPUT.PUT_LINE('FORALL loop: ' || TO_CHAR((timer2 - timer1)/100));
39   
40 END;
41     

简单讲解一下上面的测试,9-10行是初始化num_table这个集合。这里有个小问题,就是如果我在声明这个集合的时候没有指定指定INDEX BY BINARY_INTEGER,那么这里就会报一个ORA-06531: Reference to uninitialized collection的错误。 这个需要研究一下,但我们先跳过这里。 16-23行是通过FOR循环往A表中insert数据,并且在循环开始和循环结束都记录时间以便算出FOR循环的时间。31-38通过FORALL insert数据。在循环开始前和循环结束后都记录并计算时间。  最后看一下两种循环哪个更快。

1 SQL> exec for_vs_forall
2 Execution Time (secs)
3 FOR loop: 2.95
4 Execution Time (secs)
5 FORALL loop: .05
6 
7 PL/SQL procedure successfully completed.

 运行一下,结果如上所示,FORALL非常快。

INDICES OF 和 VALUES OF

/*首先创建三个表,
valid_orders 记录了customer name 以及对应的 order数量
big_orders  记录了order 数量比较大的customer name 以及order数量
rejected_orders 如果有不符合标准的数据,比如 order 数量为NULL 或者0,则把 customer name 和order数量记录进这个表*/
CREATE 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;


CREATE OR REPLACE PROCEDURE test_forall
AS
  /*创建两个集合用来存储一系列的customer name 和 order amout 这两个集合类似于数组,相同下表的
  两个元素就组成了一对儿数据对儿,可以存进 valid_order这个表中。 比如 cust_tab(1), amount_tab(1)*/
  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 order_amount;
  amount_tab amount_typ;
  
  /*创建两个数据结构,
  big_order_tab 这个结构里面每一个元素对应着一对customer name和order amount的组合,
  如果里面存着(8,10)那么就意味着第8,10对儿 customer name,order amount应该存进 big_order表
  regected_order_tab记录了应该存进rejected_order的数据对儿*/ 
  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();
  
  /*初始化cust_tab 和 amount_tab 生成一些  customer name 和 order amount的数据对儿*/
  PROCEDURE setup_data IS BEGIN 
    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来初始化,生成一些 customer name 和 order amount*/
  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;
  
  /*如果有一些记录不符合标准,比如order amount=0 或者 null,就把对应的数据对儿删掉。*/
  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;
  
  /*输出一下现在的数据, 因为有些数据被删除了,所以输出之前需要用 
  IF cust_tab.EXISTS(i)确定数据是否存*/
  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;
  
  /*注意这里才是重点,因为cust_tab里有一些数据被删除了,这时候如果你还用
  FORALL i IN cust_tab.FIRST .. cust_tab.LAST
  或者
  FORALL i IN 1 .. count
  这两种形式,就会出错。 会碰到ORA-22160: element at index [2] does not exist这种错误,
  所以需要下面的这种执行方式。
  */
  FORALL i IN INDICES OF cust_tab
    INSERT INTO valid_orders(cust_name, amount) VALUES(cust_tab(i), amount_tab(i));
    

  /*下面重新初始化数据,学习一下 values of*/
  setup_data();
  
  /*这里遍历一下cust_tab 以及 amount_tab这两个表,如果甄别数据,把对应的数据放入big_order 
  或者rejected_order*/
  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;
      rejected_order_tab(rejected_order_tab.LAST) := i;
    END IF;
    IF amount_tab(i) > 2000 THEN
      big_order_tab.EXTEND;
      big_order_tab(big_order_tab.LAST) := i;
    END IF;
  END LOOP;
  
  
  /*这里要注意一个前提就是 big_order_tab中存储的内容是指向cust_tab和order_tab的指针,所以
  如果你还像以前这样写
  FORALL i IN  big_order_tab.FIRST .. big_order_tab.LAST
    INSERT INTO big_orders VALUES (cust_tab(i), amount_tab(i));
  那么你插入的数据就错了。因为这时候的i是 big_order_tab的下标而不是内容,你需要这样写 
  */
  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;

 运行一下

SQL> delete from valid_orders;

0 rows deleted.

SQL> delete from big_orders;

4 rows deleted.

SQL> delete from rejected_orders;

0 rows deleted.

SQL> commit;

Commit complete.


SQL> EXEC test_forall
--- Original order data ---
Customer #1, Company1: $5000.01
Customer #2, Company2: $0
Customer #3, Company3: $150.25
Customer #4, Company4: $4000
Customer #5, Company5: $
--- Data with invalid orders deleted ---
Customer #1, Company1: $5000.01
Customer #3, Company3: $150.25
Customer #4, Company4: $4000

PL/SQL procedure successfully completed.

SQL> select * from valid_orders;

CUST_NAME                                                                                            AMOUNT
------------------------------------------------------------------------------------------------ ----------
Company1                                                                                            5000.01
Company3                                                                                             150.25
Company4                                                                                               4000

SQL> select * from big_orders;

CUST_NAME                                                                                            AMOUNT
------------------------------------------------------------------------------------------------ ----------
Company1                                                                                            5000.01
Company4                                                                                               4000

SQL> select * from rejected_orders;

CUST_NAME                                                                                            AMOUNT
------------------------------------------------------------------------------------------------ ----------
Company2                                                                                                  0
Company5
原文地址:https://www.cnblogs.com/kramer/p/3042063.html