Tuesday, January 1, 2013

[SQL / PLSQL] Bulk Collection Of DML Results

 
Bulk Collection of DML Results 


The RETURNING clause is used to return specific columns from rows manipulated by DML statements.  When DML statements manipulate multiple rows, the data returned can be loaded into a collection using a bulk operation.  The returning_bulk_collect.sql script provides an example of this functionality.

returning_bulk_collect.sql

SET SERVEROUTPUT ON
DECLARE
  TYPE t_object_id_tab IS TABLE OF bulk_collect_test.object_id%TYPE;
  l_tab  t_object_id_tab;
BEGIN
  DELETE FROM bulk_collect_test
  RETURNING object_id BULK COLLECT INTO l_tab;
  DBMS_OUTPUT.put_line(‘Deleted IDs : ' || l_tab.count || ' rows');
  ROLLBACK;
END;
/

The returning_bulk_collect.sql script defines a collection, deletes the data from the test table and returns the OBJECT_IDs of the deleted rows.  To preserve the data, the script performs a rollback, allowing for multiple runs.  The output from this script shows that the collection is populated as expected.

SQL> @returning_bulk_collect.sql
Deleted IDs : 61202 rows
The FORALL command can be used in conjunction with a DML statement with a RETURNING clause.  In this situation, both the DML and the return data are bulk operations.  The forall_returning_bulk_collect.sql script provides an example of this functionality.

forall_returning_bulk_collect.sql

SET SERVEROUTPUT ON
DECLARE
  TYPE t_object_id_tab IS TABLE OF bulk_collect_test.object_id%TYPE;
  l_in_tab   t_object_id_tab;
  l_out_tab  t_object_id_tab;

BEGIN

  -- Populate collection use in forall.
  SELECT object_id
  BULK COLLECT INTO l_in_tab
  FROM   bulk_collect_test
  WHERE  rownum < 101; 
  FORALL i IN l_in_tab.first .. l_in_tab.last
    DELETE FROM bulk_collect_test
    WHERE  object_id = l_in_tab(i)
    RETURNING object_id BULK COLLECT INTO l_out_tab;
  DBMS_OUTPUT.put_line('Starting IDs : ' || l_in_tab.count || ' rows');
  DBMS_OUTPUT.put_line('Deleted IDs  : ' || l_out_tab.count || ' rows');
  ROLLBACK;
END;
/

The forall_returning_bulk_collect.sql script defines two collections, one to support the FORALL statement and one to support the RETURNING clause.  The first collection is populated to provide IDs of rows to be deleted. Next the bulk delete is performed and the data returned into the second collection.  Then the size of both collections is reported.  The output from this script is displayed below.

SQL> @forall_returning_bulk_collect.sql
Starting IDs : 100 rows
Deleted IDs  : 100 rows
PL/SQL procedure successfully completed.
As expected, the contents of the input and output collections are identical.  The next section investigates the use of bulk DML operations using the FORALL statement.

FORALL in PL/SQL

The FORALL statement allows insert, update and delete statements to be bound to collections in a single operation, resulting in less communication between the PL/SQL and SQL engines.  As with the BULK COLLECT option, this reduction in context switches between the two engines results in better performance.
The following examples show how bulk DML operations are coded and the performance gains when using them.  The examples are based around the FORALL_TEST table which can be created using the create_forall_test.sql script listed below.

create_forall_test.sql
CREATE TABLE forall_test (
  id           NUMBER(10),
  code         VARCHAR2(10),
  description  VARCHAR2(50));
ALTER TABLE forall_test ADD (
  CONSTRAINT forall_test_pk PRIMARY KEY (id));
ALTER TABLE forall_test ADD (
  CONSTRAINT forall_test_uk UNIQUE (code));
First compare the performance of conventional and bulk insert operations.

Bulk INSERT Operations

With the forall_test table created, the performance of individual inserts is compared against bulk inserts using the insert_forall.sql script listed below. 
The VALUES clause of the bulk operation does not reference the individual elements of the collection.  This syntax was introduced in Oracle9i Release 2 to allow bulk operations to use record structures.

insert_forall.sql

SET SERVEROUTPUT ON
DECLARE
  TYPE t_forall_test_tab IS TABLE OF forall_test%ROWTYPE;
  l_tab    t_forall_test_tab := t_forall_test_tab();
  l_start  NUMBER;
  l_size   NUMBER            := 10000;
BEGIN
  -- Populate collection.
  FOR i IN 1 .. l_size LOOP
    l_tab.extend;
    l_tab(l_tab.last).id          := i;
    l_tab(l_tab.last).code        := TO_CHAR(i);
    l_tab(l_tab.last).description := 'Description: ' || TO_CHAR(i);
  END LOOP;
  EXECUTE IMMEDIATE ‘TRUNCATE TABLE forall_test’;
  -- Time regular inserts.
  l_start := DBMS_UTILITY.get_time;
  FOR i IN l_tab.first .. l_tab.last LOOP
    INSERT INTO forall_test (id, code, description)
    VALUES (l_tab(i).id, l_tab(i).code, l_tab(i).description);
  END LOOP;
  DBMS_OUTPUT.put_line('Normal Inserts: ' ||
                       (DBMS_UTILITY.get_time - l_start)); 
  EXECUTE IMMEDIATE ‘TRUNCATE TABLE forall_test’;
  -- Time bulk inserts. 
  l_start := DBMS_UTILITY.get_time;
  FORALL i IN l_tab.first .. l_tab.last
    INSERT INTO forall_test VALUES l_tab(i);
  DBMS_OUTPUT.put_line('Bulk Inserts  : ' ||
                       (DBMS_UTILITY.get_time - l_start));
  COMMIT;
END;
/

The insert_forall.sql script is split into four main sections:

  • Declaration - A table type is defined with the same structure as the test table, which is then used to declare and initialize a collection.  The collection is initialized to support the manual operation, not the bulk operation as these implicitly initialize collections.  In addition variables are defined to store the start time of each operation and the size of the collection.
  • Populate Collection – Extends and populates the collection to the desired size.
  • Time Regular Inserts – Loops through the collection performing a separate insert for each row and displays the time taken to complete the loop.
  • Time Bulk Inserts – Performs a bulk operation to bind the collection into an insert statement and displays the time taken to complete the operation.
The types of results expected from the script are shown below, in this case, using a collection of 10,000 records.  The times will vary between servers and individual runs depending on the specification and usage levels of the servers.  The thing to focus on is not the absolute speed, but the comparison between the two.

SQL> @insert_forall.sql
Normal Inserts: 300
Bulk Inserts  : 19
PL/SQL procedure successfully completed.

The output shows it takes approximately 3 seconds to insert 10,000 rows via a conventional loop, while the bulk operation completes that same amount of work in approximately 0.2 seconds.  This is a dramatic illustration of the benefits of bulk operations.

For the sake of completeness, the insert_forall_8i.sql script shows how this operation would be coded prior to Oracle9i Release 2.  Notice that a separate collection is defined for each column referenced in the bind operation.

insert_forall_8i.sql

SET SERVEROUTPUT ON
DECLARE
  TYPE t_id_tab IS TABLE OF forall_test.id%TYPE;
  TYPE t_code_tab IS TABLE OF forall_test.code%TYPE;
  TYPE t_desc_tab IS TABLE OF forall_test.description%TYPE;
  l_id_tab    t_id_tab   := t_id_tab();
  l_code_tab  t_code_tab := t_code_tab();
  l_desc_tab  t_desc_tab := t_desc_tab();
  l_start     NUMBER;
  l_size      NUMBER     := 10000;

BEGIN

  -- Populate collection.

  FOR i IN 1 .. l_size LOOP
    l_id_tab.extend;
    l_code_tab.extend;
    l_desc_tab.extend;
    l_id_tab(l_id_tab.last)     := i;
    l_code_tab(l_id_tab.last)   := TO_CHAR(i);
    l_desc_tab(l_desc_tab.last) := 'Description: ' || TO_CHAR(i);
  END LOOP;
  EXECUTE IMMEDIATE ‘TRUNCATE TABLE forall_test’;
  -- Time regular inserts.
  l_start := DBMS_UTILITY.get_time;
  FOR i IN l_id_tab.first .. l_id_tab.last LOOP
    INSERT INTO forall_test (id, code, description)
    VALUES (l_id_tab(i), l_code_tab(i), l_desc_tab(i));
  END LOOP;
  DBMS_OUTPUT.put_line('Normal Inserts: ' ||
                       (DBMS_UTILITY.get_time - l_start)); 
  EXECUTE IMMEDIATE ‘TRUNCATE TABLE forall_test’;
  -- Time bulk inserts. 
  l_start := DBMS_UTILITY.get_time;
  FORALL i IN l_id_tab.first .. l_id_tab.last
    INSERT INTO forall_test (id, code, description)
    VALUES (l_id_tab(i), l_code_tab(i), l_desc_tab(i));
  DBMS_OUTPUT.put_line('Bulk Inserts  : ' ||
                       (DBMS_UTILITY.get_time - l_start));
  COMMIT;
END;
/

The same performance improvements are also evident using this method.

SQL> @insert_forall_8i.sql
Normal Inserts: 274
Bulk Inserts  : 24

PL/SQL procedure successfully completed



No comments:

Post a Comment