Sunteți pe pagina 1din 20

BULK OPERATIONS

INTRO-
PL/SQL is tightly integrated with the underlying SQL engine in the Oracle
database. PL/SQL is the database programming language of choice for Oracle.

But this tight integration does not necessarily mean that there isn’t any overhead
associated with running SQL from a PL/SQL program. When the PL/SQL runtime engine
processes a block of code, it executes the procedural statements within its own engine,
but it passes the SQL statements on to the SQL engine. The SQL layer executes the SQL
statements and then returns information to the PL/SQL engine, if necessary.

This transfer of control between the PL/SQL and SQL engines is called a context switch.
Each time a switch occurs, there is additional overhead. There are a number of scenarios
in which many switches occur and performance degrades. Oracle 8.1 now offers two
enhancements to PL/SQL that allow you to bulk together multiple context switches into a
single switch, thereby improving the performance of your applications.

These new features are as follows:

FORALL - A variation on the FOR loop that bundles together multiple DML
statements based on data in a collection

BULK COLLECT - An enhancement to implicit and explicit query cursor syntax that
allows the transfer of multiple rows of data in a single round-trip between the PL/SQL
and SQL engines

Context-Switching Problem Scenarios

The following are scenarios where excessive context switches are likely to cause
problems. These are likely to happen when you are processing multiple rows of
information stored (or to be deposited) in a collection (a VARRAY, nested table, index-by
table, or host array).

Suppose, for example, that two variable arrays have been filled with managers ID
numbers and the latest count of their employees. You then want to update a table with this
information. Here’s the solution prior to Oracle 8.1 (referencing a couple of already
defined variable arrays):

CREATE OR REPLACE PROCEDURE update_tragedies (


mngr_ids IN name_varray,
num_emps IN number_varray
)
IS
BEGIN
FOR indx IN mngr_ids.FIRST .. mngr_ids.LAST
LOOP

1
UPDATE manager
SET employee_count = num_emps (indx)
WHERE mngr_id = mngr_ids (indx);
END LOOP;
END;

If you needed to update 100 rows, then you would be performing 100 context switches,
since each update is processed in a separate trip to the SQL engine.

You can also run into lots of switching when you fetch multiple rows of information from
a cursor into a collection. Here is an example of the kind of code that cries out for the
Oracle 8.1 bulk collection feature:

DECLARE
CURSOR major_polluters IS
SELECT name, mileage
FROM cars_and_trucks
WHERE vehicle_type IN ('SUV', 'PICKUP');
names name_varray := name_varray();
mileages number_varray := number_varray();
BEGIN
FOR bad_car IN major_polluters
LOOP
names.EXTEND;
names (major_polluters%ROWCOUNT) := bad_car.name;
mileages.EXTEND;
mileages (major_polluters%ROWCOUNT) := bad_car.mileage;
END LOOP;

... now work with data in the arrays ...


END;

If you find yourself writing code like either of the previous examples, you will be much
better off switching to one of the bulk operations. In particular, you should keep an eye
out for these cues in your code:

• A recurring SQL statement inside a PL/SQL loop (it doesn’t have to be a FOR loop,
but that is the most likely candidate).

• Some parameter that can be made a bind variable. You need to be able to load those
values into a collection to then have it processed by FORALL.

PL/SQL is tightly integrated with the underlying SQL engine in the Oracle database.
PL/SQL is the database programming language of choice for Oracle.

2
FOR ALL STATEMENT -

PL/SQL has a new keyword: FORALL. FORALL tells the PL/SQL runtime
engine to bulk bind into the SQL statement all the elements of one or more collections
before sending anything to the SQL engine.

Although the FORALL statement contains an iteration scheme (it iterates through all the
rows of a collection), it is not a FOR loop. It does not, consequently, have either a LOOP
or an END LOOP statement.

Here is the FORALL syntax and examples.


FORALL <index_name> IN lower_bound .. upper_bound <sql_statement>

********
DISCRIPTION
The FORALL statement instructs the PL/SQL engine to bulk-bind input
collections before sending them to the SQL engine. Although the FORALL statement
contains an iteration scheme, it is not a FOR loop.

The SAVE EXCEPTIONS parameter are optional keywords cause the FORALL loop to
continue even if some DML operations fail. The details of the errors are available after
the loop in SQL%BULK_EXCEPTIONS. The program can report or clean up all the
errors after the FORALL loop, rather than handling each exception as it happens.

Usage Notes -
The SQL statement can reference more than one collection. However, the PL/SQL
engine bulk-binds only subscripted collections.
All collection elements in the specified range must exist. If an element is missing or was
deleted, you get an error.
If a FORALL statement fails, database changes are rolled back to an implicit savepoint
marked before each execution of the SQL statement. Changes made during previous
executions are not rolled back.

EXAMPLE
The following example shows that you can use the lower and upper bounds to
bulk-bind arbitrary slices of a collection:

DECLARE
TYPE NumList IS VARRAY(15) OF NUMBER;
depts NumList := NumList();
BEGIN
-- fill varray here
...
FORALL j IN 6..10 -- bulk-bind middle third of varray
UPDATE emp SET sal = sal * 1.10 WHERE deptno = depts(j);
END;

3
Remember, the PL/SQL engine bulk-binds only subscripted collections. So, in the
following example, it does not bulk-bind the collection sals, which is passed to the
function median:

FORALL i IN 1..20
INSERT INTO emp2 VALUES (enums(i), names(i), median(sals), ...);

**********

You must follow these rules when using FORALL:

• The body of the FORALL statement is a single DML statement—an INSERT,


UPDATE, or DELETE.

• The DML must reference collection elements, indexed by the index_row variable
in the FORALL statement. The scope of the index_row variable is the FORALL
statement only; you may not reference it outside of that statement.

• Do not declare an INTEGER variable for index_row. It is declared implicitly by


the PL/SQL engine.

• The lower and upper bounds must specify a valid range of consecutive index
numbers for the collection(s) referenced in the SQL statement. The following
script, for example:

DECLARE
TYPE NumList IS TABLE OF NUMBER;
ceo_payoffs NumList :=
NumList(1000000, 42000000, 20000000, 17900000);
BEGIN
ceo_payoffs.DELETE(3); -- delete third element
FORALL indx IN ceo_payoffs.FIRST..ceo_payoffs.LAST
UPDATE excessive_comp
SET salary = ceo_payoffs(indx)
WHERE layoffs > 10000;
END;

will cause the following error:

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

This error occurs because the DELETE method has removed an element from the
collection; the FORALL statement requires a densely filled collection.

4
• The collection subscript referenced in the DML statement cannot be an
expression. For example, the following script:

DECLARE
names name_varray := name_varray();
BEGIN
FORALL indx IN names.FIRST .. names.LAST
DELETE FROM emp WHERE ename = names(indx+10);
END;
/

will cause the following error:

PLS-00430: FORALL iteration variable INDX is not allowed in this context

The DML statement can reference more than one collection. The upper and lower bounds
do not have to span the entire contents of the collection(s). When this statement is bulk
bound and passed to SQL, the SQL engine executes the statement once for each index
number in the range. In other words, the same SQL statements will be executed, but they
will all be run in the same round-trip to the SQL layer, minimizing the context switches.

ROLLBACK Behavior with FORALL

The FORALL statement allows you to pass multiple SQL statements all together (in
bulk) to the SQL engine. This means that as far as context switching is concerned, you
have one SQL “block,” but these blocks are still treated as individual DML operations.

What happens when one of those DML statements fails? The following rules apply:

• The FORALL statement stops executing. It isn’t possible to request that the
FORALL skip over the offending statement and continue on to the next row in the
collection.

• The DML statement being executed is rolled back to an implicit savepoint marked
by the PL/SQL engine before execution of the statement.

• Any previous DML operations in that FORALL statement that already executed
without error are not rolled back.

The following script demonstrates this behavior.

First, create a table for types of notebook paper and fill it with some information:

CREATE TABLE paper_type (


color VARCHAR2(15), style VARCHAR2(100), qty INTEGER);
INSERT INTO paper_type VALUES('Yellow', 'Legal', 100000);

5
INSERT INTO paper_type VALUES('Yellow', 'Lined', 50000);
INSERT INTO paper_type VALUES('White', 'Spiral', 25000000);

Then use FORALL to update the type to include the number of people using that style of
paper.

DECLARE
TYPE StgList IS TABLE OF VARCHAR2(100);
styles StgList := StgList ('Legal', 'Lined', 'Spiral');
BEGIN
FORALL indx IN style.FIRST..style.LAST
UPDATE paper_type SET style = name || '-' || killed
WHERE style = styles(indx);

DBMS_OUTPUT.PUT_LINE ('Update performed!');


EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE ('Update did not complete!');
COMMIT;
END;
/

Take note of two things:

• The styles in the nested table named “styles” have been placed in alphabetical
order; thus, the update for the Spiral will be the last one processed.

• When you concatenate the color and quantity tables for the spiral paper, the length
of this string exceeds 15 characters. This will raise a VALUE_ERROR exception.

To see the impact of this block, run the script with queries to show the contents of the
paper_type table:

SQL> @forallerr

Paper Types
---------------
Legal
Lined
Spiral

Use FORALL for update...


Update did not complete!

Paper Types
---------------

6
Legal-100000
Lined-50000
Spiral

As you can see, the first two changes stuck, whereas the last attempt to change the name
failed, causing a rollback, but only to the beginning of that third UPDATE statement.

You can check the SQL%BULK_ROWCOUNT cursor attribute to find how many of
your DML statements succeeded.

Continuing Past Exceptions with FORALL

Oracle9i offers a new clause, SAVE EXCEPTIONS, which can be used inside a
FORALL statement. By including this clause,you instruct Oracle to continue processing
even when an error has occurred. Oracle 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
pseudo-collection called SQL%BULK_EXCEPTIONS to obtain error information.

Here is an example, followed by an explanation of what is going on:

/* File on web: bulkexc.sql */


1 CREATE OR REPLACE PROCEDURE bulk_exceptions (
2 whr_in IN VARCHAR2 := NULL)
3 IS
4 TYPE namelist_t IS TABLE OF VARCHAR2 (100);
5 enames_with_errors namelist_t := -- Max of 10 characters in emp.
6 namelist_t ('LITTLE', 'BIGBIGGERBIGGEST', 'SMITHIE', '');
7 bulk_errors EXCEPTION;
8 PRAGMA EXCEPTION_INIT ( bulk_errors, -24381 );
9 BEGIN
10 FORALL indx IN
11 enames_with_errors.FIRST ..
12 enames_with_errors.LAST
13 SAVE EXCEPTIONS
14 EXECUTE IMMEDIATE
15 UPDATE emp SET ename = :newname'
16 USING enames_with_errors(indx);
17 EXCEPTION
18 WHEN bulk_errors
19 THEN
20 FOR indx IN 1 .. SQL%BULK_EXCEPTIONS.COUNT
21 LOOP
22 DBMS_OUTPUT.PUT_LINE (
23 'Error ' || indx || ' occurred during ' ||
24 'iteration ' || SQL%BULK_EXCEPTIONS(indx).ERROR_INDEX ||

7
25 ' updating name to ' ||
26 enames_with_errors (
27 SQL%BULK_EXCEPTIONS(indx).ERROR_INDEX));
28 DBMS_OUTPUT.PUT_LINE (
29 'Oracle error is ' ||
30 SQLERRM (-1 * SQL%BULK_EXCEPTIONS(indx).ERROR_CODE));
31 END LOOP;
32 END;

When this code is run (with SERVEROUTPUT turned on), the following results occur:

SQL> exec bulk_exceptions

Error 1 occurred during iteration 2 updating name to BIGBIGGERBIGGEST


Oracle error is ORA-01401: inserted value too large for column

Error 2 occurred during iteration 4 updating name to


Oracle error is ORA-01407: cannot update ( ) to NULL

In other words, Oracle encountered two exceptions as it processed the DML for the
names collection. It did not stop with the first exception, but continued on, cataloguing a
third.

The following table describes the error-handling functionality in this code:

Line(s) Description

4–6 Declare and populate a collection that will drive the FORALL statement. I have
intentionally placed data in the collection that will raise two errors.

8–9 Declare a named exception to make the exception section more readable.

11–17 Execute a dynamic UPDATE statement with FORALL using the


enames_with_errors collection.

19 Trap the “bulk exceptions error” by name. I could also have written code like:
WHEN OTHERS THEN
IF SQLCODE = -24381

20 Use a numeric FOR loop to scan through the contents of the


SQL%BULKEXCEPTIONS pseudo-collection. 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.

22–30 Extract the information from the collection and display (or log) error information.

8
24 The ERROR_INDEX field of each pseudo-collection’s row returns the row
number in the driving collection of the FORALL statement for which an exception was
raised.

30 The ERROR_CODE field of each pseudo-collection’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 multiple it by –1 before passing it to SQLERRM or displaying
the information.

BULK COLLECT -

Bulk Querying with the BULK COLLECT Clause

PL/SQL now offers the BULK COLLECT keywords. The BULK COLLECT clause in
your cursor (explicit or implicit) tells the SQL engine to bulk bind the output from the
multiple rows fetched by the query into the specified collections before returning control
to the PL/SQL engine. The syntax for this clause is:

BULK COLLECT INTO collection_name

collection_name identifies a collection.

Here are some rules and restrictions to keep in mind when using BULK COLLECT:

• Prior to Oracle9i, you could use BULK COLLECT only with static SQL. With
Oracle9i, you can use BULK COLLECT with both dynamic and static SQL.

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

• The collections you reference can store only scalar values (strings, numbers, dates).
In other words, you cannot fetch a row of data into a record structure that is a row in a
collection.

• The SQL engine automatically initializes and extends the collections you reference in
the BULK COLLECT clause. It starts filling the collections at index 1, inserts
elements consecutively (densely), and overwrites the values of any elements that were
previously defined.

• You cannot use the SELECT…BULK COLLECT statement in a FORALL statement.

Let’s explore these rules and the usefulness of BULK COLLECT through a series of
examples.

First, here is a recoding of the “major polluters” example using BULK COLLECT:

9
DECLARE
names name_varray;
mileages number_varray;
BEGIN
SELECT name, mileage
BULK COLLECT INTO names, mileages
FROM cars_and_trucks
WHERE vehicle_type IN ('SUV', 'PICKUP');

... now work with data in the arrays ...


END;

You are now able to remove the initialization and extension code from the row-by-row
fetch implementation.

But you don’t have to rely on implicit cursors to get this job done. Here is another re-
working of the major polluters example, retaining the explicit cursor:

DECLARE
CURSOR major_polluters IS
SELECT name, mileage
FROM cars_and_trucks
WHERE vehicle_type IN ('SUV', 'PICKUP');
names name_varray;
mileages number_varray;
BEGIN
OPEN major_polluters;
FETCH major_polluters BULK COLLECT INTO names, mileages;

... now work with data in the arrays ...


END;

Restricting Bulk Collection with ROWNUM


There is no regulator mechanism built into BULK COLLECT. If your SQL
statement identifies 100,000 rows of data, then the column values of all 100,000 rows
will be loaded into the target collections. This can cause serious problems in your
application—and in system memory. Remember: these collections are allocated for each
session. So if you have 100 users all running the same program that bulk collects 100,000
rows of information, then real memory is needed for a total of 10 million rows.

There are several things that can be done to avoid this problem. First of all, be careful
about the queries you write and those you offer to developers and/or users to run. You
shouldn’t provide unrestricted access to very large tables.

10
You can also fall back on ROWNUM to limit the number of rows processed by your
query. For example, suppose that a cars_and_trucks table has a very large number of rows
of vehicles that qualify as major polluters. A ROWNUM condition could be added to the
WHERE clause and another parameter to the packaged cursor as follows:

CREATE OR REPLACE PACKAGE pollution


IS
CURSOR major_polluters (
typelist IN VARCHAR2, maxrows IN INTEGER := NULL)
IS
SELECT name, mileage
FROM cars_and_trucks
WHERE INSTR (typelist, vehicle_type) > 0
AND ROWNUM < LEAST (maxrows, 10000);

PROCEDURE get_major_polluters (
typelist IN VARCHAR2,
names OUT name_varray,
mileages OUT number_varray);
END;

Now there is no way that anyone can ever get more than 10,000 rows in a single
query—and the user of that cursor (an individual developer) can also add a further
regulatory capability by overriding that 10,000 with an even smaller number.

Bulk Fetching of Multiple Columns -

You can bulk fetch the contents of multiple columns. However, you must fetch them into
separate collections, one per column.

You cannot fetch into a collection of records (or objects). The following example
demonstrates the error that you will receive if you try to do this:

DECLARE
TYPE VehTab IS TABLE OF cars_and_trucks%ROWTYPE;
gas_guzzlers VehTab;
CURSOR low_mileage_cur IS SELECT * FROM cars_and_trucks WHERE mileage <
10;
BEGIN
OPEN low_mileage_cur;
FETCH low_mileage_cur BULK COLLECT INTO gas_guzzlers;
END;
/

11
When I run this code, I get the following somewhat obscure error message:

PLS-00493: invalid reference to a server-side object or


function in a local context

You will instead have to write this block as follows:

DECLARE
guzzler_type name_varray;
guzzler_name name_varray;
guzzler_mileage number_varray;

CURSOR low_mileage_cur IS
SELECT vehicle_type, name, mileage
FROM cars_and_trucks WHERE mileage < 10;
BEGIN
OPEN low_mileage_cur;
FETCH low_mileage_cur BULK COLLECT
INTO guzzler_type, guzzler_name, guzzler_mileage;
END;

Using a Returning Clause with Bulk Collect


You can use BULK COLLECT inside a FORALL statement, in order to take
advantage of the RETURNING clause.

The RETURNING clause, new to Oracle8, 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
just completed.

The following example illustrates this point.

Suppose a law is passed requiring that a company pay its highest-compensated employee
no more than 50 times the salary of its lowest-paid employee. A large company employs a
total of 250,000 workers. The CEO is not taking a pay cut, so we need to increase the
salaries of everyone who makes less than 50 times his 2004 total compensation package
of $145 million—and decrease the salaries of all upper management except for the CEO.

You will want to use FORALL to get the job done as quickly as possible. However, you
also need to perform various kinds of processing on the employee data and then print a
report showing the change in salary for each affected employee. That RETURNING
clause would come in handy here.

First, create a reusable function to return the compensation for an executive:

12
FUNCTION salforexec (title_in IN VARCHAR2) RETURN NUMBER
IS
CURSOR ceo_compensation IS
SELECT salary + bonus + stock_options +
mercedes_benz_allowance + yacht_allowance
FROM compensation
WHERE title = title_in;
big_bucks NUMBER;
BEGIN
OPEN ceo_compensation;
FETCH ceo_compensation INTO big_bucks;
RETURN big_bucks;
END;
/

In the main block of the update program, a number of local variables and the following
query to identify underpaid employees and overpaid employees is declared:

DECLARE
big_bucks NUMBER := salforexec ('CEO');
min_sal NUMBER := big_bucks / 50;
names name_varray;
old_salaries number_varray;
new_salaries number_varray;

CURSOR affected_employees (ceosal IN NUMBER)


IS
SELECT name, salary + bonus old_salary
FROM compensation
WHERE title != 'CEO'
AND ((salary + bonus < ceosal / 50)
OR (salary + bonus > ceosal / 10)) ;

At the start of the executable section, load all of this data into the collections with a
BULK COLLECT query:

OPEN affected_employees (big_bucks);


FETCH affected_employees
BULK COLLECT INTO names, old_salaries;
Then I can use the names collection in my FORALL update:
FORALL indx IN names.FIRST .. names.LAST
UPDATE compensation
SET salary =
DECODE (
GREATEST (min_sal, salary),

13
min_sal, min_sal,
salary / 5)
WHERE name = names (indx)
RETURNING salary BULK COLLECT INTO new_salaries;

Use DECODE to give an employee either a major boost in yearly income or an 80% cut
in pay. End it with a RETURNING clause that relies on BULK COLLECT to populate a
third collection: the new salaries.

Finally, since you used RETURNING and don’t have to write another query against the
compensation table to obtain the new salaries, you can immediately move to report
generation:

FOR indx IN names.FIRST .. names.LAST


LOOP
DBMS_OUTPUT.PUT_LINE (
RPAD (names(indx), 20) ||
RPAD (' Old: ' || old_salaries(indx), 15) ||
' New: ' || new_salaries(indx)
);
END LOOP;

Here, then, is the report generated from the script:

John DayAndNight Old: 10500 New: 2900000


Holly Cubicle Old: 52000 New: 2900000
Sandra Watchthebucks Old: 22000000 New: 4000000

The RETURNING column values or expressions returned by each execution in FORALL


are added to the collection after the values returned previously. If you use RETURNING
inside a non-bulk FOR loop, previous values are overwritten by the latest DML
execution.

Using Cursor Attribute with Bulk Operation


Whenever you work with explicit and implicit cursors (including cursor
variables), PL/SQL provides a set of cursor attributes that return information about the
cursor. PL/SQL 8.1 adds another, composite attribute, SQL%BULK_ROWCOUNT, for
use with or after the FORALL statement. All of the current attributes are summarized in
the table below.

Cursor Attribute Effect -


cur%FOUND Returns TRUE if the last FETCH found a row
cur%NOTFOUND Returns FALSE if the last FETCH found a row
cur%ISOPEN Returns TRUE if the specified cursor is open.

14
cur%ROWCOUNT Returns the number of rows modified by the DML tatement
SQL%BULK_ROWCOUNT Returns the number of rows processed for each execution
of bulk DML operation

In these attributes, cur is the name of an explicit cursor, a cursor variable, or the
string “SQL” for implicit cursors (UPDATE, DELETE, and INSERT statements, since
none of the attributes can be applied to an implicit query). The %BULK_ROWCOUNT
structure has the same semantics as an index-by table. The nth row in this pseudo index-
by table stores the number of rows processed by the nth execution of the DML operation
in the FORALL statement.

Let’s examine the behavior of these cursor attributes in FORALL and BULK COLLECT
statements by running a sample script. Start by creating a utility function and general
show_attributes procedure:

CREATE OR REPLACE FUNCTION boolstg (bool IN BOOLEAN)


RETURN VARCHAR2
IS
BEGIN
IF bool THEN RETURN 'TRUE ';
ELSIF NOT bool THEN RETURN 'FALSE';
ELSE RETURN 'NULL ';
END IF;
END;
/

CREATE OR REPLACE PROCEDURE show_attributes (


depts IN number_varray)
IS
BEGIN
FORALL indx IN depts.FIRST .. depts.LAST
UPDATE emp
SET sal = sal + depts(indx)
WHERE deptno = depts(indx);

DBMS_OUTPUT.PUT_LINE (
'FOUND-' || boolstg(SQL%FOUND) || ' ' ||
'NOTFOUND-' || boolstg(SQL%NOTFOUND) || ' ' ||
'ISOPEN-' || boolstg(SQL%ISOPEN) || ' ' ||
'ROWCOUNT-' || NVL (TO_CHAR (SQL%ROWCOUNT), 'NULL'));

FOR indx IN depts.FIRST .. depts.LAST


LOOP
DBMS_OUTPUT.PUT_LINE (
depts(indx) || '-' || SQL%BULK_ROWCOUNT(indx));
END LOOP;

15
ROLLBACK;
END;
/

Then run a query to show some data and show the attributes for two different lists of
department numbers, followed by a use of BULK COLLECT:

SELECT deptno, COUNT(*) FROM emp GROUP BY deptno;

DECLARE
/* No employees in departments 98 and 99 */
depts1 number_varray := number_varray (10, 20, 98);
depts2 number_varray := number_varray (99, 98);
BEGIN
show_attributes (depts1);
show_attributes (depts2);
END;
/
DECLARE
CURSOR allsals IS
SELECT sal FROM emp;
salaries number_varray;
BEGIN
OPEN allsals;
FETCH allsals BULK COLLECT INTO salaries;

DBMS_OUTPUT.PUT_LINE (
'FOUND-' || boolstg(SQL%FOUND) || ' ' ||
'NOTFOUND-' || boolstg(SQL%NOTFOUND) || ' ' ||
'ISOPEN-' || boolstg(SQL%ISOPEN) || ' ' ||
'ROWCOUNT-' || NVL (TO_CHAR (SQL%ROWCOUNT), 'NULL'));
END;
/

Here is the output from this script:

DEPTNO COUNT(*)
------ ---------
10 3
20 5
30 6

FOUND-TRUE NOTFOUND-FALSE ISOPEN-FALSE ROWCOUNT-8


10-3
98-0

16
20-5
FOUND-FALSE NOTFOUND-TRUE ISOPEN-FALSE ROWCOUNT-0
99-0
98-0
FOUND-NULL NOTFOUND-NULL ISOPEN-FALSE ROWCOUNT-NULL

From this output, we can conclude the following:

• For FORALL, %FOUND and %NOTFOUND reflect the overall results, not the
results of any individual statement, including the last (this contradicts Oracle
documentation). In other words, if any one of the statements executed in the
FORALL modified at least one row, %FOUND returns TRUE and %NOTFOUND
returns FALSE.

• For FORALL, %ISOPEN always returns FALSE because the cursor is closed when
the FORALL statement terminates.

• For FORALL, %ROWCOUNT returns the total number of rows affected by all the
FORALL statements executed, not simply the last statement.

• For BULK COLLECT, %FOUND and %NOTFOUND always return NULL and
%ISOPEN returns FALSE because the BULK COLLECT has completed the fetching
and closed the cursor. %ROWCOUNT always returns NULL, since this attribute is
only relevant for DML statements.

• The nth row in this pseudo index-by table stores the number of rows processed by the
nth execution of the DML operation in the FORALL statement. If no rows are
processed, then the value in %BULK_ROWCOUNT is set to 0.

• The %BULK_ROWCOUNT attribute is a handy device, but it is also quite limited.


Keep the following in mind:

• Even though it looks like an index-by table, you cannot apply any methods to it.

• %BULK_ROWCOUNT cannot be assigned to other collections. Also, it cannot be


passed as a parameter to subprograms.

• The only rows defined for this pseudo index-by table are the same rows defined in the
collection referenced in the FORALL statement.

• If you reference a row in %BULK_ROWCOUNT that is outside the defined


subscripts, you will not raise a NO_DATA_FOUND error or subscript error. It will
simply return a NULL value.

If you try to execute code like either of these statements:

17
DBMS_OUTPUT.PUT_LINE (SQL%BULK_ROWCOUNT.COUNT);

IF SQL%BULK_ROWCOUNT.FIRST IS NOT NULL

you get this error:

PLS-00332: "%BULK_ROWCOUNT" is not a valid prefix for a qualified name

All you can really do with %BULK_ROWCOUNT is reference individual rows in this
special structure.

Bulk Bind Improvements

Oracle9i bulk binding has been enhanced. First, Oracle9i now allows you to use
bulk binding with Oracle collection types with the select and fetch clauses. Bulk binding
of records used in insert and update statements is also supported.

Error processing for bulk binds has been much improved. Previously, errors during bulk-
bind operations would cause the operation to stop, and an exception would be raised.
Oracle has provided the ability to allow the application to handle the error and continue
the bulk-bind process. Errors are collected during the operation and returned when the
bulk-bind operation is complete.

Bulk error handling is provided through the use of the new save exceptions keyword in a
forall statement. All errors will be stored in a new Oracle cursor attribute,
%bulk_exceptions. This cursor stores the error number and message within it for each
SQL error. The total number of errors is also stored as an attribute of %bulk_exceptions
(%bulk_exceptions.count). As a result, the number of subscripts within
%bulk_exceptions will naturally be one to %bulk_exceptions.count. Failure to use save
exceptions will cause the bulk-bind operation to operate as it always has, stopping at the
first error that occurs. You can still check %bulk_exception for the error information in
this case. Here is an example of a bulk-bind operation that, first, relies on Oracle8i's
restrictions in terms of error handling and then uses the save exceptions clause in
Oracle9i:

CREATE OR REPLACE PROCEDURE bulk_exceptions (whr_in IN VARCHAR2 :=


NULL)
IS
TYPE numlist_t IS TABLE OF NUMBER;

TYPE namelist_t IS TABLE OF VARCHAR2 (100);

enames_with_errors namelist_t := -- Max of 10 characters allowed in emp.

18
namelist_t ('LITTLE', 'BIGBIGGERBIGGEST', 'SMITHIE', '');

l_count PLS_INTEGER;

bulk_errors EXCEPTION;
PRAGMA EXCEPTION_INIT ( bulk_errors, -24381 );
BEGIN
-- In Oracle8i, FORALL statement aborts on first error.
-- Successful statements are NOT rolled back.
BEGIN
FORALL indx IN enames_with_errors.FIRST .. enames_with_errors.LAST
EXECUTE IMMEDIATE
'UPDATE emp SET ename = :newname'
USING enames_with_errors(indx);
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.PUT_LINE ('Bulk error in FORALL: ' || SQLERRM);
END;

-- Use SAVE EXCEPTIONS and SQL%BULK_EXCEPTIONS (a pseudo-collection of


records)
-- to continue past exceptions and then review the exceptions afterwards.
-- Note: FORALL still raises an exception that you will need to trap.
BEGIN
FORALL indx IN enames_with_errors.FIRST .. enames_with_errors.LAST
SAVE EXCEPTIONS
EXECUTE IMMEDIATE
'UPDATE emp SET ename = :newname'
USING enames_with_errors(indx);
EXCEPTION
WHEN bulk_errors
THEN
-- FIRST and LAST methods are NOT available on this pseudo-collection.
--l_first := SQL%BULK_EXCEPTIONS.FIRST;
--l_last := SQL%BULK_EXCEPTIONS.LAST;
l_count := SQL%BULK_EXCEPTIONS.COUNT;

FOR indx IN 1 .. l_count


LOOP
DBMS_OUTPUT.PUT_LINE (
'Error ' || indx || ' occurred during ' ||
'iteration ' || SQL%BULK_EXCEPTIONS(indx).ERROR_INDEX ||
' updating name to ' || enames_with_errors(indx));
DBMS_OUTPUT.PUT_LINE (
'Oracle error is ' ||

19
SQLERRM(-1 * SQL%BULK_EXCEPTIONS(indx).ERROR_CODE));
END LOOP;
END;

p.l (SQL%BULK_EXCEPTIONS.COUNT);
END;
/
And here is the output from running this script:
SQL> exec bulk_exceptions
Bulk error in FORALL: ORA-01401: inserted value too large for column
Error 1 occurred during iteration 2 updating name to LITTLE
Oracle error is ORA-01401: inserted value too large for column
Error 2 occurred during iteration 4 updating name to BIGBIGGERBIGGEST
Oracle error is ORA-01407: cannot update () to NULL

20

S-ar putea să vă placă și