Thursday, January 21, 2016

FORALL BULK COLLECT Performance Test in Oracle



Examples to Test the BULK Collect:
Example-1:
Test to compare the performance of populating plsql table type variable using for loop cursor and bulk collect.
declare
    type bulk_test_mah_tab is table of bulk_test_mah%rowtype;
    v_bulk_test_mah_tab bulk_test_mah_tab := bulk_test_mah_tab();
    v_start_time  pls_integer;
begin
    v_start_time :=dbms_utility.get_time;

    for i in (select * from bulk_test_mah) loop
        v_bulk_test_mah_tab.extend; -- to increment one cell to hold new values       
        v_bulk_test_mah_tab (v_bulk_test_mah_tab.last) := i;
    end loop;
    dbms_output.put_line ('Time taken by for loop -> ' || (dbms_utility.get_time - v_start_time ) ||' for rows -'|| v_bulk_test_mah_tab.count);
    rollback;

    v_start_time :=dbms_utility.get_time;
    select * bulk collect into v_bulk_test_mah_tab from bulk_test_mah;
    dbms_output.put_line ('Time taken by bulk collect ->' || (dbms_utility.get_time - v_start_time) || ' for rows -' || v_bulk_test_mah_tab.count);

exception
    when others then
        dbms_output.put_line ('error -' || sqlerrm );
end;

output1:
Time taken by for loop -> 30 for rows -57958
Time taken by bulk collect ->10 for rows -57958

Conclusion -1: It is proven with above example-1 that populating plsql table type with bulk collect is much more efficient than populating it using for loop.
------------------------------------------------------------------------------------------------
Example-2:
Test to compare the time taken by select bulk collect into and for all statement with DML associated with it.
declare
    st_time pls_integer ;
    type ord_mah_tab is table of ord_mah.order_no%type;
    v_ord_mah_tab ord_mah_tab;
begin
    st_time := dbms_utility.get_time;
    dbms_output.put_line ('st_time 1 is -> ' || st_time );

select order_no bulk collect into v_ord_mah_tab from ord_mah;
dbms_output.put_line ('Time taken for select with bulk collect ->' || (dbms_utility.get_time - st_time ) );

    st_time := dbms_utility.get_time;
    dbms_output.put_line ('st_time 2 is -> ' || st_time );

    forall i in v_ord_mah_tab.first..v_ord_mah_tab.count
    update ord_mah set customer_po_no = substr(customer_po_no,1,7) where order_no = v_ord_mah_tab(i);
    dbms_output.put_line ('Time taken for exeuction using forall  ->' || (dbms_utility.get_time - st_time ) );
    rollback;

exception
    when others then
        dbms_output.put_line ( dbms_utility.format_error_stack || '-' || dbms_utility.format_error_backtrace );
end;

output2:
st_time 1 is -> 431409877
Time taken for select with bulk collect ->4
st_time 2 is -> 431409881
Time taken for exeuction using forall  ->25009
Conclusion -2:
From the above test, it is proven that select bulk collect into is executed in no time and the over head is associated with update statement in forall.
------------------------------------------------------------------------------------------------
Example-3:
Test to compare DML using direct update, using for loop and using FORALL statements
declare
    st_time pls_integer ;
    type ord_mah_tab is table of ord_mah.order_no%type;
    v_ord_mah_tab ord_mah_tab;
begin

    st_time := dbms_utility.get_time;
    dbms_output.put_line ('st_time 1 is -> ' || st_time );

    update ord_mah set customer_po_no = substr(customer_po_no,1,7);
    dbms_output.put_line ('Time taken for exeuction using direct update ->' || (dbms_utility.get_time - st_time ) );
    rollback;

    st_time := dbms_utility.get_time;
    dbms_output.put_line ('st_time 2 is -> ' || st_time );

    for i in (select order_no from ord_mah) loop
        update ord_mah set customer_po_no = substr(customer_po_no,1,7) where order_no = i.order_no;
    end loop;
    dbms_output.put_line ('Time taken for exeuction using for loop ->' || (dbms_utility.get_time - st_time ) );
    rollback;

    st_time := dbms_utility.get_time;
    dbms_output.put_line ('st_time 3 is -> ' || st_time );

    select order_no bulk collect into v_ord_mah_tab from ord_mah;

    forall i in v_ord_mah_tab.first..v_ord_mah_tab.count
    update ord_mah set customer_po_no = substr(customer_po_no,1,7) where order_no = v_ord_mah_tab(i);
    dbms_output.put_line ('Time taken for exeuction using forall  ->' || (dbms_utility.get_time - st_time ) );
    rollback;

exception
    when others then
        dbms_output.put_line ( dbms_utility.format_error_stack || '-' || dbms_utility.format_error_backtrace );
end;
output3:
st_time 1 is -> 431311406
Time taken for exeuction using direct update ->137
st_time 2 is -> 431311654
Time taken for exeuction using for loop ->22853
st_time 3 is -> 431334572
Time taken for exeuction using forall  ->23074


------------------------------------------------------------------------------------------------
LIMIT Clause:
Remember that collections are held in memory, so doing a bulk collect from a large query could cause a considerable performance problem. In actual fact you would rarely do a straight bulk collect in this manner. Instead you would limit the rows returned using the LIMIT clause and move through the data processing smaller chunks. This gives you the benefits of bulk binds, without hogging all the server memory.
Example-4:
Test to limit the bulk collect into given number of rows
declare
    type bulk_test_mah_tab is table of bulk_test_mah%rowtype;
    v_bulk_test_mah_tab bulk_test_mah_tab := bulk_test_mah_tab();
    v_start_time  pls_integer;

    cursor c1 is select * from bulk_test_mah;
begin
    v_start_time :=dbms_utility.get_time;

    open c1;
    loop
    fetch c1 bulk collect into v_bulk_test_mah_tab limit 10000;
    --exit when c1%notfound;
    exit when v_bulk_test_mah_tab.count = 0;
   
    dbms_output.put_line ('No of rows in table type ->' || v_bulk_test_mah_tab.count );
    end loop;
    close c1;

exception
    when others then
        dbms_output.put_line ('error -' || sqlerrm );
end;

Output-4:
No of rows in table type ->10000
No of rows in table type ->10000
No of rows in table type ->10000
No of rows in table type ->10000
No of rows in table type ->10000
No of rows in table type ->7958
-----------------------------------------------------------------------------------------------
Example-5:
Test to track the errors using SAVE EXCEPTIONS while  performing DML using FORALL
declare
    type ord_mah_tab is table of ord_mah.order_no%type;
    v_ord_mah_tab ord_mah_tab;
begin
    select order_no bulk collect into v_ord_mah_tab from ord_mah;

    forall i in v_ord_mah_tab.first..v_ord_mah_tab.count save exceptions
        update ord_mah set customer_po_no = customer_po_no ||'X' where order_no = v_ord_mah_tab(i);

exception
    when others then
        if sqlcode= -24381 then
            for indx in 1..SQL%BULK_EXCEPTIONS.COUNT loop
               dbms_output.put_line ( SQL%BULK_EXCEPTIONS(indx).ERROR_INDEX || '- ORA-' || SQL%BULK_EXCEPTIONS(indx).ERROR_CODE );
            end loop;       
        else
            dbms_output.put_line ( dbms_utility.format_error_stack || '- ORA-' || dbms_utility.format_error_backtrace );
        end if;
end;
Output-5: The below are the failed rows while performing update
1- ORA-12899
2- ORA-12899
3- ORA-12899
9199- ORA-12899
9313- ORA-12899
14968- ORA-12899

-----------------------------------------------------------------------------------------------
Example-5:
Test FORALL with sparse collections by using VALUES OF and/or INDICES OF clauses
Use INDICES OF for Nested table type collection and VALUES OF for associative array type collection
In a FORALL statement, the VALUES OF clause allows the values of one collection to be used as a index pointers to another collection.
DECLARE
  TYPE t_tab1 IS TABLE OF tab1%ROWTYPE;
  TYPE t_tab2 IS TABLE OF BINARY_INTEGER;

  l_tab1  t_tab1 := t_tab1();
  l_tab2  t_tab2 := t_tab2();
BEGIN
  FOR i IN 1 .. 1000 LOOP
    l_tab1.extend;
    l_tab1(l_tab1.last).id := i;
    IF MOD(i, 100) = 0 THEN
      l_tab2.extend;
      l_tab2(l_tab2.last) := i;
    END IF;
  END LOOP;

  l_tab1.delete(301);
  l_tab1.delete(601);
  l_tab1.delete(901);

  -- This would fail due to sparse collection.   --FORALL i IN l_tab.first .. l_tab.last
  --  INSERT INTO tab1 VALUES l_tab(i);    -- This works fine with sparse collections.
  FORALL i IN INDICES OF l_tab1
    INSERT INTO tab1 VALUES l_tab1(i);

  -- This works fine for collections of indexes   -- pointing to elements of another collection.
  FORALL i IN VALUES OF l_tab2
    INSERT INTO tab1 VALUES l_tab1(i);
END;


PLS-00436 Restriction in FORALL Statements Removed in 11G
The PLS-00436 restriction has been removed, which means you can now reference the individual elements of a collection within the SET and WHERE clauses of a DML statement in a FORALL construct.
If we execute the following test in ORACLE 10G, we will get the following error
PLS-00436: implementation restriction: cannot reference fields of BULK In-BIND table of records

Example-6:
CREATE TABLE forall_test (
  id          NUMBER,
  description VARCHAR2(50)
);

INSERT INTO forall_test VALUES (1, 'ONE');
INSERT INTO forall_test VALUES (2, 'TWO');
INSERT INTO forall_test VALUES (3, 'THREE');
INSERT INTO forall_test VALUES (4, 'FOUR');
INSERT INTO forall_test VALUES (5, 'FIVE');
COMMIT;
DECLARE
  TYPE t_forall_test_tab IS TABLE OF forall_test%ROWTYPE;
  l_tab t_forall_test_tab;
BEGIN
  -- Retrieve the existing data into a collection.
  SELECT *
  BULK COLLECT INTO l_tab
  FROM   forall_test;

  -- Alter the data in the collection.
  FOR i IN l_tab.first .. l_tab.last LOOP
    l_tab(i).description := 'Description for ' || i;
  END LOOP;

  -- Update the table using the collection.
  FORALL i IN l_tab.first .. l_tab.last
    UPDATE forall_test
    SET    description = l_tab(i).description
    WHERE  id          = l_tab(i).id;

  COMMIT;
END;

Output:


-----------------------------------------------------------------------------------------------
Example-7: Testing BULK COLLECT with RETURNING clause
declare
    type ord_mah_tab is table of ord_mah.order_no%type;
    v_ord_mah_tab ord_mah_tab;
begin
    delete from ord_mah  returning order_no bulk collect into v_ord_mah_tab;
    rollback;
    dbms_output.put_line ('No of Rows going to be deleted -> '  || v_ord_mah_tab.count );
end;

Output-7:
No of Rows going to be deleted -> 17960
Example-8: Testing BULK COLLECT with FORALL, RETURNING
declare
    type ord_mah_ord is table of ord_mah.order_no%type;
    v_ord_mah_ord ord_mah_ord;
    v_ord_mah_tab ord_mah_ord;
begin
    select order_no bulk collect into v_ord_mah_ord from ord_mah where rownum < 101;

    forall i in 1..v_ord_mah_ord.count
    delete from ord_mah  where order_no = v_ord_mah_ord(i) returning order_no bulk collect into v_ord_mah_tab;
    rollback;
    dbms_output.put_line ('No of Rows in v_ord_mah_ord -> '  || v_ord_mah_ord.count );
    dbms_output.put_line ('No of Rows in v_ord_mah_tab -> '  || v_ord_mah_tab.count );
end;

output-8:
No of Rows in v_ord_mah_ord -> 100
No of Rows in v_ord_mah_tab -> 100

Example-9: Testing SQL%BULK_ROWCOUNT
declare
    type ord_mah_tab is table of ord_mah.customer_po_no%type;
    v_ord_mah_tab ord_mah_tab ;
begin
    select distinct customer_po_no bulk collect into v_ord_mah_tab from  ord_mah
        where customer_po_no in  ('621000441XX','621000442XX','253P14XX','M7312210BXX','FG04/TOOLCRIBXX','PLANNEDXX','AC449AXX','5500452851-0001188335XX','5500452851-0001180273XX');
        dbms_output.put_line ('no of rows ->' || v_ord_mah_tab.count );

    forall idx in v_ord_mah_tab.first..v_ord_mah_tab.last
        delete from ord_mah where customer_po_no = v_ord_mah_tab(idx);

    for idx in  1..v_ord_mah_tab.count loop
        dbms_output.put_line ('No of orders assosciated with customer po no ->' || v_ord_mah_tab(idx) ||' --- '|| SQL%BULK_ROWCOUNT(idx) );
    end loop;
    rollback;
exception
    when others then
        dbms_output.put_line ('Error-' || sqlerrm );
end;
Output-9:
no of rows ->9
No of orders associated with customer po no ->621000441XX --- 13
No of orders associated with customer po no ->621000442XX --- 11
No of orders associated with customer po no ->253P14XX --- 2
No of orders associated with customer po no ->M7312210BXX --- 2
No of orders associated with customer po no ->FG04/TOOLCRIBXX --- 2
No of orders associated with customer po no ->PLANNEDXX --- 2
No of orders associated with customer po no ->AC449AXX --- 13
No of orders associated with customer po no ->5500452851-0001188335XX --- 3
No of orders associated with customer po no ->5500452851-0001180273XX --- 5











Note:
·         In Oracle 10g and above, the optimizing PL/SQL compiler rewrites conventional cursor for loops to use a BULK COLLECT with a LIMIT 100, so code that previously didn't take advantage of bulk binds may now run faster.
·         Oracle 10g introduced support for handling sparse collections in FORALL statements (here).
·         The restriction on accessing individual columns of the collection with a FORALL has been removed in Oracle 11g (here).
·         For bulk updates and deletes the timing points remain unchanged. Each row in the collection triggers a before statement, before row, after row and after statement timing point. For bulk inserts, the statement level triggers only fire at the start and the end of the the whole bulk operation, rather than for each row of the collection


1 comment: