O'Reilly Network    
 Published on O'Reilly Network (http://www.oreillynet.com/)
 http://www.oreillynet.com/pub/a/network/2003/01/07/feuerstein.html
 See this if you're having trouble printing code examples


Oracle PL/SQL Programming, 3rd Edition

Native Compilation, CASE, and Dynamic Bulk Binding

by Steven Feuerstein, coauthor of Oracle PL/SQL Programming, 3rd Edition and Bryn Llewellyn
01/07/2003

Editor's note: In previous articles (see sidebar below for links) Steven Feuerstein examined the Oracle 9i inheritance capabilities of Oracle object types. This article continues his exploration of new PL/SQL features in Oracle 9i, an exploration that will consist of a series of articles written with Bryn Llewellyn, PL/SQL Product Manager at Oracle Corporation. The material in this article is based on an Oracle Corporation white paper originally presented by Bryn Llewellyn at Oracle OpenWorld 2001 in San Francisco.

Introduction

We'll kick this off this second part of our series on new PL/SQL features with an exploration of Oracle 9i PL/SQL enhancements. The most important enhancements to PL/SQL in Oracle 9i can be categorized as falling into one of these areas:

Also In This Series

Inherit the Database: Oracle9i's Support for Object Type Inheritance

Substituting and Converting Object Types in a Hierarchy

New Datatypes, New Possibilities

Table Functions and Cursor Expressions

Multi-Level Collections in Oracle 9i

HTTP Communication from Within the Oracle Database

Oracle 9i Release 2 Developments for PL/SQL Collections

Using PL/SQL Records in SQL Statements

Some of the enhancements are transparent; for example, the change to using the same parser for compile-time checked embedded SQL as is used for compiling SQL issued from other programming environments; or the re-implementation of the UTL_TCP package (moving from Java to native C). You don't have to do anything besides upgrade to Oracle 9i to take advantage of these improvements.

Some are semi-transparent, which means that you'll need to take small, declarative steps (that is, you don't have to change any of your code). The best example of such a semi-transparent enhancement is the new option to compile PL/SQL source to native C--a topic to be explored in a future article.

Finally, some new features introduce new semantics, either in the language itself or by virtue of new APIs in the supplied PL/SQL library units. To take advantage of these enhancements, you'll need to first learn what's possible and then carefully decide how to upgrade existing code or integrate these features in new applications.

In future articles, we'll explore in much more detail the following Oracle 9i PL/SQL enhancements:

Related Reading

Oracle PL/SQL Programming

Oracle PL/SQL Programming
By Steven Feuerstein

Table of Contents
Index
Sample Chapter

Read Online--Safari Search this book on Safari:
 

Code Fragments only

In each case, we'll introduce you to the technology and then provide extensive, complete code samples that provide working demonstrations for all these features. In this article, we'll cover native compilation of PL/SQL, CASE statements and CASE expressions, and bulk binding in native dynamic SQL.

Native Compilation of PL/SQL

In pre-Oracle 9i versions, compilation of PL/SQL source code always results in a representation (usually referred to as bytecode) that's stored in the database and interpreted at runtime by a virtual machine implemented within Oracle that, in turn, runs natively on the given platform. Oracle 9i introduces a new approach. PL/SQL source code may optionally be compiled into native object code that's linked into Oracle. (Note, however, that an anonymous PL/SQL block is never compiled natively.) When would this feature come in handy? How do you turn on native compilation? So many questions... and here come the answers.

PL/SQL is often used as a thin wrapper for executing SQL statements, setting bind variables, and handling result sets; one example of such a wrapper can be seen in Example 1. For these kinds of programs, the execution speed of the PL/SQL code is rarely an issue. It is, rather, the execution speed of the SQL that determines the performance. (The efficiency of the context switch between the PL/SQL and the SQL operating environments might be an issue, but that's a different discussion. See the section in this article on bulk binding as one way of addressing this context switch issue.)

Example 1. Thin wrapper for executing SQL statements.

BEGIN
   FOR department IN  (SELECT department_id d, department_name
                         FROM departments
                     ORDER BY department_name)
   LOOP
      DBMS_OUTPUT.PUT_LINE (   CHR (10)
                            || department.department_name);

      FOR employee IN  (SELECT last_name
                          FROM employees
                         WHERE department_id = department.d
                      ORDER BY last_name)
      LOOP
         DBMS_OUTPUT.PUT_LINE (   '- '
                               || employee.last_name);
      END LOOP;
   END LOOP;
END;

There are many other applications and programs, however, that rely on PL/SQL to perform computationally-intensive tasks that are all but completely independent of the database. It is, after all, a fully functional procedural language. Consider, for example, the code shown in Example 2. This program takes on the task of finding all right-angled triangles with all side lengths integer (a.k.a. perfect triangles). We must count only unique triangles-that is, those whose sides are not each the same integral multiple of the sides of a perfect triangle already found.

Example 2. Computing perfect triangles.

CREATE OR REPLACE PROCEDURE perfect_triangles (p_max IN INTEGER)
IS
   t1             INTEGER;
   t2             INTEGER;
   long           INTEGER;
   short          INTEGER;
   hyp            NUMBER;
   ihyp           INTEGER;

   TYPE side_r IS RECORD (
      short                         INTEGER,
      long                          INTEGER);

   TYPE sides_t IS TABLE OF side_r
      INDEX BY BINARY_INTEGER;

   unique_sides   sides_t;
   n      INTEGER := 0 /* curr max elements in unique_sides */;
   dup_sides      sides_t;
   m      INTEGER := 0 /* curr max elements in dup_sides */;

   PROCEDURE store_dup_sides (p_long IN INTEGER, p_short IN INTEGER)
   IS
      mult         INTEGER := 2;
      long_mult    INTEGER :=  p_long * 2;
      short_mult   INTEGER :=  p_short * 2;
   BEGIN
      WHILE (long_mult < p_max)
         OR (short_mult < p_max)
      LOOP
         n := n + 1;
         dup_sides (n).long := long_mult;
         dup_sides (n).short := short_mult;
         mult := mult + 1;
         long_mult := p_long * mult;
         short_mult := p_short * mult;
      END LOOP;
   END store_dup_sides;

   FUNCTION sides_are_unique (p_long IN INTEGER, p_short IN INTEGER)
      RETURN BOOLEAN
   IS
   BEGIN
      FOR j IN 1 .. n
      LOOP
         IF      (p_long = dup_sides (j).long)
             AND (p_short = dup_sides (j).short)
         THEN
            RETURN FALSE;
         END IF;
      END LOOP;

      RETURN TRUE;
   END sides_are_unique;
BEGIN                            /* Perfect_Triangles */
   t1 := DBMS_UTILITY.get_time;

   FOR long IN 1 .. p_max
   LOOP
      FOR short IN 1 .. long
      LOOP
         hyp := SQRT (  long * long + short * short);
         ihyp := FLOOR (hyp);

         IF   hyp
            - ihyp < 0.01
         THEN
            IF (ihyp * ihyp =
                        long * long
                      + short * short
               )
            THEN
               IF sides_are_unique (long, short)
               THEN
                  m := m + 1;
                  unique_sides (m).long := long;
                  unique_sides (m).short := short;
                  store_dup_sides (long, short);
               END IF;
            END IF;
         END IF;
      END LOOP;
   END LOOP;

   t2 := DBMS_UTILITY.get_time;
   DBMS_OUTPUT.put_line (
         CHR (10)
      || TO_CHAR (((  t2
                    - t1
                   ) / 100
                  ), '9999.9')
      || ' sec'
   );
END perfect_triangles;

This program implements an exhaustive search among candidate triangles with all possible combinations of lengths of the two shorter sides, each in the range of one to a specified maximum. Testing whether the square root of the sum of the squares of the two short sides is within 0.01 of an integer coarsely filters each candidate. Exactly applying Pythagoras's theorem using integer arithmetic tests triangles that pass this test. Candidate- perfect triangles are tested against the list of multiples of perfect triangles found so far. Each new unique perfect triangle is stored in a PL/SQL table, and its multiples (up to the maximum length) are stored in a separate PL/SQL table to facilitate uniqueness testing.

The implementation thus involves a doubly nested loop with these steps at its heart: several arithmetic operations, casts and comparisons; calls to procedures implementing comparisons driven by iteration through a PL/SQL table (with yet more arithmetic operations); and extension of PL/SQL tables where appropriate.

So what impact does native compilation have on such code? The elapsed time was measured for p_max =5000 (that is, 12.5 million repetitions of the heart of the loop) using interpreted and natively compiled versions of the procedure. The times were 548 seconds and 366 seconds, respectively (on a Sun Ultra60 with no load apart from the test). Thus, the natively compiled version was about 33 percent faster.

That's not bad for a semi-transparent enhancement. In other words, no code changes were required in our application. And, while for data-intensive programs, native compilation may give only a marginal performance improvement, we've never seen it give performance degradation. So how do you turn on native compilation? Read on...

One-Time DBA Setup

Native PL/SQL compilation is achieved by translating the PL/SQL source code into C source code that's then compiled on the given platform. The compiling and linking of the generated C source code is done using third-party utilities whose location has been specified by the DBA, typically in the init.ora parameter file. (Check the Release Notes for your platform to see which third-party utilities are supported.)

The object code for each natively compiled PL/SQL library unit is stored on the platform's file system in directories, similarly under the DBA's control. Thus, native compilation does take longer than interpreted-mode compilation. Our tests have shown a factor of about times two. This is because it involves these extra steps: generating C code from the initial output of the PL/SQL compilation; writing this to the file system; invoking and running the C compiler; and linking the resulting object code into Oracle.

Oracle recommends that the C compiler be configured to do no optimization. Our tests have shown that optimizing the generated C produces negligible improvement in runtime performance but substantially increases the compilation time.

The DBA should appreciate that the utilities for compilation and linking must be under his or her strict control, owned by the Oracle user or root (or its equivalent on non-Unix systems) and with write-access granted only to these users. You already protect the Oracle executables in this way, for obvious reasons. You can imagine what might happen if you didn't! Well, the risk would be just the same if a malicious user could subvert the utilities for compilation and linking used by native compilation.

Choosing Between Interpreted and Native Compilation Modes

The compiler mode is determined by the session parameter plsql_compiler_flags. The user may set it as follows:

ALTER SESSION 
   SET plsql_compiler_flags = 
   'NATIVE' /* or 'INTERPRETED' */;

The compilation mode is then set for subsequently compiled PL/SQL library units (during that session). The mode is stored with the library unit's metadata, so that if the program is implicitly recompiled as a consequence of dependency checking, the original mode the user intended will be used.

You can determine the compilation mode by querying the data dictionary using the SELECT statement shown in Example 3.

Example 3. Determining the compilation mode of a program unit.

SELECT o.object_name NAME, s.param_value comp_mode

  FROM USER_STORED_SETTINGS s, 
       USER_OBJECTS o

 WHERE o.object_id = s.object_id
   AND param_name = 'plsql_compiler_flags'
   AND o.object_type IN ('PACKAGE', 'PROCEDURE', 'FUNCTION');

One thing to be aware of: If you use DBMS_UTILITY.COMPILE_SCHEMA to attempt to recompile all invalid program units in your schema, it will use the current value of plsql_compiler_flags rather than the compilation mode stored with each program unit. (In other words, it does the equivalent of alter ... compile without reuse settings for the whole schema.)

Oracle recommends that all the PL/SQL library units that are called from a given top- level unit be compiled in the same mode. This is because there's a cost for the context switch when a library unit compiled in one mode invokes one compiled in the other mode, particularly when a native unit calls an interpreted unit. Significantly, this recommendation includes the Oracle-supplied library units. These are always shipped (in the seed databases) compiled in interpreted mode.

Of course, if you compile some critical library units in native mode while everything else is in interpreted mode and if you measure an improvement in performance, then this is only good! But you might be missing an opportunity for yet more improvement by not having every unit in the database native. When starting from scratch, use the Database Configuration Assistant to set the plsql_% initialization parameters appropriately and create a new database. To upgrade an existing database to all native, use Oracle's script. (See the Oracle Technical Network for more information.)

Case Study: 170 Systems

170 Systems, Inc. has been an Oracle partner for 11 years and participated in the Beta Program for the Oracle 9i Database with particular interest in PL/SQL native compilation. They've now certified their 170 MarkView Document Management and Imaging SystemT against Oracle 9i and have updated the install scripts to optionally turn on native compilation.

The 170 MarkView Document Management and Imaging System provides Content Management, Document Management, Imaging, and Workflow Solutions-all tightly integrated with the Oracle 9i Database, Oracle 9i Application Server, and the Oracle E- Business Suite. Enabling businesses to capture and manage all of their information online in a single, unified system-regardless of original source or format-the 170 MarkView solution provides scalable, secure, production-quality Internet B2B and intranet access to all of an organization's vital information, while streamlining the associated business processes and maximizing operational efficiency.

A large-scale multi-user, multi-access system, 170 MarkViewT supports the very large numbers of documents, images, concurrent users, and the high transaction rates required by 170 Systems customers. Therefore, performance and scalability are especially important. 170 Systems customers include organizations such as British Telecommunications, E*TRADE Group, the Apollo Group, and the University of Pennsylvania. 170 MarkView uses several different mechanisms to interface to the Oracle 9i Database. Part of the business logic, including preparation of data for presentation, is implemented in the database in PL/SQL.

The computation involves string processing supported by stacks and lists of values modeled as PL/SQL collections. Several PL/SQL modules implement complex logic and include intensive string manipulation and processing. PL/SQL collections are leveraged in this complex processing.

They've observed a performance increase of up to 40 percent for computationally- intensive routines, and no performance degradation, in line with our observations using the code in Example 1 and Example 2 of this article. Native compilation offers compelling advantages to existing and new applications written in PL/SQL. It's just one example of Oracle's commitment to improving PL/SQL "from the bottom on up."

CASE Statements and CASE Expressions

Over the years, the PL/SQL user community has been vocal about how they'd like to see the language improved. These enhancements have ranged from "big picture" functionality like, "Gee, we'd really like to have a debugger!" to very concrete requests such as, "Please let me insert into a table using a record." Another example of a long-requested, very specific desire is support for CASE within PL/SQL. Well, with Oracle 9i, your wish has come true!

Actually, Oracle implemented support for a CASE expression in Oracle8i, but 1) it was only available inside SQL statements, and 2) those SQL statements weren't recognized as valid when compiled within a PL/SQL block. With Oracle 9i, SQL that uses a CASE construct can be used in static SQL in a PL/SQL block...

BEGIN
  FOR j IN (
    SELECT 
      CASE ename 
        WHEN 'SMITH' THEN 'MR. SMITH' 
        WHEN 'ALLEN' THEN 'MR. ALLEN' 
        ELSE         ename 
      END 
      FROM emp
    )
    LOOP ...; END LOOP;
END;

...since any SQL that works in the SQL environment works in the PL/SQL environment by virtue of the new common parser. You can also write your own CASE statements and expressions within your PL/SQL code.

CASE constructs don't offer any fundamentally new semantics (anything you write in a CASE statement can be implemented with IF). They do, however, allow a more compact notation and some elimination of repetition with respect to what otherwise would be expressed with an IF construct. Consider the implementation of a decision table whose predicate is the value of a particular expression. The following two fragments are semantically identical, but coding best practice gurus generally recommend the CASE formulation because it more directly models the idea behind the code:

CASE   expr
  WHEN 1 THEN Action1;
  WHEN 2 THEN Action2;
  WHEN 3 THEN Action3;
  ELSE        ActionOther;
END CASE;

and:

IF      expr = 1 THEN Action1;
  ELSIF expr = 2 THEN Action2;
  ELSIF expr = 3 THEN Action2;
  ELSE                ActionOther;
END IF;

By pulling out the decision expression expr to the start and by mentioning it only once, the programmer's intention is clearer. This is significant both to a person reviewing the code and to the compiler, which therefore has better information from which to generate efficient code. For example, the compiler knows immediately that the decision expression needs to be evaluated just once. Moreover, since the IF formulation repeats the decision expression for each leg, there's a greater risk of typographical error that can be difficult to spot.

Oracle offers a stand-alone CASE statement and a CASE expression (part of a larger statement, usually an expression). Let's look at the statement first.

About CASE statement syntax

The CASE statement begins with the keyword CASE. The keyword is followed by a selector. The selector expression can be arbitrarily complex. For example, it can contain function calls. Usually, however, it consists of a single variable. The selector expression is evaluated only once. The value it yields can have any PL/SQL datatype other than BLOB, BFILE, an object type, a PL/SQL record, an index-by-table, a varray, or a nested table.

So, basically, you can use CASE statements with scalar values, like strings, dates, Booleans, intervals, and so on.

The selector is followed by one or more WHEN clauses, which are checked sequentially. The value of the selector determines which clause is executed. If the value of the selector equals the value of a WHEN-clause expression, that WHEN clause is executed.

The ELSE clause works similarly to the ELSE clause in an IF statement. The ELSE clause is optional. However, if you omit the ELSE clause, PL/SQL adds the following implicit ELSE clause:

ELSE RAISE CASE_NOT_FOUND;

If the CASE statement selects the implicit ELSE clause, PL/SQL raises the predefined exception CASE_NOT_FOUND. So there's always a default action, even when you omit the ELSE clause.

The keywords END CASE terminate the CASE statement. These two keywords must be separated by a space.

Here's another example of a typical CASE statement:

CREATE OR REPLACE FUNCTION 
   grade_translator (grade_in IN VARCHAR2)
   RETURN VARCHAR2
IS
   retval VARCHAR2(100);
BEGIN   
   CASE
      WHEN grade_in = 'A' 
         THEN retval := 'Excellent';
      WHEN grade_in = 'B' 
         THEN retval := 'Very Good';
      WHEN grade_in = 'C' 
         THEN retval := 'Good';
      WHEN grade_in = 'D' 
         THEN retval := 'Fair';
      WHEN grade_in = 'F' 
         THEN retval := 'Poor';
      ELSE retval := 'No such grade';
   END CASE;
   RETURN retval;
END;

The CASE Expression

The CASE expression is a fragment of a statement that returns a value. Its syntax is similar to that of a CASE statement with the following differences:

Here's a rewrite of the previous grade translator using the expression syntax:

CREATE OR REPLACE FUNCTION 
   grade_translator (grade_in IN VARCHAR2)
   RETURN VARCHAR2
IS
BEGIN   
   RETURN 
    CASE
      WHEN grade_in = 'A' 
         THEN 'Excellent'
      WHEN grade_in = 'B' 
         THEN 'Very Good'
      WHEN grade_in = 'C' 
         THEN 'Good'
      WHEN grade_in = 'D' 
         THEN 'Fair'
      WHEN grade_in = 'F' 
         THEN 'Poor'
      ELSE 'No such grade'
   END;
END;

Notice that, in this case, we don't assign the expression to a variable. We RETURN it directly from the function.

Searched CASE Statements and Expressions

With a "searched CASE" statement or expression, you don't provide a selector value for the CASE statement. Instead, the WHEN clauses contain search conditions that evaluate to a Boolean value. Use this form when you aren't checking simply for matching values, but you need to evaluate arbitrarily complex Boolean expressions.

Here are examples of searched CASE statements and expressions:

CASE
  WHEN n = 1  THEN Action1;
  WHEN n = 2  THEN Action2;
  WHEN n = 3  THEN Action3;
  WHEN ( n > 3 and n < 8 ) 
    THEN Action4through7;
  ELSE ActionOther;
END CASE;

and:

text := 
   CASE
     WHEN n = 1 THEN one
     WHEN n = 2 THEN two
     WHEN n = 3 THEN three
     WHEN ( n > 3 and n < 8 ) 
       THEN four_through_seven
     ELSE other
   END;

Example 4 offers one more example of a searched CASE expression, this time using the new Oracle 9i INTERVAL datatype to determine whether I'm too old or too young to be forced to work!

Example 4. Searched CASE expression with INTERVALs.

DECLARE -- Example of CASE searched expression
   cant_play_now BOOLEAN;
   how_young INTERVAL YEAR TO MONTH :=
      (SYSDATE - TO_DATE ('09-23-1958', 'MM-DD-YYYY')) YEAR TO MONTH;
   max_age CONSTANT INTERVAL YEAR TO MONTH := INTERVAL '16' YEAR;
   min_age CONSTANT INTERVAL YEAR TO MONTH := INTERVAL '70' YEAR;
BEGIN
   -- Notice: no semi-colons between WHEN clauses.
   cant_play_now :=
      CASE 
       WHEN how_young < min_age THEN FALSE
        WHEN how_young > max_age THEN FALSE
        ELSE TRUE
      END;
    
   IF cant_play_now 
   THEN
      must_go_to_work;
   END IF;
END;

Bulk Binding in Native Dynamic SQL

One of the most exciting new features in Oracle8i was the native dynamic SQL: an implementation of dynamic SQL that relies on two native statements (EXECUTE IMMEDIATE and OPEN FOR) to run dynamically constructed SQL statements at runtime. NDS is dramatically easier to use than DBMS_SQL and, in most cases, is noticeably faster. But NDS wasn't the only new feature for improving SQL performance and flexibility. Oracle also introduced "bulk binding" for DML and queries.

And never the twain could meet. In other words, we couldn't use bulk binding to run a dynamic SQL statement. In Oracle 9i, all that's changed. We can now take advantage of these two great features together. This section refreshes your knowledge of bulk binding and then shows how to use it with dynamic SQL, finishing up with an explanation of improved error handling for bulk-bind operations.

Suppose we need to write a program to populate elements of a PL/SQL collection from a SELECT query. Our first pre-Oracle8i inclination would be to write what you see in Example 5.

Example 5. Using a cursor FOR loop to populate a collection.

DECLARE
   TYPE employee_ids_t IS 
      TABLE OF employees.employee_id%TYPE
      INDEX BY BINARY_INTEGER;

   employee_ids   employee_ids_t;
   n              INTEGER        := 0;
BEGIN
   FOR j IN  (SELECT employee_id
                FROM employees
               WHERE salary < 3000)
   LOOP
      n :=  n + 1;
      employee_ids (n) := j.employee_id;
   END LOOP;
END;

Using this approach, each explicit row-by-row assignment of the collection element to the cursor component causes a context switch between the PL/SQL engine and the SQL engine resulting in performance overhead. With Oracle8i and above, we can move away from these context switches (and all that code) by performing a BULK COLLECTION, as shown here:

BEGIN
   SELECT employee_id
     BULK COLLECT INTO employee_ids
     FROM employees
    WHERE salary < 3000;
   ...
END;

Wow! That's quite a change in code volume. And you'll see substantial improvements in performance, as well, by minimizing the number of context switches required to execute the block.

But what if you want to execute this same query (and then process the rows) for one of any number of different employee tables, segregated by location?

In Oracle 9i, we can take the preceding query and transform it into a dynamic SQL query that will handle this additional complexity. There are many application implementation situations that require dynamic SQL. Native dynamic SQL (execute immediate and related constructs) is usually preferred over Dbms_Sql because it's easier to write and proofread and executes faster. However, pre-Oracle9i, only Dbms_Sql could be used for dynamic bulk binding. Oracle 9i introduces the following syntax for bulk binding in native dynamic SQL:

CREATE OR REPLACE PROCEDURE
   process_employees (loc_in IN VARCHAR2)
IS
BEGIN
   EXECUTE IMMEDIATE 
     'SELECT employee_id FROM '
     || loc_in || '_employees'
     BULK COLLECT INTO employee_ids;
   ...
END;

It's so nice to see the fairly arbitrary restrictions in the PL/SQL language falling away with each new release! Let's take a look at how you implement "in-binding" (binding variables into the dynamic SQL), "out-binding" (extracting values from the dynamic SQL), and error handling when using bulk operations on dynamic SQL statements.

In-Binding

Both the EXECUTE IMMEDIATE and FORALL (for bulk DML operations) offer a USING clause to bind variable values into the SQL statement. Let's follow the progression of explicit row-from-row processing to bulk binding to bulk binding in native dynamic DML to see how the USING clause is deployed.

We start with this kind of explicit FOR loop in our Oracle7 and Oracle8 code base:

FOR indx IN 
   employee_ids.FIRST .. employee_ids.LAST
LOOP
   UPDATE employees
      SET salary = salary * 1.1
    WHERE employee_id = employee_ids (indx);
END LOOP;

Then, with Oracle8i, we get rid of most of the context switches by moving to FORALL:

FORALL indx IN 
   employee_ids.FIRST .. employee_ids.LAST
  UPDATE employees
     SET salary = salary * 1.1
   WHERE employee_id = employee_ids (indx);

And that handles all of our needs-unless, once again, we need or would like to perform this same operation on different tables, based on location (or for any other kind of dynamic SQL situation). In this case, we can combine FORALL with EXECUTE IMMEDIATE, with these wonderful results:

CREATE OR REPLACE PROCEDURE upd_employees (
  loc_in IN VARCHAR2,
  employees_in IN employees_t )
IS
BEGIN
   FORALL indx in
      employees_in.first..
    employees_in.last
    EXECUTE IMMEDIATE
     'UPDATE '
        || loc_in
          || ' employees
        SET salary = salary*1.1'
          || ' WHERE employee_id = :the_id'
      USING employee_in (indx);
END;

Notice that in the USING clause, we must include both the name of the collection and the subscript for a single row using the same FORALL loop index variable.

Out-Binding

Let's again follow the progression from individual row updates to bulk bind relying on BULK COLLECT INTO to retrieve information, and finally the dynamic approach possible in Oracle 9i.

Oracle8 enhanced DML capabilities by providing support for the RETURNING clause. Shown in the following FOR loop, it allows us to obtain information (in this case, the updated salary) from the DML statement itself (thereby avoiding a separate and expensive query).

BEGIN
   FOR indx IN 
      employee_ids.FIRST .. employee_ids.LAST
   LOOP
      UPDATE    employees
            SET salary = salary * 1.1
          WHERE employee_id = employee_ids (indx)
      RETURNING salary
           INTO salaries (indx);
   END LOOP;
END;

Starting with Oracle8i, we can take advantage of FORALL to improve performance dramatically:

BEGIN
   FORALL indx IN employee_ids.FIRST .. employee_ids.LAST
      UPDATE    employees
            SET salary = salary * 1.1
          WHERE employee_id = employee_ids (indx)
      RETURNING salary
           BULK COLLECT INTO salaries;
END;

There's one seemingly odd aspect of this code you should remember: Inside the DML statement, any reference to the collection that drives the FORALL statement must be subscripted as in:

WHERE employee_id = employee_ids (indx)

In the RETURNING clause, however, you BULK COLLECT INTO the collection and not a single subscripted row of the collection.

That's all well and good, but what if (not to sound like a broken record) we want to execute this same update for any of the employee tables for different locations? Time to go to NDS and, with Oracle 9i only, also employ a RETURNING BULK COLLECT clause:

CREATE OR REPLACE PROCEDURE
   upd_employees (
      loc_in IN VARCHAR2,
    employees_in IN employees_t
)
IS
   my_salaries salaries_t;
BEGIN
   FORALL indx in
      employees_in.first..
    employees_in.last
    EXECUTE IMMEDIATE
     'UPDATE '
        || loc_in
          || ' employees
        SET salary = salary*1.1'
          || ' WHERE employee_id = :the_id
         RETURNING salary INTO :salaries'
      USING employee_in (indx)
    RETURNING BULK COLLECT INTO
       my_salaries;
END;

Handling and Reporting Exceptions

In Oracle8i, FORALL was wonderful for rapid processing of bulk DML statements. One problem with it, however, is that you lose some of the granularity in exception handling that you help with row-by-row processing. Suppose, for example, that we want to load a whole bunch of words into a vocabulary table. We can do it very efficiently as follows:

BEGIN
   FORALL indx IN 
      words.FIRST .. words.LAST
    INSERT INTO vocabulary
                  (text)
        VALUES (words (indx));
END;

If, however, an error occurred in any single INSERT, the entire FORALL statement would fail. This is the sort of scenario that was easily handled with row-level processing, such as with a loop like this:

FOR indx IN words.FIRST .. words.LAST
LOOP
   BEGIN
      INSERT INTO t text)
           VALUES (words (indx));
   EXCEPTION
      WHEN OTHERS
      THEN
         error_codes (indx) := SQLERRM;
   END;
END LOOP;

With this kind of code, we'd insert all rows that didn't cause an error to occur. With Oracle 9i, you can now do this with both static and dynamic FORALL SQL statements, by taking advantage of the SAVE EXCEPTIONS clause.

FORALL indx IN
    words.first..words.last
  SAVE EXCEPTIONS 
  INSERT INTO vocabulary ( text ) 
     VALUES ( words(indx) );

Use of SAVE EXCEPTIONS allows the FORALL to continue through all the rows indicated by the collection; it "saves up" the exceptions as it encounters them. This saving step begs the obvious question: How can you, the developer, get information about the errors that were "saved"? By taking advantage of the new SQL%BULK_COLLECTIONS pseudo-collection, as demonstrated in the code shown in Example 6.

Example 6. Using the SQL%BULK_COLLECTIONS pseudo-collection.

DECLARE
  bulk_errors   EXCEPTION;
  PRAGMA EXCEPTION_INIT (bulk_errors,  -24381);
BEGIN
  FORALL indx IN words.FIRST .. words.LAST
    SAVE EXCEPTIONS
    INSERT INTO t (text)
        VALUES (words (indx));
EXCEPTION
  WHEN bulk_errors
  THEN
    FOR j IN 1 .. SQL%BULK_EXCEPTIONS.COUNT
    LOOP
      log_error (
         SQL%BULK_EXCEPTIONS(indx).ERROR_INDEX,
         SQLERRM(-1 * SQL%BULK_EXCEPTIONS(indx).ERROR_CODE)
      );
    END LOOP;
END;

Each row of this pseudo-collection is a record consisting of two fields: ERROR_INDEX and ERROR_CODE. The former field shows which index in the original bulk-load collection causes the failure. ERROR_CODE is the error number encountered.

You must both use the SAVE EXCEPTIONS construct and handle the BULK_ERRORS exception to get the intended benefit (that is, that all non-erroring rows are inserted).

The March of Progress

The new Oracle 9i features covered in this article should provide a comfortable feeling about Oracle's commitment to the PL/SQL language. Native compilation offers a path to formidable and transparent improvements in our application performance (even, and perhaps especially, existing "legacy" code). The CASE statement and expression, as well as support for dynamic SQL in bulk binding, help round out the language semantics.

This article was originally published in the April 2002 issue of Oracle Professional. The material in Feuerstein's articles--and those he cowrote with Bryn Llewellyn--are based on Oracle Corporation white papers originally prepared by Llewellyn for Oracle OpenWorld 2001 in San Francisco and OracleWorld Copenhagen in June 2002, and Feuerstein book, Oracle PL/SQL Programming, 3rd Edition.

In the next article, we'll embark on an in-depth examination of table functions and cursor expressions.

Steven Feuerstein is considered one of the world's leading experts on the Oracle PL/SQL language.

Bryn Llewellyn is PL/SQL Product Manager, Database and Application Server Technologies Development Group, at Oracle Corporation Headquarters.


O'Reilly & Associates recently released (September 2002) Oracle PL/SQL Programming, 3rd Edition.


Return to the O'Reilly Network.

Copyright © 2007 O'Reilly Media, Inc.