|
Native Compilation, CASE, and Dynamic Bulk Bindingby Steven Feuerstein, coauthor of Oracle PL/SQL Programming, 3rd Edition and Bryn Llewellyn01/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.
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:
Its implementation (that which effects the execution characteristics of a given system of source code)
Language features (the addition of new syntax to express powerful new semantics)
Oracle-supplied PL/SQL library units
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:
UTL_HTTP package|
Related Reading
|
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.
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...
|
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.
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.)
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."
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.
|
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 is a fragment of a statement that returns a value. Its syntax is similar to that of a CASE statement with the following differences:
Each WHEN clause does not end with a semicolon (it's all part of a single expression,
whereas a semicolon indicates the end of a logical statement).
The END clause does not include the CASE keyword. You simply END the expression.
You don't perform assignments within the WHEN clauses. Instead, you simply
provide the value that you want to be assigned after the CASE expression is evaluated.
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.
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;
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.
|
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.
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;
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 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.
Sample Chapter 10, Dates and Timestamps, is available free online.
You can also look at the Table of Contents, the Index, and the Full Description of the book.
For more information, or to order the book, click here.
Return to the O'Reilly Network.
Copyright © 2007 O'Reilly Media, Inc.