Notice that there is no exposure here of the fact that arrays are being used. */ FOR i IN 1 empfetch.numfetched LOOP v_ename := empfetch.ename_val (i); v_empno := empfetch.empno_val (i); END LOOP; END LOOP; DBMS_OUTPUT.PUT_LINE ('dynamic = ' || TO_CHAR (DBMS_UTILITY.GET_TIME − timing)); END; / And here are the astounding results from execution of this test script: SQL> @empfetch.tst 10 static = 114 dynamic = 119 SQL> @empfetch.tst 100 static = 1141 dynamic = 1167 In other words, there was virtually no difference in performance between these two approaches. This closeness exceeded my expectations, and is more than one might expect simply from the array processing. My hat is off to the PL/SQL development team! They really have reduced the overhead of executing dynamic SQL and anonymous blocks. You will find the full implementation of the empfetch body in the empfetch.spp file; later I will show the different elements of the package and offer some observations. First, the following data structures are declared at the package level: 1. A cursor for the dynamic SQL. I open the cursor when the package is initialized and keep that cursor allocated for the duration of the session, minimizing memory requirements and maximizing performance. c PLS_INTEGER := DBMS_SQL.OPEN_CURSOR; 2. Two arrays to hold the employee IDs and the names. Notice that I must use the special table types provided by DBMS_SQL. empno_array DBMS_SQL.NUMBER_TABLE; ename_array DBMS_SQL.VARCHAR2_TABLE; 3. A global variable keeping track of the number of rows fetched. I could retrieve this value with the empno.COUNT operator, but this would be misleading if you were appending rows, and it would also entail more overhead than simply storing the value in this variable. g_num_fetched PLS_INTEGER := 0; Now we will look at the programs that make use of these data structures. First, the rows procedure, which populates the arrays: PROCEDURE rows (numrows_in IN INTEGER, where_clause_in IN VARCHAR2 := NULL, append_rows_in IN BOOLEAN := FALSE) IS v_start PLS_INTEGER := 1; [Appendix A] What's on the Companion Disk? 2.5.6 Array Processing with DBMS_SQL 131 BEGIN IF append_rows_in THEN v_start := NVL (GREATEST (empno_array.LAST, ename_array.LAST), 0) + 1; ELSE /* Clean out the tables from the last usage. */ empno_array.DELETE; ename_array.DELETE; END IF; /* Parse the query with a dynamic WHERE clause */ DBMS_SQL.PARSE (c, 'SELECT empno, ename FROM emp WHERE ' || NVL (where_clause_in, '1=1'), DBMS_SQL.NATIVE); /* Define the columns in the cursor for this query */ DBMS_SQL.DEFINE_ARRAY (c, 1, empno_array, numrows_in, v_start); DBMS_SQL.DEFINE_ARRAY (c, 2, ename_array, numrows_in, v_start); /* Execute the query and fetch the rows. */ g_num_fetched:= DBMS_SQL.EXECUTE_AND_FETCH (c); /* Move the column values into the arrays */ DBMS_SQL.COLUMN_VALUE (c, 1, empno_array); DBMS_SQL.COLUMN_VALUE (c, 2, ename_array); END; Areas of interest in this program include: • I set the starting row for the call to DEFINE_ARRAY according to the append_rows argument. If not appending, I clean out the arrays and start at row 1. If appending, I determine the highest−defined row in both of the arrays and start from the next row. • If the user does not provide a WHERE clause, I append a trivial "1=1" condition after the WHERE keyword. This is admittedly kludgy and little more than a reflection of this programmer's laziness. The alternative is to do more complex string analysis and concatenation. • Both calls to DBMS_SQL.DEFINE_ARRAY use the same number of rows and the starting row, ensuring that their contents are correlated properly. • I execute and fetch with a single line of code. When performing array processing, there is no reason to separate these two steps, unless you will be fetching N number of rows more than once to make sure you have gotten them all. That takes care of most of the work and complexity of the empfetch package. Let's finish up by looking at the functions that retrieve individual values from the arrays. These are very simple pieces of code; I will show ename_val, but empno_val is almost exactly the same: FUNCTION ename_val (row_in IN INTEGER) RETURN emp.ename%TYPE IS BEGIN IF ename_array.EXISTS (row_in) THEN RETURN ename_array (row_in); ELSE RETURN NULL; [Appendix A] What's on the Companion Disk? 2.5.6 Array Processing with DBMS_SQL 132 END IF; END; Notice that I avoid raising the NO_DATA_FOUND exception by checking whether the row exists before I try to return it. As you can see from empfetch, it doesn't necessarily take lots of code to build a solid encapsulation around internal data structures. From the performance of this package, you can also see that array processing with dynamic SQL offers some new opportunities for building an efficient programmatic interface to your data. 2.5.6.5 Using array processing in dynamic PL/SQL The first release of Oracle8 (8.0.3) contained a number of bugs relating to the use of array processing in dynamic PL/SQL. The 8.0.4 and 8.0.5 releases, however, fix many (if not all) of these problems. I offer in this section one example to demonstrate the techniques involved in order to copy the contents of one index table to another index table. You can find another example of array processing with index tables in the next section. The following testarray procedure moves the rows of one index table to another. on companion disk: plsqlarray.sp */* CREATE OR REPLACE PROCEDURE testarray IS cur INTEGER := DBMS_SQL.OPEN_CURSOR; fdbk INTEGER; mytab1 DBMS_SQL.NUMBER_TABLE; mytab2 DBMS_SQL.NUMBER_TABLE; BEGIN mytab1 (25) := 100; mytab1 (100) := 1000; mytab2 (25) := −100; mytab2 (100) := −1000; DBMS_SQL.PARSE (cur, 'BEGIN :newtab := :oldtab; END;', DBMS_SQL.NATIVE); DBMS_SQL.BIND_ARRAY (cur, 'newtab', mytab2, 25, 100); DBMS_SQL.BIND_ARRAY (cur, 'oldtab', mytab1, 25, 100); fdbk := DBMS_SQL.EXECUTE (cur); DBMS_SQL.VARIABLE_VALUE (cur, 'newtab', mytab2); DBMS_OUTPUT.PUT_LINE('mytab2(1) := ' || mytab2(1)); DBMS_OUTPUT.PUT_LINE('mytab2(2) := ' || mytab2(2)); DBMS_OUTPUT.PUT_LINE('mytab2(25) := ' || mytab2(25)); DBMS_OUTPUT.PUT_LINE('mytab2(100) := ' || mytab2(100)); DBMS_SQL.CLOSE_CURSOR (cur); END; / Notice that I must call DBMS_SQL.BIND_VARIABLE twice, once for the "IN" index table (the "old table," mytab1) and once for the "OUT" index table (the "new table," mytab2). Finally, I call DBMS_SQL.VARIABLE_VALUE to transfer the result of the assignment into my local index table. Here are the results from execution of this procedure: SQL> exec testarray mytab2(1) := −1000 mytab2(2) := 100 mytab2(25) := −100 mytab2(100) := −1000 Ah! Not quite what we might have expected. This procedure did transfer the contents of mytab1 to mytab2, [Appendix A] What's on the Companion Disk? 2.5.6 Array Processing with DBMS_SQL 133 but it filled rows sequentially in mytab2 starting from row 1 −− definitely something to keep in mind when you take advantage of this technology. You might want to DELETE from the index table before the copy. 2.5.7 Using the RETURNING Clause in Dynamic SQL One of the many enhancements in Oracle8 is the addition of the RETURNING clause to INSERTs, UPDATEs, and DELETEs. You can use the RETURNING clause to retrieve data (or expressions derived from the data) from the rows affected by the DML statement. Here is an example of an INSERT statement that returns the just−generated primary key: DECLARE mykey emp.empno%TYPE; total_comp NUMBER; BEGIN INSERT INTO emp (empno, ename, deptno) VALUES (emp_seq.NEXTVAL, 'Feuerstein', 10) RETURNING empno, sal + NVL (comm, 0) INTO mykey, total_comp; END; / You return data into scalar values when the DML has modified only one row. If the DML modifies more than one row, you can return data into index tables using DBMS_SQL. If you modify more than one row and try to return values into a scalar variable, then you will raise an exception, as follows: SQL> DECLARE mykey emp.empno%TYPE; BEGIN /* Updating all rows in the table */ UPDATE emp SET sal = 1000 RETURNING empno INTO mykey; END; / ERROR at line 1: ORA−01422: exact fetch returns more than requested number of rows If you are changing more than one row of data and employing the RETURNING clause, you must use DBMS_SQL to pass back those values into an index table. You cannot simply specify an index table in the RETURNING clause with static SQL. If you try this, you will receive the following error: PLS−00385: type mismatch found at '<index table>' in SELECT INTO statement The rest of this section demonstrates the use of the RETURNING clause for both single− and multiple−row operations from within DBMS_SQL. 2.5.7.1 RETURNING from a single−row insert Suppose that you want to insert a row into a table with the primary key generated from a sequence, and then return that sequence value to the calling program. In this example, I will create a table to hold notes (along with a sequence and an index, the latter to show the behavior when an INSERT fails): /*Filename on companion disk: insret.sp */* CREATE TABLE note (note_id NUMBER, text VARCHAR2(500)); CREATE UNIQUE INDEX note_text_ind ON note (text); CREATE SEQUENCE note_seq; I then build a procedure around the INSERT statement for this table: /* Filename on companion disk: insret.sp */* [Appendix A] What's on the Companion Disk? 2.5.7 Using the RETURNING Clause in Dynamic SQL 134 CREATE OR REPLACE PROCEDURE ins_note (text_in IN note.text%TYPE, note_id_out OUT note.note_id%TYPE) IS cur INTEGER := DBMS_SQL.OPEN_CURSOR; fdbk INTEGER; v_note_id note.note_id%TYPE; BEGIN DBMS_SQL.PARSE (cur, 'INSERT INTO note (note_id, text) ' || ' VALUES (note_seq.NEXTVAL, :newtext) ' || ' RETURNING note_id INTO :newid', DBMS_SQL.NATIVE); DBMS_SQL.BIND_VARIABLE (cur, 'newtext', text_in); DBMS_SQL.BIND_VARIABLE (cur, 'newid', v_note_id); fdbk := DBMS_SQL.EXECUTE (cur); DBMS_SQL.VARIABLE_VALUE (cur, 'newid', v_note_id); note_id_out := v_note_id; DBMS_SQL.CLOSE_CURSOR (cur); EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE ('Note insert failure:'); DBMS_OUTPUT.PUT_LINE (SQLERRM); END; / These operations should be familiar to you by this time. Here are the steps to perform: 1. Open a cursor and then parse the INSERT statement. Notice that I reference the next value of the sequence right in the VALUES list. The RETURNING clause passes the primary key, note_id, into a placeholder. 2. Bind both the incoming text value and the outgoing primary key. (I am not really binding a value in the second call to DBMS_SQL.BIND_VARIABLE, but I have to perform the bind step anyway.) 3. Execute the cursor and retrieve the primary key from the bind variable in the RETURNING clause. Notice that I pass back the value into a local variable, which is then copied to the OUT argument. Otherwise, I would need to define the note_id_out parameter as an IN OUT parameter. 4. I include an exception section to display any errors that might arise. The following anonymous block tests this procedure by inserting a row and then trying to insert it again: /* Filename on companion disk: insret.sp */* DECLARE myid note.note_id%TYPE; BEGIN ins_note ('Note1', myid); DBMS_OUTPUT.PUT_LINE ('New primary key = ' || myid); myid := NULL; ins_note ('Note1', myid); EXCEPTION WHEN OTHERS THEN NULL; [Appendix A] What's on the Companion Disk? 2.5.7 Using the RETURNING Clause in Dynamic SQL 135 . programmatic interface to your data. 2.5.6.5 Using array processing in dynamic PL/SQL The first release of Oracle8 (8.0.3) contained a number of bugs relating to the use of array processing in dynamic PL/SQL before the copy. 2.5.7 Using the RETURNING Clause in Dynamic SQL One of the many enhancements in Oracle8 is the addition of the RETURNING clause to INSERTs, UPDATEs, and DELETEs. You can use the