27
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 1

BulkOp -ForAll & Bulk Collect

  • Upload
    sbukka

  • View
    113

  • Download
    0

Embed Size (px)

Citation preview

Page 1: BulkOp -ForAll & Bulk Collect

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 )ISBEGIN FOR indx IN mngr_ids.FIRST .. mngr_ids.LAST LOOP

1

Page 2: BulkOp -ForAll & Bulk Collect

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

Page 3: BulkOp -ForAll & Bulk Collect

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.

EXAMPLEThe following example shows that you can use the lower and upper bounds to

bulk-bind arbitrary slices of a collection:

DECLARETYPE 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

Page 4: BulkOp -ForAll & Bulk Collect

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..20INSERT 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

Page 5: BulkOp -ForAll & Bulk Collect

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

Page 6: BulkOp -ForAll & Bulk Collect

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---------------LegalLinedSpiral

Use FORALL for update...Update did not complete!

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

6

Page 7: BulkOp -ForAll & Bulk Collect

Legal-100000Lined-50000Spiral

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 IS4 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 BEGIN10 FORALL indx IN11 enames_with_errors.FIRST ..12 enames_with_errors.LAST13 SAVE EXCEPTIONS14 EXECUTE IMMEDIATE15 UPDATE emp SET ename = :newname'16 USING enames_with_errors(indx);17 EXCEPTION18 WHEN bulk_errors19 THEN20 FOR indx IN 1 .. SQL%BULK_EXCEPTIONS.COUNT21 LOOP22 DBMS_OUTPUT.PUT_LINE (23 'Error ' || indx || ' occurred during ' ||24 'iteration ' || SQL%BULK_EXCEPTIONS(indx).ERROR_INDEX ||

7

Page 8: BulkOp -ForAll & Bulk Collect

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 BIGBIGGERBIGGESTOracle error is ORA-01401: inserted value too large for column

Error 2 occurred during iteration 4 updating name toOracle 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 THENIF 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

Page 9: BulkOp -ForAll & Bulk Collect

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

Page 10: BulkOp -ForAll & Bulk Collect

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

Page 11: BulkOp -ForAll & Bulk Collect

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 pollutionIS 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

Page 12: BulkOp -ForAll & Bulk Collect

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 CollectYou 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

Page 13: BulkOp -ForAll & Bulk Collect

FUNCTION salforexec (title_in IN VARCHAR2) RETURN NUMBERIS 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

Page 14: BulkOp -ForAll & Bulk Collect

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.LASTLOOP 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: 2900000Holly Cubicle Old: 52000 New: 2900000Sandra 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 OperationWhenever 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 rowcur%NOTFOUND Returns FALSE if the last FETCH found a rowcur%ISOPEN Returns TRUE if the specified cursor is open.

14

Page 15: BulkOp -ForAll & Bulk Collect

cur%ROWCOUNT Returns the number of rows modified by the DML tatementSQL%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 VARCHAR2ISBEGIN 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)ISBEGIN 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

Page 16: BulkOp -ForAll & Bulk Collect

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-810-398-0

16

Page 17: BulkOp -ForAll & Bulk Collect

20-5FOUND-FALSE NOTFOUND-TRUE ISOPEN-FALSE ROWCOUNT-099-098-0FOUND-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

Page 18: BulkOp -ForAll & Bulk Collect

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. namelist_t ('LITTLE', 'BIGBIGGERBIGGEST', 'SMITHIE', '');

18

Page 19: BulkOp -ForAll & Bulk Collect

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 ' || SQLERRM(-1 * SQL%BULK_EXCEPTIONS(indx).ERROR_CODE));

19

Page 20: BulkOp -ForAll & Bulk Collect

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 columnError 1 occurred during iteration 2 updating name to LITTLEOracle error is ORA-01401: inserted value too large for columnError 2 occurred during iteration 4 updating name to BIGBIGGERBIGGESTOracle error is ORA-01407: cannot update () to NULL

20