Bulk Collect

Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 8

Bulk Collect

Bulk Processing for Repeated SQL Statement Execution


The bulk processing features of PL/SQL are designed specifically to reduce the number of
context switches required to communicate from the PL/SQL engine to the SQL engine.

With FORALL and BULK COLLECT, however, you can fine-tune the way these two engines
communicate, effectively telling the PL/SQL engine to compress multiple context switches into a
single switch, thereby improving the performance of your applications.

At runtime, the PL/SQL engine takes the “template” UPDATE statement and collection of bind
variable values and generates them into a set of statements, which are then passed to the SQL
engine with a single context switch. In other words, the same SQL statements are executed, but
they are all run in the same roundtrip to the SQL layer, minimizing the context switches

Bulk Collect Syntax.

BULK COLLECT INTO collection_name[, collection_name] ...


 You can use BULK COLLECT keywords in any of the following clauses: SELECT INTO,
FETCH INTO, and RETURNING INTO.

 The SQL engine automatically initializes and extends the collections you reference in the
BULK COLLECT clause. It starts filling the collections at index 1, and inserts elements
consecutively (densely).

 SELECT...BULK COLLECT will not raise NO_DATA_FOUND if no rows are found. Instead,


you must check the contents of the collection to see if there is any data inside it.

Limiting rows retrieved with BULK COLLECT


Oracle provides a LIMIT clause for BULK COLLECT that allows you to limit the number of rows
fetched from the database. The syntax is:

FETCH cursor BULK COLLECT INTO ... [LIMIT rows];


LIMIT is very useful with BULK COLLECT, because it helps you manage how much memory your
program will use to process data. Suppose, for example, that you need to query and process
10,000 rows of data. You could use BULK COLLECT to retrieve all those rows and populate a
rather large collection. However, this approach will consume lots of memory in the PGA for that
session. If this code is run by many separate Oracle schemas, your application’s performance may
degrade because of PGA swapping.

DECLARE
CURSOR allrows_cur IS SELECT * FROM employees;
TYPE employee_aat IS TABLE OF allrows_cur%ROWTYPE INDEX BY
BINARY_INTEGER;
l_employees employee_aat;
BEGIN
OPEN allrows_cur;
LOOP
FETCH allrows_cur BULK COLLECT INTO l_employees LIMIT 100;
/* Process the data by scanning through the collection. */
FOR l_row IN 1 .. l_employees.COUNT
LOOP
upgrade_employee_status (l_employees(l_row).employee_id);
END LOOP;

EXIT WHEN allrows_cur%NOTFOUND;


END LOOP;

CLOSE allrows_cur;
END;
Starting with Oracle Database 12c, you can also use the FIRST ROWS clause to limit the number
of rows fetched with BULK COLLECT. The following block of code uses the LIMIT clause in a
FETCH that is inside a simple loop. This code will retrieve just the first 50 rows identified by the
SELECT statement:

DECLARE
TYPE salaries_t IS TABLE OF employees.salary%TYPE;
l_salaries salaries_t;
BEGIN
SELECT salary BULK COLLECT INTO sals FROM employees
FETCH FIRST 50 ROWS ONLY;
END;
/
Using the RETURNING clause with bulk operations
The RETURNING clause allows you to obtain information (such as a newly updated value for a
salary) from a DML statement. RETURNING can help you avoid additional queries to the database
to determine the results of DML operations that have just completed.

set SERVEROUTPUT on;


declare
type l_type_t is table of number index by pls_integer;
l_type l_type_t;

type l_deleted_emp_id_t is table of number index by pls_integer;


l_deleted_emp_id l_deleted_emp_id_t;
begin

update employee
set salary = salary * 1.1
returning salary bulk collect into l_type;

-- Print New salary


for i in 1..l_type.count
loop

dbms_output.put_line(l_type(i));

end loop;

delete employee
where dept_id = 20
returning employee_id bulk collect into l_deleted_emp_id;

-- Print Deleted employee id.


for i in 1..l_deleted_emp_id.COUNT
loop
      dbms_output.put_line(l_deleted_emp_id(i));
end loop;

end;
HIGH-SPEED DML WITH FORALL
FORALL index IN
[ lower_bound ... upper_bound |
INDICES OF indexing_collection |
VALUES OF indexing_collection
]
[ SAVE EXCEPTIONS ]
sql_statement;
Example

DECLARE
TYPE employee_aat IS TABLE OF employees%ROWTYPE
INDEX BY PLS_INTEGER;
l_employees employee_aat;
BEGIN
FORALL l_index IN l_employees.FIRST .. l_employees.LAST
INSERT INTO employee (employee_id, last_name)
VALUES (l_employees (l_index).employee_id
, l_employees (l_index).last_name
);
END;
 Use the RETURNING clause in a FORALL statement to retrieve information about each
separate DELETE statement. Notice that the RETURNING clause in FORALL must use BULK
COLLECT INTO (the corresponding “bulk” operation for queries):

 FUNCTION remove_emps_by_dept (deptlist IN dlist_t)


 RETURN enolist_t
 IS
 enolist enolist_t;
 BEGIN
 FORALL aDept IN deptlist.FIRST..deptlist.LAST
 DELETE FROM employees WHERE department_id IN deptlist(aDept)
 RETURNING employee_id BULK COLLECT INTO enolist;
 RETURN enolist;
 END;
 Dynamic SQL using Bulk Collect

 FORALL indx IN INDICES OF l_top_employees


 EXECUTE IMMEDIATE
 'INSERT INTO ' || l_table || ' VALUES (:emp_pky, :new_salary)'
 USING l_new_salaries(indx).employee_id,
 l_new_salaries(indx).salary;

Cursor attributes for FORALL

 SQL%BULK_ROWCOUNT - Returns a pseudo collection that tells you the number of


rows processed by each corresponding SQL statement executed via FORALL. Note that
when %BULK_ROWCOUNT(i) is zero, %FOUND and %NOTFOUND are FALSE and TRUE,
respectively.
 SQL%BULK_EXCEPTIONS - Returns a pseudo collection that provides information about
each exception raised in a FORALL statement that includes the SAVE EXCEPTIONS clause.

DECLARE
TYPE isbn_list IS TABLE OF VARCHAR2(13);

my_books isbn_list
:= isbn_list (
'1-56592-375-8', '0-596-00121-5', '1-56592-849-0',
'1-56592-335-9', '1-56592-674-9', '1-56592-675-7',
'0-596-00180-0', '1-56592-457-6'
);
BEGIN
FORALL book_index IN
my_books.FIRST..my_books.LAST
UPDATE books
SET page_count = page_count / 2
WHERE isbn = my_books (book_index);

-- Did I update the total number of books I expected?


IF SQL%ROWCOUNT != 8
THEN
DBMS_OUTPUT.PUT_LINE (
'We are missing a book!');
END IF;

-- Did the 4th UPDATE statement affect any rows?


IF SQL%BULK_ROWCOUNT(4) = 0
THEN
DBMS_OUTPUT.PUT_LINE (
'What happened to Oracle PL/SQL Programming?');
END IF;
END;

 The FORALL statement and %BULK_ROWCOUNT use the same subscripts or row numbers
in the collections. For example, if the collection passed to FORALL has data in rows 10
through 200, then the %BULK_ROWCOUNT pseudocollection will also have rows 10
through 200 defined and populated. Any other rows will be undefined.
 When the INSERT affects only a single row (when you specify a VALUES list, for example),
a row’s value in %BULK_ROWCOUNT will be equal to 1. For INSERT...SELECT statements,
however, %BULK_ROWCOUNT can be greater than 1

ROLLBACK behavior with FORALL


What happens when one of those DML statements fails?

1. The DML statement that raised the exception is rolled back to an implicit savepoint
marked by the PL/SQL engine before execution of the statement. Changes to all rows
already modified by that statement are rolled back.
2. Any previous DML operations in that FORALL statement that have already completed
without error are not rolled back.
3. If you do not take special action (by adding the SAVE EXCEPTIONS clause to FORALL,
discussed next), the entire FORALL statement stops and the remaining statements are not
executed at all.

Continuing past exceptions with SAVE EXCEPTIONS


By adding the SAVE EXCEPTIONS clause to your FORALL header, you instruct the Oracle database
to continue processing even when an error has occurred. The database will then “save the
exception” (or multiple exceptions, if more than one error occurs). When the DML statement
completes, it will then raise the ORA-24381 exception. In the exception section, you can then
access a pseudocollection called SQL%BULK_EXCEPTIONS to obtain error information.

/* File on web: bulkexc.sql */


DECLARE
bulk_errors EXCEPTION;
PRAGMA EXCEPTION_INIT (bulk_errors, −24381);
TYPE namelist_t IS TABLE OF VARCHAR2(32767);
enames_with_errors namelist_t
:= namelist_t ('ABC',
'DEF',
NULL, /* Last name cannot be NULL */
'LITTLE',
RPAD ('BIGBIGGERBIGGEST', 250, 'ABC'), /* Value too long */
'SMITHIE'
);
BEGIN
FORALL indx IN enames_with_errors.FIRST .. enames_with_errors.LAST
SAVE EXCEPTIONS
UPDATE EMPLOYEES
SET last_name = enames_with_errors (indx);

EXCEPTION
WHEN bulk_errors
THEN
DBMS_OUTPUT.put_line ('Updated ' || SQL%ROWCOUNT || ' rows.');

FOR indx IN 1 .. SQL%BULK_EXCEPTIONS.COUNT --you cannot call


FIRST and LAST here
LOOP
DBMS_OUTPUT.PUT_LINE ('Error '
|| indx
|| ' occurred during '
|| 'iteration '
|| SQL%BULK_EXCEPTIONS (indx).ERROR_INDEX
|| ' updating name to '
|| enames_with_errors (SQL%BULK_EXCEPTIONS
(indx).ERROR_INDEX);
DBMS_OUTPUT.PUT_LINE ('Oracle error is '
|| SQLERRM ( −1 * SQL%BULK_EXCEPTIONS
(indx).ERROR_CODE)
);
END LOOP;
END;

 The ERROR_INDEX field of each pseudocollection’s row returns the row number in the
driving collection of the FORALL statement for which an exception was raised
 The ERROR_CODE field of each pseudocollection’s row returns the error number of the
exception that was raised. Note that this value is stored as a positive integer; you will
need to multiply it by −1 before passing it to SQLERRM or displaying the information.
 Note that I can call the COUNT method to determine the number of defined rows (errors
raised), but I cannot call other methods, such as FIRST and LAST

Driving FORALL with nonsequential arrays


INDICES OF

set SERVEROUTPUT on;


declare
type l_type_t is table of number index by pls_integer;
l_type l_type_t;
begin

l_type(10) := 78;
l_type(15) := 115;
l_type(200) := 122;

forall i in INDICES of l_type between 1 and 120


update employee set salary = 500
where emp_id = l_type(i);

end;
I also include an optional BETWEEN clause to restrict which of those index values will be used.

With INDICES OF , the contents of the indexing array are ignored. All that matters are the
positions or row numbers that are defined in the collection.

VALUES OF example
/* File on web: 10g_values_of.sql */
1 DECLARE
2 TYPE employee_aat IS TABLE OF employees.employee_id%TYPE
3 INDEX BY PLS_INTEGER;
4
5 l_employees employee_aat;
6
7 TYPE indices_aat IS TABLE OF PLS_INTEGER
8 INDEX BY PLS_INTEGER;
9
10 l_employee_indices indices_aat;
11 BEGIN
12 l_employees (-77) := 7820;
13 l_employees (13067) := 7799;
14 l_employees (99999999) := 7369;
15 --
16 l_employee_indices (100) := −77;
17 l_employee_indices (200) := 99999999;
18 --
19 FORALL l_index IN VALUES OF l_employee_indices
20 UPDATE employees
21 SET salary = 10000
22 WHERE employee_id = l_employees (l_index);
23 END;

You might also like