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