Bulk SQL - Oracle PL/SQL by Example, Fifth Edition (2015)

Oracle PL/SQL by Example, Fifth Edition (2015)

Chapter 18. Bulk SQL


In this chapter, you will learn about

Image FORALL Statements

Image The BULK COLLECT Clause

Image Binding Collections in SQL Statements


In Chapter 1, you learned that the PL/SQL engine sends SQL statements to the SQL engine, which then returns results back to the PL/SQL engine. The communication between the PL/SQL and SQL engines is also known as a context switch. A certain performance overhead is associated with these context switches. However, the PL/SQL language has a number of features that can minimize the performance overhead, which are collectively known as bulk SQL. Generally, if a SQL statement affects four or more rows, bulk SQL may improve performance significantly. Bulk SQL supports batch processing of SQL statements and their results, and it consists of two features, the FORALL statement and the BULK COLLECT clause.

Starting with Oracle 12c, support for collection data types and bulk SQL has been extended to dynamic SQL. As a consequence, you are able to bind collection variables when using an EXECUTE IMMEDIATE statement or OPEN-FOR, FETCH, and CLOSE statements. This ability is covered in detail in Lab 18.3.

Lab 18.1: FORALL Statements


After this Lab, you will be able to

Image Use FORALL Statements

Image Use the SAVE EXCEPTIONS Option

Image Use the INDICES OF Option

Image Use the VALUES OF Option


Consider an INSERT statement enclosed by a numeric FOR loop that iterates 10 times, as shown in Listing 18.1.

Listing 18.1 INSERT Statement Enclosed by a Numeric FOR Loop

FOR i IN 1..10
LOOP
INSERT INTO table_name
VALUES (...);
END LOOP;

This INSERT statement will be sent from the PL/SQL engine to the SQL engine 10 times. In other words, 10 context switches take place. If the numeric FOR loop is replaced with a FORALL statement, however, the INSERT statement is sent only once from PL/SQL engine to the SQL engine, yet it is still executed 10 times. In this case, there is only one context switch between PL/SQL and SQL.

Using FORALL Statements

The FORALL statement sends INSERT, UPDATE, or DELETE statements in batches from the PL/SQL engine to the SQL engine instead of one statement at a time. It has the structure shown in Listing 18.2 (the reserved words and phrases surrounded by brackets are optional).

Listing 18.2 FORALL Statement Syntax

FORALL loop_counter IN bounds_clause
SQL_STATEMENT [SAVE EXCEPTIONS];

where BOUNDS_CLAUSE is one of the following:

lower_limit..upper_limit

INDICES OF collection_name BETWEEN lower_limit..upper_limit

VALUES OF collection_name

The FORALL statement has an implicitly defined loop counter variable associated with it. The values of this loop counter variable and the number of loop iterations are controlled by the BOUNDS_CLAUSE, which has three forms. The first form specifies lower and upper limits for the loop counter; this syntax is very similar to the numeric FOR loop. The second form, INDICES OF . . . references subscripts of the individual elements of a particular collection. This collection may be a nested table or an associative array that has numeric subscripts. The third form of theBOUNDS_CLAUSE, VALUES OF . . . references values of the individual elements of a particular collection that is either a nested table or an associative array.


By the Way

A collection referenced by the INDICES OF clause may be sparse. In other words, some of its elements have been deleted.



Watch Out!

When using the VALUES OF option, the following restrictions apply:

Image If the collection used in the VALUES OF clause is an associative array, it must be indexed by a PLS_INTEGER.

Image The elements of the collection used in the VALUES OF clause must be PLS_INTEGER.

Image When a collection referenced by the VALUES OF clause is empty, the FORALL statement causes an exception.


Next, the SQL_STATEMENT is a static or dynamic INSERT, UPDATE, or DELETE statement that references one or more collections. Finally, the SAVE EXCEPTION clause is optional; it allows the FORALL statement to continue even when SQL_STATEMENT causes an exception.

The following example illustrates how the FORALL statement may be used. This example, as well as other examples in this chapter, use a TEST table created specifically for this purpose. The rows from the TEST table can be easily inserted, updated, or deleted without affecting theSTUDENT schema or violating any integrity constraints.

For Example ch18_1a.sql

CREATE TABLE test
(row_num NUMBER
,row_text VARCHAR2(10));

DECLARE
-- Define collection types and variables
TYPE row_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE row_text_type IS TABLE OF VARCHAR2(10) INDEX BY PLS_INTEGER;

row_num_tab row_num_type;
row_text_tab row_text_type;
v_rows NUMBER;
BEGIN
-- Populate collections
FOR i IN 1..10
LOOP
row_num_tab(i) := i;
row_text_tab(i) := 'row '||i;
END LOOP;

-- Populate TEST table
FORALL i IN 1..10
INSERT INTO test (row_num, row_text)
VALUES (row_num_tab(i), row_text_tab(i));

COMMIT;
-- Check how many rows where inserted in the TEST table
-- display it on the screen
SELECT COUNT(*)
INTO v_rows
FROM TEST;

DBMS_OUTPUT.PUT_LINE ('There are '||v_rows||' rows in the TEST table');
END;

As mentioned earlier, when SQL statements are used with FORALL statements, they reference collection elements. Thus, in this script, you define two collection types, row_num_type and row_text_type, as associative arrays. In addition, two collections, row_num_tab androw_text_tab, are populated via the numeric FOR loop. Next, you populate the TEST table with the data from these two collections.

When run, this example produces the following output:

There are 10 rows in the TEST table

The next example demonstrates the performance gain realized through use of the FORALL statement. This script compares the execution times of the INSERT statements issued against the TEST table. The first 100 INSERT statements are enclosed by the numeric FOR loop and the second 100 INSERT statements are enclosed by the FORALL statement.

For Example ch18_2a.sql

TRUNCATE TABLE test;

DECLARE
-- Define collection types and variables
TYPE row_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE row_text_type IS TABLE OF VARCHAR2(10) INDEX BY PLS_INTEGER;

row_num_tab row_num_type;
row_text_tab row_text_type;

v_start_time INTEGER;
v_end_time INTEGER;
BEGIN
-- Populate collections
FOR i IN 1..100
LOOP
row_num_tab(i) := i;
row_text_tab(i) := 'row '||i;
END LOOP;

-- Record start time
v_start_time := DBMS_UTILITY.GET_TIME;

-- Insert first 100 rows
FOR i IN 1..100
LOOP
INSERT INTO test (row_num, row_text)
VALUES (row_num_tab(i), row_text_tab(i));
END LOOP;

-- Record end time
v_end_time := DBMS_UTILITY.GET_TIME;

-- Calculate and display elapsed time
DBMS_OUTPUT.PUT_LINE ('Duration of the FOR LOOP: '||
(v_end_time - v_start_time));

-- Record start time
v_start_time := DBMS_UTILITY.GET_TIME;

-- Insert second 100 rows
FORALL i IN 1..100
INSERT INTO test (row_num, row_text)
VALUES (row_num_tab(i), row_text_tab(i));

-- Record end time
v_end_time := DBMS_UTILITY.GET_TIME;

-- Calculate and display elapsed time
DBMS_OUTPUT.PUT_LINE ('Duration of the FORALL statement: '||
(v_end_time – v_start_time));

COMMIT;
END;

To calculate the execution times for the numeric FOR loop and the FORALL statement, the script employs the GET_TIME function defined in the DBMS_UTILITY package that is owned by the Oracle user SYS. The GET_TIME function returns the current time in 100ths of a second. Here is the output produced by the preceding example:

Duration of the FOR LOOP: 1
Duration of the FORALL statement: 0

SAVE EXCEPTIONS Option

The SAVE EXCEPTIONS option enables the FORALL statement to continue even when a corresponding SQL statement causes an exception. These exceptions are stored in the cursor attribute called SQL%BULK_EXCEPTIONS. The SQL%BULK_EXCEPTIONS attribute is a collection of records in which each record consists of two fields, ERROR_INDEX and ERROR_CODE. The ERROR_INDEX field stores the number of the iteration of the FORALL statement during which an exception was encountered, and the ERROR_CODE stores the Oracle error code corresponding to the raised exception.

The number of exceptions that occurred during the execution of the FORALL statement can be retrieved via SQL%BULK_EXCEPTIONS.COUNT. Although the individual error messages are not saved, they can be looked up via the SQLERRM function.

The following example uses a FORALL statement with the SAVE EXCEPTIONS option.

For Example ch18_3a.sql

TRUNCATE TABLE TEST;

DECLARE
-- Define collection types and variables
TYPE row_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE row_text_type IS TABLE OF VARCHAR2(11) INDEX BY PLS_INTEGER;

row_num_tab row_num_type;
row_text_tab row_text_type;

-- Define user-defined exception and associated Oracle
-- error number with it
errors EXCEPTION;
PRAGMA EXCEPTION_INIT(errors, -24381);

v_rows NUMBER;
BEGIN
-- Populate collections
FOR i IN 1..10
LOOP
row_num_tab(i) := i;
row_text_tab(i) := 'row '||i;
END LOOP;

-- Modify 1, 5, and 7 elements of the V_ROW_TEXT collection
-- These rows will cause exceptions in the FORALL statement
row_text_tab(1) := RPAD(row_text_tab(1), 11, ' ');
row_text_tab(5) := RPAD(row_text_tab(5), 11, ' ');
row_text_tab(7) := RPAD(row_text_tab(7), 11, ' ');

-- Populate TEST table
FORALL i IN 1..10 SAVE EXCEPTIONS
INSERT INTO test (row_num, row_text)
VALUES (row_num_tab(i), row_text_tab(i));
COMMIT;

EXCEPTION
WHEN errors
THEN
-- Display total number of records inserted in the TEST table
SELECT count(*)
INTO v_rows
FROM test;
DBMS_OUTPUT.PUT_LINE ('There are '||v_rows||' records in the TEST table');

-- Display total number of exceptions encountered
DBMS_OUTPUT.PUT_LINE ('There were '||SQL%BULK_EXCEPTIONS.COUNT||' exceptions');

-- Display detailed exception information
FOR i in 1.. SQL%BULK_EXCEPTIONS.COUNT LOOP
DBMS_OUTPUT.PUT_LINE ('Record '||
SQL%BULK_EXCEPTIONS(i).error_index||' caused error '||i||': '||
SQL%BULK_EXCEPTIONS(i).error_code||' '||
SQLERRM(-SQL%BULK_EXCEPTIONS(i).error_code));
END LOOP;
END;

This example declares a user-defined exception and associates it with the ORA-24381 exception. This exception occurs when errors are encountered in an array DML statement—in this case, the INSERT statement that uses collection elements.

In the execution section of the script, the first, fifth, and seventh elements of the row_text_tab collection are expanded to store 11 characters instead of 10, thereby causing exceptions in the INSERT statement applied against the TEST table. Note the presence of theSAVE EXCEPTION clause in the FORALL statement. As mentioned earlier, the SAVE EXCEPTION clause allows the FORALL statement to execute to completion.

The exception-handling section checks how many records were added to the TEST table and how many exception records are present in the SQL%BULK_EXCEPTIONS collection. The latter task is accomplished by employing the COUNT method. In addition, this section of the script displays detailed exception information, such as the record number that caused an exception and the error message associated with this exception.

To display the number of the record that caused an exception, the error_index field is referenced in the DBMS_OUTPUT.PUT_LINE statement as follows:

SQL%BULK_EXCEPTIONS(i).error_index

To display the error message itself, the error_code field is passed as an input parameter to the SQLERRM function. Note that when the error_code is passed to the SQLERRM function, it is prefixed by a minus sign.

SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE)

When run, this script produces the following output:

There are 7 records in the TEST table
There were 3 exceptions
Record 1 caused error 1: 12899 ORA-12899: value too large for column (actual: , maximum: )
Record 5 caused error 2: 12899 ORA-12899: value too large for column (actual: , maximum: )
Record 7 caused error 3: 12899 ORA-12899: value too large for column (actual: , maximum: )

As mentioned previously, adding the SAVE EXCEPTIONS clause to the FORALL statement enables this statement to execute to completion. As a result, the INSERT statement was able to add seven records to the TEST table successfully.

INDICES OF Option

As stated previously, the INDICES OF option enables you to loop through a sparse collection. Recall that such collection may be a nested table or an associative array. The use of the INDICES OF option is illustrated in the following example.

For Example ch18_4a.sql

TRUNCATE TABLE TEST;

DECLARE
-- Define collection types and variables
TYPE row_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE row_text_type IS TABLE OF VARCHAR2(10) INDEX BY PLS_INTEGER;

row_num_tab row_num_type;
row_text_tab row_text_type;

v_rows NUMBER;
BEGIN
-- Populate collections
FOR i IN 1..10
LOOP
row_num_tab(i) := i;
row_text_tab(i) := 'row '||i;
END LOOP;

-- Delete 1, 5, and 7 elements of collections
row_num_tab.DELETE(1); row_text_tab.DELETE(1);
row_num_tab.DELETE(5); row_text_tab.DELETE(5);
row_num_tab.DELETE(7); row_text_tab.DELETE(7);

-- Populate TEST table
FORALL i IN INDICES OF row_num_tab
INSERT INTO test (row_num, row_text)
VALUES (row_num_tab(i), row_text_tab(i));
COMMIT;

SELECT COUNT(*)
INTO v_rows
FROM test;

DBMS_OUTPUT.PUT_LINE ('There are '||v_rows||' rows in the TEST table');
END;

To make the associative arrays sparse, the first, fifth, and seventh elements are deleted from both collections. As a result, the FORALL statement iterates seven times, and seven rows are added to the TEST table. This outcome is illustrated by the following output:

There are 7 rows in the TEST table

VALUES OF Option

The VALUES OF option specifies that the values of the loop counter in the FORALL statement are based on the values of the elements of the specified collection. Essentially, this collection is a group of indices that the FORALL statement can loop through. Furthermore, these indices do not need to be unique and can be listed in arbitrary order. The following example demonstrates the use of the VALUES OF option.

For Example ch18_5a.sql

CREATE TABLE TEST_EXC
(row_num NUMBER
,row_text VARCHAR2(50));

TRUNCATE TABLE TEST;

DECLARE
-- Define collection types and variables
TYPE row_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE row_text_type IS TABLE OF VARCHAR2(11) INDEX BY PLS_INTEGER;
TYPE exc_ind_type IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;

row_num_tab row_num_type;
row_text_tab row_text_type;
exc_ind_tab exc_ind_type;

-- Define user-defined exception and associated Oracle
-- error number with it
errors EXCEPTION;
PRAGMA EXCEPTION_INIT(errors, -24381);

BEGIN
-- Populate collections
FOR i IN 1..10
LOOP
row_num_tab(i) := i;
row_text_tab(i) := 'row '||i;
END LOOP;

-- Modify 1, 5, and 7 elements of the ROW_TEXT_TAB collection
-- These rows will cause exceptions in the FORALL statement
row_text_tab(1) := RPAD(row_text_tab(1), 11, ' ');
row_text_tab(5) := RPAD(row_text_tab(5), 11, ' ');
row_text_tab(7) := RPAD(row_text_tab(7), 11, ' ');

-- Populate TEST table
FORALL i IN 1..10 SAVE EXCEPTIONS
INSERT INTO test (row_num, row_text)
VALUES (row_num_tab(i), row_text_tab(i));
COMMIT;

EXCEPTION
WHEN errors
THEN
-- Populate EXC_IND_TAB collection to be used in the VALUES OF
-- clause
FOR i in 1..SQL%BULK_EXCEPTIONS.COUNT
LOOP
exc_ind_tab(i) := SQL%BULK_EXCEPTIONS(i).error_index;
END LOOP;
-- Insert records that caused exceptions in the TEST_EXC table
FORALL i in VALUES OF exc_ind_tab
INSERT INTO test_exc (row_num, row_text)
VALUES (row_num_tab(i), row_text_tab(i));
COMMIT;
END;

This script employs the TEST_EXC table, which has the same structure as the TEST table but expanded data type sizes. The newly created table is used to store records that cause exceptions when they are inserted in the TEST table. Next, the new collection data type, exc_ind_type, is defined as an associative array of PLS_INTEGERS. Finally, the new collection variable, exc_ind_tab, is defined based on the exc_ind_type. This new collection is referenced by the VALUES OF clause in the exception-handling section of the script.

To cause exceptions in the FORALL statement, the first, fifth, and seventh elements of the row_text_tab associative array are modified to contain 11 characters instead of 10. Then, in the exception-handling section of the script, the exc_ind_tab collection is populated with index values of the rows that caused the exceptions. In this example, these index values are 1, 5, and 7, and they are stored in the error_index field of the SQL%BULK_EXCEPTION collection. Once the exc_ind_tab is populated, it is used to iterate through the row_num_tab androw_test_tab collections again and insert erroneous records in the TEST_EXC table.

When this script is executed, the TEST and TEST_EXC tables contain the following records:

select *
from test;

ROW_NUM ROW_TEXT
---------- ------------------------------------------------
2 row 2
3 row 3
4 row 4
6 row 6
8 row 8
9 row 9
10 row 10



select *
from test_exc;

ROW_NUM ROW_TEXT
---------- --------------------------------------------------
1 row 1
5 row 5
7 row 7

Lab 18.2: The BULK COLLECT Clause


After this lab, you will be able to

Image Use the BULK COLLECT Clause


The BULK COLLECT clause fetches the batches of results and brings them back from the SQL engine to the PL/SQL engine. For example, consider a cursor against the STUDENT table that returns the student’s ID, first name, and last name. Once this cursor is opened, the rows are fetched one by one until all rows have been processed. Then the cursor is closed. These steps are illustrated in the following example.

For Example ch18_6a.sql

DECLARE
CURSOR student_cur IS
SELECT student_id, first_name, last_name
FROM student;
BEGIN
FOR rec IN student_cur
LOOP
DBMS_OUTPUT.PUT_LINE ('student_id: '||rec.student_id);
DBMS_OUTPUT.PUT_LINE ('first_name: '||rec.first_name);
DBMS_OUTPUT.PUT_LINE ('last_name: '||rec.last_name);
END LOOP;
END;

Recall that the cursor FOR loop opens and closes the cursor and fetches cursor records implicitly.

The same task of fetching records from the STUDENT table can be accomplished by employing the BULK COLLECT clause. The difference here is that the BULK COLLECT clause will fetch all rows from the STUDENT table at once. Because BULK COLLECT fetches multiple rows, these rows are stored in collection variables.

In the following modified version of the preceding example, cursor processing is replaced by the BULK COLLECT clause.

For Example ch18_6b.sql

DECLARE
-- Define collection type and variables to be used by the
-- BULK COLLECT clause
TYPE student_id_type IS TABLE OF student.student_id%TYPE;
TYPE first_name_type IS TABLE OF student.first_name%TYPE;
TYPE last_name_type IS TABLE OF student.last_name%TYPE;
student_id_tab student_id_type;
first_name_tab first_name_type;
last_name_tab last_name_type;

BEGIN
-- Fetch all student data at once via BULK COLLECT clause
SELECT student_id, first_name, last_name
BULK COLLECT INTO student_id_tab, first_name_tab, last_name_tab
FROM student;

FOR i IN student_id_tab.FIRST..student_id_tab.LAST
LOOP
DBMS_OUTPUT.PUT_LINE ('student_id: '||student_id_tab(i));
DBMS_OUTPUT.PUT_LINE ('first_name: '||first_name_tab(i));
DBMS_OUTPUT.PUT_LINE ('last_name: '||last_name_tab(i));
END LOOP;
END;

This script declares three nested table types and variables. These variables are used to store data returned by the SELECT statement with the BULK COLLECT clause. Because this version of the script is using the BULK COLLECT clause, there is no need to declare and process a cursor.


Did You Know?

When nested tables are populated via SELECT with the BULK COLLECT clause, they are initialized and extended automatically. Recall that a nested table must usually be initialized prior to its use by calling a constructor function that has the same name as its nested table type. Once it has been initialized, the nested table must be extended via the EXTEND method before the next value can be assigned to it.


To display the data that has been selected into the individual collections, the script loops through them via the numeric FOR loop. Notice that the lower and upper limits for the loop counter are specified via the FIRST and LAST methods.


Did You Know?

The BULK COLLECT clause is similar to a cursor loop in that it does not raise a NO_DATA_FOUND exception when the SELECT statement does not return any records. As a result, it is considered good practice to check whether the resulting collection contains any data.


Because the BULK COLLECT clause does not restrict the size of a collection and extends it automatically, it is also a good idea to limit the result set when a SELECT statement returns large amounts of data. This can be achieved by using BULK COLLECT with a cursor SELECT statement and by adding the LIMIT option.

For Example ch18_6c.sql

DECLARE
CURSOR student_cur IS
SELECT student_id, first_name, last_name
FROM student;
-- Define collection type and variables to be used by the
-- BULK COLLECT clause
TYPE student_id_type IS TABLE OF student.student_id%TYPE;
TYPE first_name_type IS TABLE OF student.first_name%TYPE;
TYPE last_name_type IS TABLE OF student.last_name%TYPE;

student_id_tab student_id_type;
first_name_tab first_name_type;
last_name_tab last_name_type;

-- Define variable to be used by the LIMIT clause
v_limit PLS_INTEGER := 50;

BEGIN
OPEN student_cur;
LOOP
-- Fetch 50 rows at once
FETCH student_cur
BULK COLLECT INTO student_id_tab, first_name_tab, last_name_tab
LIMIT v_limit;

EXIT WHEN student_id_tab.COUNT = 0;

FOR i IN student_id_tab.FIRST..student_id_tab.LAST
LOOP
DBMS_OUTPUT.PUT_LINE ('student_id: '||student_id_tab(i));
DBMS_OUTPUT.PUT_LINE ('first_name: '||first_name_tab(i));
DBMS_OUTPUT.PUT_LINE ('last_name: '||last_name_tab(i));
END LOOP;
END LOOP;
CLOSE student_cur;
END;

This version of the script employs the BULK COLLECT clause with the LIMIT option to fetch 50 rows from the STUDENT table at once. In other words, each collection will contain at most 50 records. To accomplish this task, the BULK COLLECT clause is used in conjunction with the cursor loop. In this case, the exit condition of the loop is based on the number of records in the collection rather than the student_cur%NOTFOUND attribute.

Note how the numeric FOR loop that displays information on the screen has been moved inside the cursor loop. This is done because every new batch of 50 records fetched by the BULK COLLECT clause will replace the previous batch of 50 records fetched in the previous iteration.

So far, you have seen examples of the BULK COLLECT clause fetching data into collections where the underlying elements are simple data types such as NUMBER or VARCHAR2. However, the BULK COLLECT clause can also be used to fetch data into collections of records or objects. Collections of objects are discussed in Chapter 23. In the following modified version of the previous example, student data is fetched into a collection of user-defined records.

For Example ch18_6d.sql

DECLARE
CURSOR student_cur IS
SELECT student_id, first_name, last_name
FROM student;

-- Define record type
TYPE student_rec IS RECORD
(student_id student.student_id%TYPE,
first_name student.first_name%TYPE,
last_name student.last_name%TYPE);

-- Define collection type
TYPE student_type IS TABLE OF student_rec;

-- Define collection variable
student_tab student_type;

-- Define variable to be used by the LIMIT clause
v_limit PLS_INTEGER := 50;

BEGIN
OPEN student_cur;
LOOP
-- Fetch 50 rows at once
FETCH student_cur BULK COLLECT INTO student_tab LIMIT v_limit;

EXIT WHEN student_tab.COUNT = 0;

FOR i IN student_tab.FIRST..student_tab.LAST
LOOP
DBMS_OUTPUT.PUT_LINE ('student_id: '||student_tab(i).student_id);
DBMS_OUTPUT.PUT_LINE ('first_name: '||student_tab(i).first_name);
DBMS_OUTPUT.PUT_LINE ('last_name: '||student_tab(i).last_name);
END LOOP;
END LOOP;
CLOSE student_cur;
END;

In this version of the script, the result set returned by the cursor is fetched into collection of user-defined records, student_tab. As a consequence, the FETCH statement with the BULK COLLECTION option does not need to reference individual record elements.

All versions of this example produce the same output, a portion of which is shown here:

student_id: 230
first_name: George
last_name : Kocka
student_id: 232
first_name: Janet
last_name : Jung
student_id: 233
first_name: Kathleen
last_name : Mulroy
student_id: 234
first_name: Joel
last_name : Brendler
...

So far you have seen how to use the BULK COLLECT clause with the SELECT statement. However, oftentimes BULK COLLECT is used with INSERT, UPDATE, and DELETE statements. In this case, the BULK COLLECT clause may be used in conjunction with the RETURNINGclause, as shown in the following example.

For Example ch18_7a.sql

DECLARE
-- Define collection types and variables
TYPE row_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE row_text_type IS TABLE OF VARCHAR2(10) INDEX BY PLS_INTEGER;

row_num_tab row_num_type;
row_text_tab row_text_type;

BEGIN
DELETE FROM test
RETURNING row_num, row_text
BULK COLLECT INTO row_num_tab, row_text_tab;

DBMS_OUTPUT.PUT_LINE ('Deleted '||SQL%ROWCOUNT||' rows:');

FOR i IN row_num_tab.FIRST..row_num_tab.LAST
LOOP
DBMS_OUTPUT.PUT_LINE
('row_num = '||row_num_tab(i)||' row_text = ' ||row_text_tab(i));
END LOOP;

COMMIT;
END;

This script deletes records from the TEST table created and populated in Lab 18.1. The DELETE statement returns the ROW_NUM and ROW_TEXT values via the RETURNING clause. These values are then fetched by the BULK COLLECT clause into two collections, row_num_tab androw_text_tab. Next, to display the data that has been fetched into the individual collections, they are looped through via the numeric FOR loop.

When run, this script produces the following output:

Deleted 7 rows:
row_num = 2 row_text = row 2
row_num = 3 row_text = row 3
row_num = 4 row_text = row 4
row_num = 6 row_text = row 6
row_num = 8 row_text = row 8
row_num = 9 row_text = row 9
row_num = 10 row_text = row 10

As mentioned previously, the BULK COLLECT clause is similar to the cursor loop in that it does not generate a NO_DATA_FOUND exception when no rows are returned by the SELECT statement. This is illustrated by the following example.

For Example ch18_8a.sql

DECLARE
-- Define collection types and variables
TYPE row_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE row_text_type IS TABLE OF VARCHAR2(10) INDEX BY PLS_INTEGER;

row_num_tab row_num_type;
row_text_tab row_text_type;

BEGIN
SELECT row_num, row_text
BULK COLLECT INTO row_num_tab, row_text_tab
FROM test;

FOR i IN row_num_tab.FIRST..row_num_tab.LAST
LOOP
DBMS_OUTPUT.PUT_LINE
('row_num = '||row_num_tab(i)||' row_text = ' ||row_text_tab(i));
END LOOP;
END;

In this example, the data is selected from the TEST table and populated into two collections, row_num_tab and row_text_tab. This is accomplished via the BULK COLLECT clause. Next, the collection data is displayed on the screen through the numeric FOR loop, with the values returned by the row_num_tab.FIRST and row_num_tab.LAST methods being used as lower and upper bounds of the loop.

At first glance, this example seems very similar to the example ch18_6b.sql that appeared earlier in this lab, as it follows the same steps. First, collection types and variables are declared. Second, the data is selected in the collection variables. Third, the data in the collection variables is displayed on the screen. However, when it is run, this script raises the following exception:

ORA-06502: PL/SQL: numeric or value error
ORA-06512: at line 14

This error is caused by the

FOR i IN row_num_tab.FIRST..row_num_tab.LAST

statement, as the collection variables do not have any data in them. This situation occurs because the data from the TEST table was deleted in the ch18_7a.sql example. To remedy this problem, the example should be modified as follows (affected statements are shown in bold):

For Example ch18_8b.sql

DECLARE
-- Define collection types and variables
TYPE row_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE row_text_type IS TABLE OF VARCHAR2(10) INDEX BY PLS_INTEGER;
row_num_tab row_num_type;
row_text_tab row_text_type;

BEGIN
SELECT row_num, row_text
BULK COLLECT INTO row_num_tab, row_text_tab
FROM test;

IF row_num_tab.COUNT != 0
THEN
FOR i IN row_num_tab.FIRST..row_num_tab.LAST
LOOP
DBMS_OUTPUT.PUT_LINE
('row_num = '||row_num_tab(i)||' row_text = ' ||row_text_tab(i));
END LOOP;
ELSE
DBMS_OUTPUT.PUT_LINE ('row_num_tab.COUNT = '||row_num_tab.COUNT);
DBMS_OUTPUT.PUT_LINE ('row_text_tab.COUNT = '||row_text_tab.COUNT);
END IF;
END;

When run, this version of the script does not cause any exception. The IF statement evaluates to FALSE when the COUNT method returns 0, as illustrated by the output:

row_num_tab.COUNT = 0
row_text_tab.COUNT = 0

Throughout this chapter, you have seen how to use the FORALL statement and BULK COLLECT clause. Now we will consider an example that combines both techniques. This example uses the MY_ZIPCODE table, which is created based on the ZIPCODE table. Note that the CREATE TABLE statement creates an empty table because the criteria specified in the WHERE clause do not return any records.

For Example ch18_9a.sql

CREATE TABLE my_zipcode AS
SELECT *
FROM zipcode
WHERE 1 = 2;

DECLARE
-- Declare collection types
TYPE string_type IS TABLE OF VARCHAR2(100) INDEX BY PLS_INTEGER;
TYPE date_type IS TABLE OF DATE INDEX BY PLS_INTEGER;

-- Declare collection variables to be used by the FORALL statement
zip_tab string_type;
city_tab string_type;
state_tab string_type;
cr_by_tab string_type;
cr_date_tab date_type;
mod_by_tab string_type;
mod_date_tab date_type;

v_rows INTEGER := 0;
BEGIN
-- Populate individual collections
SELECT *
BULK COLLECT INTO zip_tab, city_tab, state_tab, cr_by_tab,
cr_date_tab, mod_by_tab, mod_date_tab
FROM zipcode
WHERE state = 'CT';

-- Populate MY_ZIPCODE table
FORALL i in 1..zip_tab.COUNT
INSERT INTO my_zipcode
(zip, city, state, created_by, created_date, modified_by,
modified_date)
VALUES
(zip_tab(i), city_tab(i), state_tab(i), cr_by_tab(i),
cr_date_tab(i), mod_by_tab(i), mod_date_tab(i));
COMMIT;

-- Check how many records were added to MY_ZIPCODE table
SELECT COUNT(*)
INTO v_rows
FROM my_zipcode;

DBMS_OUTPUT.PUT_LINE (v_rows||' records were added to MY_ZIPCODE table');
END;

This script populates the MY_ZIPCODE table with the records selected from the ZIPCODE table. To enable use of the BULK COLLECT and FORALL statements, it employs seven collections. Note that there are only two collection types associated with these seven collections. This is because the individual collections store only two data types, VARCHAR2 and DATE. When run, this example produces the following output:

19 records were added to MY_ZIPCODE table

Next, consider another example where the FORALL statement and BULK COLLECT clause are used together with the DELETE statement. In this example, the records from the MY_ZIPCODE table are deleted for a few ZIP codes, and the corresponding city names along with the deleted ZIP codes are stored in the city_tab and zip_tab collections, respectively.

For Example ch18_10a.sql

DECLARE
-- Declare collection types
TYPE string_type IS TABLE OF VARCHAR2(100);

-- Declare collection variables to be used by the FORALL statement
-- and BULK COLLECT clause
zip_codes string_type := string_type ('06401', '06455', '06483', '06520', '06605');
zip_tab string_type;
city_tab string_type;

v_rows INTEGER := 0;
BEGIN
-- Delete some records from MY_ZIPCODE table
FORALL i in zip_codes.FIRST..zip_codes.LAST
DELETE FROM my_zipcode
WHERE zip = zip_codes(i)
RETURNING zip, city
BULK COLLECT INTO zip_tab, city_tab;
COMMIT;

DBMS_OUTPUT.PUT_LINE ('The following records were deleted from MY_ZIPCODE table:');
FOR i in zip_tab.FIRST..zip_tab.LAST
LOOP
DBMS_OUTPUT.PUT_LINE ('Zip code '||zip_tab(i)||', city '||city_tab(i));
END LOOP;
END;

In this script, the FORALL statement runs the DELETE statement for a given list of ZIP code values stored in the zip_codes collection. Also, the DELETE statement contains the RETURNING clause with the BULK COLLECT clause, which in turn stores ZIP codes and city names in thezip_tab and city_tab collections, respectively. Finally, the numeric FOR loop is used to display the data stored in the zip_tab and city_tab collections, as illustrated by the output from the script:

The following records were deleted from MY_ZIPCODE table:
Zip code 06401, city Ansonia
Zip code 06455, city Middlefield
Zip code 06483, city Oxford
Zip code 06520, city New Haven
Zip code 06605, city Bridgeport

Lab 18.3: Binding Collections in SQL Statements


After this lab, you will be able to

Image Bind Collections When Using EXECUTE IMMEDIATE Statements

Image Bind Collections When Using OPEN-FOR, FETCH, and CLOSE Statements


As mentioned previously, the ability to bind collection data types when employing dynamic SQL has been added in Oracle 12c. Recall that dynamic SQL was covered in Chapter 17, where you learned how to use the EXECUTE IMMEDIATE statement and OPEN-FOR, FETCH, and CLOSEstatements.

Binding Collections with EXECUTE IMMEDIATE Statements

In Chapter 17, you saw numerous examples of the EXECUTE IMMEDIATE statement. All of these examples have one thing in common: The data types of the bind variables are known SQL types. In other words, these data types are all supported by SQL, such as NUMBER and VARCHAR2. In Oracle 12c, it is possible to use bind variables based on the collection and record types, albeit with one restriction applied: The collection or record data type must be declared in the package specification.

Consider the package called TEST_ADM_PKG, shown in Listing 18.3. This package contains definitions of two collection types and three procedures that insert, update, and delete records from the TEST table. (Procedures, functions, and packages are covered in detail in Chapters 19through 21.)

Listing 18.3 TEST_ADM_PKG Package with Collection Types

CREATE OR REPLACE PACKAGE test_adm_pkg
AS
-- Define collection types
TYPE row_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE row_text_type IS TABLE OF VARCHAR2(10) INDEX BY PLS_INTEGER;

-- Define procedures
PROCEDURE populate_test (row_num_tab ROW_NUM_TYPE
,row_num_type ROW_TEXT_TYPE);

PROCEDURE update_test (row_num_tab ROW_NUM_TYPE
,row_num_type ROW_TEXT_TYPE);

PROCEDURE delete_test (row_num_tab ROW_NUM_TYPE);
END test_adm_pkg;
/

CREATE OR REPLACE PACKAGE BODY test_adm_pkg
AS
PROCEDURE populate_test (row_num_tab ROW_NUM_TYPE
,row_num_type ROW_TEXT_TYPE)
IS
BEGIN
FORALL i IN 1..10
INSERT INTO test (row_num, row_text)
VALUES (row_num_tab(i), row_num_type(i));
END populate_test;

PROCEDURE update_test (row_num_tab ROW_NUM_TYPE
,row_num_type ROW_TEXT_TYPE)
IS
BEGIN
FORALL i IN 1..10
UPDATE test
SET row_text = row_num_type(i)
WHERE row_num = row_num_tab(i);
END update_test;

PROCEDURE delete_test (row_num_tab ROW_NUM_TYPE)
IS
BEGIN
FORALL i IN 1..10
DELETE from test
WHERE row_num = row_num_tab(i);
END delete_test;

END test_adm_pkg;
/

This package has both a package specification and a package body. The package specification contains declarations of two associative array types (ROW_NUM_TYPE and ROW_TEXT_TYPE) and three procedures (POPULATE_TEST, UPDATE_TEST, and DELETE_TEST). Each procedure has parameters ROW_NUM_TAB and ROW_TEXT_TAB that are based on the collection types defined in this package. The package body contains the code for the procedures declared in the package specification.

The following example uses this newly created package. References to the package objects are highlighted in bold.

For Example ch18_11a.sql

DECLARE
row_num_tab test_adm_pkg.row_num_type;
row_text_tab test_adm_pkg.row_text_type;

v_rows NUMBER;

BEGIN
-- Populate collections
FOR i IN 1..10
LOOP
row_num_tab(i) := i;
row_text_tab(i) := 'row '||i;
END LOOP;

-- Delete previously added data from the TEST table
test_adm_pkg.delete_test (row_num_tab);

-- Populate TEST table
test_adm_pkg.populate_test (row_num_tab, row_text_tab);
COMMIT;

-- Check how many rows where inserted in the TEST table
-- and display this number on the screen
SELECT COUNT(*)
INTO v_rows
FROM TEST;

DBMS_OUTPUT.PUT_LINE ('There are '||v_rows||' rows in the TEST table');
END;

This example is very similar to the example ch18_1a.sql used in Lab 18.1. It populates the TEST table, checks how many records were added to the TEST table, and displays this information on the screen. The main difference is that it references the TEST_ADM_PKG package when declaring two collection variables and it calls the DELETE_TEST and POPULATE_TEST procedures to delete previously added records to the TEST table and repopulate the table with the new data. Note that all of the references to the packaged objects are prefixed by the package name.

When run, this example produces the following output:

There are 10 rows in the TEST table

Now consider a modified version of this example where calls to the procedures DELETE_TEST and POPULATE_TEST are embedded in the dynamic SQL. All changes are shown in bold.

For Example ch18_11b.sql

DECLARE
row_num_tab test_adm_pkg.row_num_type;
row_text_tab test_adm_pkg.row_text_type;

v_dyn_sql VARCHAR2(1000);
v_rows NUMBER;

BEGIN
-- Populate collections
FOR i IN 1..10
LOOP
row_num_tab(i) := i;
row_text_tab(i) := 'row '||i;
END LOOP;

-- Delete previously added data from the TEST table
v_dyn_sql := 'begin test_adm_pkg.delete_test (:row_num_tab); end;';
EXECUTE IMMEDIATE v_dyn_sql USING row_num_tab;

-- Populate TEST table
v_dyn_sql := 'begin test_adm_pkg.populate_test (:row_num_tab, :row_text_tab); end;';
EXECUTE IMMEDIATE v_dyn_sql USING row_num_tab, row_text_tab;
COMMIT;

-- Check how many rows where inserted in the TEST table
-- display it on the screen
SELECT COUNT(*)
INTO v_rows
FROM TEST;

DBMS_OUTPUT.PUT_LINE ('There are '||v_rows||' rows in the TEST table');
END;

This version of the script declares a new variable, v_dyn_sql, that is used to store dynamic SQL statement. Next, the calls to the DELETE_TEST and POPULATE_TEST procedures are replaced by the dynamic SQL statements that are executed by the EXECUTE IMMEDIATEstatement.

Notice the syntax of the dynamic SQL statements. In the original example, the packaged procedures are invoked in this way:

-- Delete previously added data from the TEST table
test_adm_pkg.delete_test (row_num_tab);

-- Populate TEST table
test_adm_pkg.populate_test (row_num_tab, row_text_tab);

In the modified version of the example, calls to these procedures are placed between the BEGIN and END statements:

-- Delete previously added data from the TEST table
v_dyn_sql := 'begin test_adm_pkg.delete_test (:row_num_tab); end;';
EXECUTE IMMEDIATE v_dyn_sql USING row_num_tab;

-- Populate TEST table
v_dyn_sql := 'begin test_adm_pkg.populate_test (:row_num_tab, :row_text_tab); end;';
EXECUTE IMMEDIATE v_dyn_sql USING row_num_tab, row_text_tab;

This approach is used because each dynamic SQL statement is executed as an anonymous PL/SQL block and, therefore, requires BEGIN and END statements. If these BEGIN and END statements are omitted from the dynamic SQL, the script will not execute successfully. This is illustrated by the following example (affected statements are shown in bold):

For Example ch18_11c.sql

DECLARE
row_num_tab test_adm_pkg.row_num_type;
row_text_tab test_adm_pkg.row_text_type;

v_dyn_sql VARCHAR2(1000);
v_rows NUMBER;

BEGIN
-- Populate collections
FOR i IN 1..10
LOOP
row_num_tab(i) := i;
row_text_tab(i) := 'row '||i;
END LOOP;

-- Delete previously added data from the TEST table
v_dyn_sql := 'test_adm_pkg.delete_test (:row_num_tab);';
EXECUTE IMMEDIATE v_dyn_sql USING row_num_tab;

-- Populate TEST table
v_dyn_sql := 'test_adm_pkg.populate_test (:row_num_tab, :row_text_tab);';
EXECUTE IMMEDIATE v_dyn_sql USING row_num_tab, row_text_tab;
COMMIT;

-- Check how many rows where inserted in the TEST table
-- display it on the screen
SELECT COUNT(*)
INTO v_rows
FROM TEST;

DBMS_OUTPUT.PUT_LINE ('There are '||v_rows||' rows in the TEST table');
END;

This version of the script produces the following error:

ORA-00900: invalid SQL statement
ORA-06512: at line 18

As mentioned previously, starting with Oracle 12c you can also use bind variables based on the record types. Similar to the collection types, the record types must be defined in the package specification. Consider the modified version of TEST_ADM_PKG shown in Listing 18.4. The package now contains a definition of a record type and a new procedure that populates a record variable from the TEST table using the newly created record type. Newly added items are shown in bold.

Listing 18.4 TEST_ADM_PKG Package with Record Type

CREATE OR REPLACE PACKAGE test_adm_pkg
AS
-- Define collection types
TYPE row_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE row_text_type IS TABLE OF VARCHAR2(10) INDEX BY PLS_INTEGER;

-- Define record type
TYPE rec_type IS RECORD
(row_num NUMBER
,row_text VARCHAR2(10));

-- Define procedures
PROCEDURE populate_test (row_num_tab ROW_NUM_TYPE
,row_num_type ROW_TEXT_TYPE);

PROCEDURE update_test (row_num_tab ROW_NUM_TYPE
,row_num_type ROW_TEXT_TYPE);

PROCEDURE delete_test (row_num_tab ROW_NUM_TYPE);

PROCEDURE populate_test_rec (row_num_val IN NUMBER
,test_rec OUT REC_TYPE);
END test_adm_pkg;
/

CREATE OR REPLACE PACKAGE BODY test_adm_pkg
AS
PROCEDURE populate_test (row_num_tab ROW_NUM_TYPE
,row_num_type ROW_TEXT_TYPE);
IS
BEGIN
FORALL i IN 1..10
INSERT INTO test (row_num, row_text)
VALUES (row_num_tab(i),row_num_type(i));
END populate_test;

PROCEDURE update_test (row_num_tab ROW_NUM_TYPE
,row_num_type ROW_TEXT_TYPE);
IS
BEGIN
FORALL i IN 1..10
UPDATE test
SET row_text = row_num_type(i)
WHERE row_num = row_num_tab(i);
END update_test;

PROCEDURE delete_test (row_num_tab ROW_NUM_TYPE)
IS
BEGIN
FORALL i IN 1..10
DELETE from test
WHERE row_num = row_num_tab(i);
END delete_test;
PROCEDURE populate_test_rec (row_num_val IN NUMBER
,test_rec OUT REC_TYPE)
IS
BEGIN
SELECT *
INTO test_rec
FROM test
WHERE row_num = row_num_val;
END populate_test_rec;

END test_adm_pkg;
/

This version of the package contains definitions of the user-defined record type, rec_type, and a new procedure, POPULATE_TEST_REC, that selects a record from the TEST table into the test_rec parameter based on the row_num value provided at run time. Note the IN and OUTparameter modes specified in the POPULATE_TEST_REC procedure header: They denote that the row_num_val parameter is used to pass a value into the procedure, and that the test_rec parameter is used to pass a value from the procedure. (Parameter modes are covered in detail inChapters 19 through 21.)

The next example uses the newly created record type and procedure to display a record from the TEST table.

For Example ch18_12a.sql

DECLARE
test_rec test_adm_pkg.rec_type;

v_dyn_sql VARCHAR2(1000);

BEGIN
-- Select record from the TEST table
v_dyn_sql := 'begin test_adm_pkg.populate_test_rec (:val, :rec); end;';
EXECUTE IMMEDIATE v_dyn_sql USING IN 10, OUT test_rec;
COMMIT;

-- Display newly selected record
DBMS_OUTPUT.PUT_LINE ('test_rec.row_num = '||test_rec.row_num);
DBMS_OUTPUT.PUT_LINE ('test_rec.row_text = '||test_rec.row_text);
END;

In this example, the USING clause in the EXECUTE IMMEDIATE statement contains parameter modes. This is done to ensure that the variables used by the EXECUTE IMMEDIATE statement have the same modes as the parameters in the procedure. When run, this example produces the following output:

test_rec.row_num = 10
test_rec.row_text = row 10

Binding Collections with OPEN-FOR, FETCH, and CLOSE Statements

Recall that the OPEN-FOR, FETCH, and CLOSE statements are used with multirow queries or cursors. This is illustrated by the example below.

For Example ch18_13a.sql

DECLARE
TYPE student_cur_typ IS REF CURSOR;

student_cur student_cur_typ;
student_rec student%ROWTYPE;

v_zip_code student.zip%TYPE := '06820';

BEGIN
OPEN student_cur
FOR 'SELECT * FROM student WHERE zip = :my_zip' USING v_zip_code;

LOOP
FETCH student_cur INTO student_rec;
EXIT WHEN student_cur%NOTFOUND;

-- Display student ID, first and last names
DBMS_OUTPUT.PUT_LINE ('student_rec.student_id = '||student_rec.student_id);
DBMS_OUTPUT.PUT_LINE ('student_rec.first_name = '||student_rec.first_name);
DBMS_OUTPUT.PUT_LINE ('student_rec.last_name = '||student_rec.last_name);
END LOOP;
CLOSE student_cur;
END;

The declaration portion of this script specifies the cursor type, student_cur_typ, defined as REF CURSOR, and a cursor variable, student_cur, based on this type. Next, it defines a record variable, student_rec, based on the STUDENT table.

The executable portion of the script associates the SELECT statement with the STUDENT table for a given ZIP code with the student_cur variable and opens it. Next, each row returned by the SELECT statement is fetched into the student_rec variable and the student’s ID, first name, and last name are displayed on the screen. Once all the records returned by the SELECT statement are fetched, the cursor terminates.

When run, this example produces the following output:

student_rec.student_id = 240
student_rec.first_name = Z.A.
student_rec.last_name = Scrittorale
student_rec.student_id = 326
student_rec.first_name = Piotr
student_rec.last_name = Padel
student_rec.student_id = 360
student_rec.first_name = Calvin
student_rec.last_name = Kiraly

Next, consider a modified version of this example where all student records for a given ZIP code are fetched at once into a collection of records. Recall that to bind a collection or a record type, the following restriction must be respected: The collection or record data type must be declared in the package specification. To comply with this rule, the STUDENT_ADM_PKG package shown in Listing 18.5 is created specifically for this purpose.

Listing 18.5 STUDENT_ADM_PKG Package with Record and Collection Types

CREATE OR REPLACE PACKAGE student_adm_pkg
AS
-- Define collection type
TYPE student_tab_type IS TABLE OF student%ROWTYPE INDEX BY PLS_INTEGER;

-- Define procedures
PROCEDURE populate_student_tab (zip_code IN VARCHAR2
,student_tab OUT student_tab_type);

PROCEDURE display_student_info (student_rec student%ROWTYPE);

END student_adm_pkg;
/

CREATE OR REPLACE PACKAGE BODY student_adm_pkg
AS
PROCEDURE populate_student_tab (zip_code IN VARCHAR2
,student_tab OUT student_tab_type)

IS
BEGIN
SELECT *
BULK COLLECT INTO student_tab
FROM student
WHERE zip = zip_code;
END populate_student_tab;

PROCEDURE display_student_info (student_rec student%ROWTYPE)
IS
BEGIN
DBMS_OUTPUT.PUT_LINE ('student_rec.zip = '||student_rec.zip);
DBMS_OUTPUT.PUT_LINE ('student_rec.student_id = '||student_rec.student_id);
DBMS_OUTPUT.PUT_LINE ('student_rec.first_name = '||student_rec.first_name);
DBMS_OUTPUT.PUT_LINE ('student_rec.last_name = '||student_rec.last_name);
END display_student_info;

END student_adm_pkg;
/

The package specification declares the associative array type, student_tab_type, where each element of the collection is a record of the student%ROWTYPE. Next, it declares the populate_student_tab procedure, which accepts the value of a ZIP code and returns a collection of records, student_tab, populated via the BULK COLLECT INTO statement. Note the parameter modes specified in the procedure declaration. The zip_code is specified as an IN parameter. Based on its value, the OUT parameter student_tab is populated via theSELECT statement from the STUDENT table.

The second procedure, display_student_info, accepts one input parameter, student_rec based on the STUDENT table and displays the student’s ZIP code, ID, first name, and last name on the screen.

The package body contains the executable code of the populate_student_tab and display_student_info procedures.

The following modified version of example ch18_13a.sql employs this newly created package. References to packages objects are highlighted in bold.

For Example ch18_13b.sql

DECLARE
TYPE student_cur_typ IS REF CURSOR;
student_cur student_cur_typ;

-- Collection and record variables
student_tab student_adm_pkg.student_tab_type;
student_rec student%ROWTYPE;

BEGIN
-- Populate collection of records
student_adm_pkg.populate_student_tab ('06820', student_tab);

OPEN student_cur
FOR 'SELECT * FROM TABLE(:my_table)' USING student_tab;

LOOP
FETCH student_cur INTO student_rec;
EXIT WHEN student_cur%NOTFOUND;

student_adm_pkg.display_student_info (student_rec);
END LOOP;
CLOSE student_cur;
END;

This version of the script declares a collection of records, student_tab, based on the collection type defined in the STUDENT_ADM_PKG package. The execution section of the script populates the student_tab collection with the records from the STUDENT table for a particular ZIP code. This is accomplished by calling the populate_student_tab procedure defined in the STUDENT_ADM_PKG package. Next, the student records are selected from the newly populated student_tab collection. Note the usage of the built-in TABLE function in the SELECTstatement.


Did You Know?

The TABLE function allows you to query a collection like a physical database table. Essentially, it accepts a collection as its input parameter and returns the appropriate result set based on the SELECT statement. Note that an input parameter can also be a REF CURSOR.


When run, this version of the script produces the following output:

student_rec.zip = 06820
student_rec.student_id = 240
student_rec.first_name = Z.A.
student_rec.last_name = Scrittorale
student_rec.zip = 06820
student_rec.student_id = 326
student_rec.first_name = Piotr
student_rec.last_name = Padel
student_rec.zip = 06820
student_rec.student_id = 360
student_rec.first_name = Calvin
student_rec.last_name = Kiraly

Summary

In this chapter, you learned how to optimize PL/SQL code with features known as bulk SQL. Fundamentally, you discovered how to batch SQL statements and their results so as to minimize the performance overhead associated with the number of context switches between the PL/SQL and SQL engines. Specifically, you learned about the FORALL statement and the BULK COLLECT clause. In addition, you learned that starting with Oracle 12c, you can employ bulk SQL and collection data types along with dynamic SQL.


By the Way

The companion website provides additional exercises and suggested answers for this chapter, with discussion related to how those answers resulted. The main purpose of these exercises is to help you test the depth of your understanding by utilizing all of the skills that you have acquired throughout this chapter.