Skip to main content

Using sparse collections with FORALL

FORALL is a key performance feature of PL/SQL. It helps you avoid row-by-row processing of non-query DML (insert, update, delete, merge) from within a PL/QL block. Best of all, almost always, is to do all your processing entirely within a single SQL statement. Sometimes, however, that isn't possible (for example, you need to sidestep SQL's "all or nothing" approach) or simply too difficult (not all of us have the insane SQL writing skills of a Tom Kyte or a Chris Saxon or a Connor McDonald).

To dive in deep on FORALL, check out any of the following resources:
In this post, I am going to focus on special features of FORALL that make it easy to work with space collections: the INDICES OF and VALUES OF clauses.

Typical FORALL Usage with Dense Bind Array

Here's the format you will most commonly see with FORALL: the header looks just like a numeric FOR loop, but notice: no loop keywords. Two rows will be updated, because the collection is filled sequentially or densely: every index value between the lowest and the highest are defined.

DECLARE
   TYPE employee_aat IS TABLE OF employees.employee_id%TYPE
      INDEX BY PLS_INTEGER;
   l_employees   employee_aat;
BEGIN
   l_employees (1) := 7839;
   l_employees (2) := 7654;

   FORALL l_index IN 1 .. l_employees.COUNT
      UPDATE employees SET salary = 10000
       WHERE employee_id = l_employees (l_index);
END;
/

When We Go Sparse...

But take a close look at the way I assign values in the next block. Now my lowest index value is 1 and my highest is 100, with nothing in between. This is known as a sparse collection.

Now when I run the same code, I get an error: ORA-22160: element at index [2] does not exist.

DECLARE
   TYPE employee_aat IS TABLE OF employees.employee_id%TYPE
      INDEX BY PLS_INTEGER;
   l_employees   employee_aat;
BEGIN
   l_employees (1) := 7839;
   l_employees (100) := 7654;

   FORALL l_index IN 1 .. l_employees.COUNT
      UPDATE employees SET salary = 10000
       WHERE employee_id = l_employees (l_index);
END;
/

ORA-22160: element at index [2] does not exist

Notice that this is a SQL error, not a PL/SQL exception (if the latter, we might have predicted that the ORA-01403 No data found might have been raised): the collection was passed to the SQL engine, the SQL engine tried to go from first to last, incrementing the counter each time - and then it blew up.

When you are trying to use FORALL with a sparse collection, you must do one of the following:
  1. "Densify" the collection - get rid of the gaps. This was necessary prior to Oracle Database 10g. Hopefully that means you can ignore this option.
  2. Use INDICES OF 
  3. Use VALUES OF
Simplest INDICES OF Use Case

INDICES OF is the solution you will most likely use. Use this approach when you have a collection (the indexing array) whose defined index values can be used to specify the index values in the bind array (referenced within the FORALL's DML statement) that are to be used by FORALL.

In other words, if the element at index value N is not defined in the indexing array, you want the FORALL statement to ignore the element at position N in the bind array.

And in the simplest use case of INDICES OF, the indexing and bind arrays are the same, as you see in the example below.

DECLARE
   TYPE employee_aat IS TABLE OF employees.employee_id%TYPE
      INDEX BY PLS_INTEGER;
   l_employees   employee_aat;
BEGIN
   l_employees (1) := 7839;
   l_employees (2) := 7654;

   FORALL l_index IN INDICES OF l_employees
      UPDATE employees SET salary = 10000
       WHERE employee_id = l_employees (l_index);
END;
/

This simply says: only use the defined index values of l_employees, and skip over any gaps. Nice!

More Interesting INDICES OF Usage

But you can do more with INDICES OF than that.

Suppose your bind array has 10,000 elements defined in it. You need to perform three different FORALL operations against different subsets of those elements.

You could copy the selected contents required for each FORALL "run" into its own collection. But that could use more PGA memory then necessary. You could instead construct three different indexing arrays, each of which simply point back to elements in the bind array that are relevant for that run.

In the example below, the l_employee_indices is my indexing array. Notice that the actual contents of each element in this array is of no importance. The PL/SQL engine will look only at the index values.

Notice that I can also use a BETWEEN clause to restrict which index values I want to use. So in this block, I update the rows for employee IDs 7839 and 7950 only.

DECLARE
   TYPE employee_aat IS TABLE OF employees.employee_id%TYPE
      INDEX BY PLS_INTEGER;
   l_employees           employee_aat;

   TYPE boolean_aat IS TABLE OF BOOLEAN
      INDEX BY PLS_INTEGER;

   l_employee_indices   boolean_aat;
BEGIN
   l_employees (1) := 7839;
   l_employees (100) := 7654;
   l_employees (500) := 7950;
   
   l_employee_indices (1) := TRUE;
   l_employee_indices (500) := TRUE;
   l_employee_indices (799) := TRUE;

   FORALL l_index IN INDICES OF l_employee_indices
        BETWEEN 1 AND 500
     UPDATE employees             
        SET salary = 10000
      WHERE employee_id = l_employees (l_index);
END;
/

And Then There is VALUES OF

I've met lots of developers over the years who have used INDICES OF. I've not yet encountered anyone who took advantage of VALUES OF. So if you ever do find a use for it in your code, please let me know! :-)

Use this clause when you have a collection of integers (again, the indexing array) whose content (the value of the element at a specified position) identifies the position in the binding array that you want to be processed by the FORALL statement.

So while with INDICES OF, the PL/SQL engine uses the index values of the indexing array, with VALUES OF, it uses the values of the elements in the collection.

Here's an example:

DECLARE
   TYPE employee_aat IS TABLE OF employees.employee_id%TYPE
      INDEX BY PLS_INTEGER;

   l_employees           employee_aat;

   TYPE indices_aat IS TABLE OF PLS_INTEGER
      INDEX BY PLS_INTEGER;

   l_employee_indices   indices_aat;
BEGIN
   l_employees (-77) := 7820;
   l_employees (13067) := 7799;
   l_employees (99999999) := 7369;
   
   l_employee_indices (100) := −77;
   l_employee_indices (200) := 99999999;
   
   FORALL l_index IN VALUES OF l_employee_indices
      UPDATE employees
         SET salary = 10000
       WHERE employee_id = l_employees (l_index);
END;
/

I populate (sparsely) three rows (–77, 13067, and 99999999) in the collection of employee IDs.

I want to set up the indexing array to identify which of those rows to use in my update. Because I am using VALUES OF, the row numbers that I use are unimportant. Instead, what matters is the value found in each of the rows in the indexing array. Again, I want to skip over that “middle” row of 13067, so here I define just two rows in the l_employee_indices array and assign them values –77 and 9999999, respectively.

Rather than specify a range of values from FIRST to LAST, I simply specify VALUES OF l_employee_indices. Notice that I populate rows 100 and 200 in the indices collection. VALUES OF does not require a densely filled indexing collection.

VALUES OF also does not support a BETWEEN clause like INDICES OF.

So VALUES OF gives you lots of flexibility - perhaps more than you will ever need!

Sparse is Fine with FORALL

So remember: it's no problem using the powerful FORALL feature with sparse collections. All you have to do is pick between INDICES OF or VALUES OF, and let the PL/SQL do all (or more) of the work for you.

Here are LiveSQL scripts covering much the same material as shown above:

INDICES OF
VALUES OF

Comments

Popular posts from this blog

Running out of PGA memory with MULTISET ops? Watch out for DISTINCT!

A PL/SQL team inside Oracle made excellent use of nested tables and MULTISET operators in SQL, blending data in tables with procedurally-generated datasets (nested tables).  All was going well when they hit the dreaded: ORA-04030: out of process memory when trying to allocate 2032 bytes  They asked for my help.  The error occurred on this SELECT: SELECT  *    FROM header_tab trx    WHERE (generated_ntab1 SUBMULTISET OF trx.column_ntab)       AND ((trx.column_ntab MULTISET             EXCEPT DISTINCT generated_ntab2) IS EMPTY) The problem is clearly related to the use of those nested tables. Now, there was clearly sufficient PGA for the nested tables themselves. So the problem was in executing the MULTISET-related functionality. We talked for a bit about dropping the use of nested tables and instead doing everything in SQL, to avoid the PGA error. That would, however require lots of wo...

How to Pick the Limit for BULK COLLECT

This question rolled into my In Box today: In the case of using the LIMIT clause of BULK COLLECT, how do we decide what value to use for the limit? First I give the quick answer, then I provide support for that answer Quick Answer Start with 100. That's the default (and only) setting for cursor FOR loop optimizations. It offers a sweet spot of improved performance over row-by-row and not-too-much PGA memory consumption. Test to see if that's fast enough (likely will be for many cases). If not, try higher values until you reach the performance level you need - and you are not consuming too much PGA memory.  Don't hard-code the limit value: make it a parameter to your subprogram or a constant in a package specification. Don't put anything in the collection you don't need. [from Giulio Dottorini] Remember: each session that runs this code will use that amount of memory. Background When you use BULK COLLECT, you retrieve more than row with each fetch, ...

PL/SQL 101: Three ways to get error message/stack in PL/SQL

The PL/SQL Challenge quiz for 10 September - 16 September 2016 explored the different ways you can obtain the error message / stack in PL/SQL. Note: an error stack is a sequence of multiple error messages that can occur when an exception is propagated and re-raised through several layers of nested blocks. The three ways are: SQLERRM - The original, traditional and (oddly enough) not currently recommended function to get the current error message. Not recommended because the next two options avoid a problem which you are unlikely  to run into: the error stack will be truncated at 512 bytes, and you might lose some error information. DBMS_UTILITY.FORMAT_ERROR_STACK - Returns the error message / stack, and will not truncate your string like SQLERRM will. UTL_CALL_STACK API - Added in Oracle Database 12c, the UTL_CALL_STACK package offers a comprehensive API into the execution call stack, the error stack and the error backtrace.  Note: check out this LiveSQL script if...