examine the consequences of using a placeholder. Suppose my dynamic PL/SQL block were constructed as follows: 'BEGIN ' || nm_in || ' := :val; END;', When I bind in my value with a call to DBMS_SQL.BIND_VARIABLE, it is not treated as a literal. For example, if I call dynvar.copyto with the following arguments, dynvar.copyto ('steven''s hairline', 'rapid.retreat'); then the PL/SQL block I would execute is, BEGIN rapid.retreat := steven's hairline; END; which would definitely run into compile problems. When I first encountered this problem, I figured that I would embed the :val placeholder inside single quotes to make sure that the value I pass in was treated as literal. My parse statement then looked like this: DBMS_SQL.PARSE (cur, 'BEGIN ' || nm_in || ' := '':val''; END;', DBMS_SQL.NATIVE); But this would not work; DBMS_SQL did not recognize :val as a placeholder, any longer since it was inside single quotes. Sigh. The only solution was to remove :val, and, instead of using placeholders, simply concatenate the value directly into the string inside single quotes. The dynvar package should get you comfortable with executing dynamic PL/SQL code and also with the concept of indirect referencing. I suggest that you try extending dynvar to support other datatypes (numbers and dates, in particular). See what challenges you encounter. You will definitely need to make some changes to incorporate these different datatypes into your dynamically constructed blocks. 2.5.6 Array Processing with DBMS_SQL One of the most significant advances for DBMS_SQL in Oracle8 is the support for "array processing" through use of the BIND_ARRAY and DEFINE_ARRAY procedures. Using these procedures, you can greatly speed up dynamic SQL processing that involves multiple rows of data. This section offers some general suggestions and then provides detailed examples of using array processing in DBMS_SQL to perform fetches, queries, and dynamic PL /SQL. In order to take advantage of array processing, you will need to be comfortable with the use of index−by tables (called PL /SQL tables in Oracle7). These structures are like single−dimension arrays and are described in detail in Chapter 10 of Oracle PL /SQL Programming. With this feature under your belt, you are ready to implement array processing with DBMS_SQL. As you do so, keep in mind the following tips: • When you declare index−by tables to be used in DBMS_SQL processing, you must declare them based on one of the DBMS_SQL table TYPES: DBMS_SQL.NUMBER_TABLE DBMS_SQL.VARCHAR2_TABLE DBMS_SQL.DATE_TABLE DBMS_SQL.BLOB_TABLE DBMS_SQL.CLOB_TABLE DBMS_SQL.BFILE_TABLE Here is an example of declaring a table used to receive multiple pointers to BFILEs from a dynamically constructed query: [Appendix A] What's on the Companion Disk? 2.5.6 Array Processing with DBMS_SQL 121 DECLARE bfiles_list DBMS_SQL.BFILE_TABLE; One very significant downside to the approach taken by Oracle to support array processing with DBMS_SQL is that you cannot use index−by tables of records. Instead, you will need to declare individual tables for each of the columns you wish to query or placeholders you wish to put in your SQL statements. • If you are going to fetch more than one row with a single call to FETCH_ROWS, you must define the columns in that cursor as "array columns." To do this, you must provide an index−by table in the call to DEFINE_ARRAY. The following example shows the steps required to fetch a whole bunch of salaries (up to 100 at a time) from the emp table: DECLARE cur PLS_INTEGER := DBMS_SQL.OPEN_CURSOR; sal_table DBMS_SQL.NUMBER_TABLE; BEGIN DBMS_SQL.PARSE (cur, 'SELECT sal FROM emp', DBMS_SQL.NATIVE); DBMS_SQL.DEFINE_COLUMN (cur, 1, sal_table, 100, 10); • When you call DEFINE_ARRAY, you also can specify the starting row in which data is to be placed when you call DBMS_SQL.COLUMN_VALUE (in the previous example, I specified that the starting row to be 10). This is a little bit odd and worth emphasizing. That fifth argument does not have anything to do with the index−by table you pass as the third argument to DEFINE_ARRAY. It applies to the index−by table that is passed to the call to COLUMN_VALUE (which might or might not actually be the same index−by table). If you don't provide a value for this starting row, values are assigned to the receiving index−by table from row 1. In all cases, the rows are filled sequentially. Why would you not use the default value of 1 for starting row? You might want to preserve the values already stored in the index−by table, and simply add the new rows of data to the table. • If you want to bind multiple values into a SQL statement, you must call BIND_ARRAY instead of the scalar BIND_VARIABLE procedure. You will do this when you want to modify more than one row in the table with a single DML statement. • If you are using more than one array or index−by table in a single SQL statement, remember that DBMS_SQL applies a "lowest common denominator" rule. It determines the highest starting row number and the lowest ending row number across all arrays and then uses only those rows between those endpoints in all of the arrays. The following section shows in more detail how to use array processing in DBMS_SQL to fetch multiple rows with a single call to DBMS_SQL.FETCH_ROWS, update multiple rows of data with multiple bind variables, and manipulate index−by table contents with dynamic PL/SQL. 2.5.6.1 Using array processing to insert There are three different ways to change the contents of a table: INSERT, DELETE, and UPDATE. This section shows examples of the first of these DML statements, INSERT, utilizing the array features of DBMS_SQL. The next two sections show examples of DELETE and UPDATE. The following procedure inserts the contents of a index−by table into a database table using single−row INSERTs (the Version 7 behavior). Notice that I parse once, but bind and execute for each individual row in the index−by table. I do not need to reparse, since I am not changing the SQL statement, only the value I am [Appendix A] What's on the Companion Disk? 2.5.6 Array Processing with DBMS_SQL 122 binding into that SQL statement. Notice also that I first create a package to define two index−by table types. Why do I do this? I am passing in index−by tables as arguments to my insert procedure. The parameter list of the procedure must therefore reference existing index−by table types in order to compile. I could also have put the table type declarations and the procedure into a single package. /* Filename on companion disk: dynins.sp */* CREATE OR REPLACE PACKAGE mytables IS TYPE number_table IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; TYPE varchar2_table IS TABLE OF VARCHAR2(2000) INDEX BY BINARY_INTEGER; END; / CREATE OR REPLACE PROCEDURE instab7 (empnotab IN mytables.number_table, enametab IN mytables.varchar2_table) IS cur PLS_INTEGER := DBMS_SQL.OPEN_CURSOR; fdbk PLS_INTEGER; totfdbk PLS_INTEGER := 0; BEGIN DBMS_SQL.PARSE (cur, 'INSERT INTO emp2 (empno, ename) VALUES (:empno, :ename)', DBMS_SQL.NATIVE); FOR emprow IN empnotab.FIRST empnotab.LAST LOOP DBMS_SQL.BIND_VARIABLE (cur, 'empno', empnotab(emprow)); DBMS_SQL.BIND_VARIABLE (cur, 'ename', enametab(emprow)); fdbk := DBMS_SQL.EXECUTE (cur); totfdbk := totfdbk + fdbk; END LOOP; DBMS_OUTPUT.PUT_LINE ('Rows inserted: ' || TO_CHAR (totfdbk)); DBMS_SQL.CLOSE_CURSOR (cur); END; / How would this implementation change with Oracle8? You no longer have to perform separate INSERTs for each of the rows in the index−by table. Instead, you can bind an index−by table directly into the SQL statement. The following version of the instab procedure relies on this new feature. Notice that the FOR LOOP is gone. Instead, I call BIND_ARRAY once for each column in the INSERT statement, passing in the appropriate index−by table. Notice that I must now use the DBMS_SQL table types, whereas in the previous example I created my own index−by table types and used those in the parameter list. /* Filename on companion disk: dynins.sp */* CREATE OR REPLACE PROCEDURE instab8 (empnotab IN DBMS_SQL.NUMBER_TABLE, enametab IN DBMS_SQL.VARCHAR2_TABLE) IS cur PLS_INTEGER := DBMS_SQL.OPEN_CURSOR; fdbk PLS_INTEGER; BEGIN DBMS_SQL.PARSE (cur, 'INSERT INTO emp2 (empno, ename) VALUES (:empno, :ename)', DBMS_SQL.NATIVE); DBMS_SQL.BIND_ARRAY (cur, 'empno', empnotab); DBMS_SQL.BIND_ARRAY (cur, 'ename', enametab); fdbk := DBMS_SQL.EXECUTE (cur); [Appendix A] What's on the Companion Disk? 2.5.6 Array Processing with DBMS_SQL 123 DBMS_OUTPUT.PUT_LINE ('Rows inserted: ' || TO_CHAR (fdbk)); DBMS_SQL.CLOSE_CURSOR (cur); END; So in this case I end up with less code to write. In other situations, you may have to copy your data from your own index−by tables, perhaps a table or record that is not supported by DBMS_SQL, into the appropriate DBMS_SQL−declared index−by tables before you can call BIND_ARRAY. But putting aside code volume, what about the impact on performance? To compare these two approaches, I wrote the following SQL*Plus script: /* Filename on companion disk: dynins.tst */* DECLARE timing PLS_INTEGER; empnos7 mytables.number_table; enames7 mytables.varchar2_table; empnos8 DBMS_SQL.NUMBER_TABLE; enames8 DBMS_SQL.VARCHAR2_TABLE; BEGIN /* Load up the index−by tables. */ FOR i IN 1 &1 LOOP empnos&2(i) := 10000 + i; enames&2(i) := 'Eli ' || TO_CHAR (i); END LOOP; timing := DBMS_UTILITY.GET_TIME; instab&2 (empnos&2, enames&2); DBMS_OUTPUT.PUT_LINE ('V&2 style = ' || TO_CHAR (DBMS_UTILITY.GET_TIME − timing)); END; / Though SQL*Plus is a fairly crude tool, those substitution parameters let you write some clever, concise utilities. In this case, I populate my index−by tables with &1 number of rows −− but the index−by table I fill up depends on the &2 or second argument: the Oracle7 or Oracle8 versions. Once the tables have their data, I pass them to the appropriate version of "instab," and, using DBMS_UTILITY.GET_TIME, display the number of hundredths of seconds that elapsed. I ran this script a number of times to even out the bumps of initial load and parse and so on. The following numbers are representative: SQL> @dynins.tst 1000 7 Rows inserted: 1000 V7 style = 90 SQL> @dynins.tst 1000 8 Rows inserted: 1000 V8 style = 2 As you can see, dynamic SQL using arrays or index−by tables will perform significantly better than the single−row processing available in Oracle7. So that's how you do INSERTs: for each column for which you are inserting a value, you must bind in an array or index−by table. You can also mix together scalars and arrays. If, for example, I wanted to insert a set of new employees and make the hiredate the value returned by SYSDATE, the bind steps in my instab8 procedure would be modified as follows: DBMS_SQL.BIND_ARRAY (cur, 'empno', empnotab); DBMS_SQL.BIND_ARRAY (cur, 'ename', enametab); [Appendix A] What's on the Companion Disk? 2.5.6 Array Processing with DBMS_SQL 124 DBMS_SQL.BIND_VARIABLE (cur, 'hiredate', SYSDATE); In other words: two array binds and one scalar bind. The same value returned by SYSDATE is then applied to all rows inserted. 2.5.6.2 Using array processing to delete The process for deleting multiple rows with a single call to the EXECUTE function is similar to that for INSERTSs: one call to BIND_ARRAY for each placeholder in the SQL statement. With DELETEs, however, the placeholders are in the WHERE clause, and each row in the index−by table is used to identify one or more rows in the database table. The following procedure removes all rows as specified in the array of names. Notice the use of LIKE and UPPER operators to increase the flexibility of the table entries (if that is what you want!). /* Filename on companion disk: dyndel.sp */* CREATE OR REPLACE PROCEDURE delemps (enametab IN DBMS_SQL.VARCHAR2_TABLE) IS cur PLS_INTEGER := DBMS_SQL.OPEN_CURSOR; fdbk PLS_INTEGER; BEGIN DBMS_SQL.PARSE (cur, 'DELETE FROM emp WHERE ename LIKE UPPER (:ename)', DBMS_SQL.NATIVE); DBMS_SQL.BIND_ARRAY (cur, 'ename', enametab); fdbk := DBMS_SQL.EXECUTE (cur); DBMS_OUTPUT.PUT_LINE ('Rows deleted: ' || TO_CHAR (fdbk)); DBMS_SQL.CLOSE_CURSOR (cur); END; / The standard emp table contains the following 14 names: ADAMS JAMES SCOTT ALLEN JONES SMITH BLAKE KING TURNER CLARK MARTIN WARD FORD MILLER Now let's run the following script: /* Filename on companion disk: dyndel.tst */* DECLARE empnos8 DBMS_SQL.NUMBER_TABLE; enames8 DBMS_SQL.VARCHAR2_TABLE; BEGIN /* Load up the index−by table. */ enames8(1) := '%S%'; enames8(2) := '%I%'; delemps (enames8); END; / SQL> @dyndel.tst Rows deleted: 8 And we are then left with the following employees: [Appendix A] What's on the Companion Disk? 2.5.6 Array Processing with DBMS_SQL 125 . of index−by tables (called PL /SQL tables in Oracle7 ). These structures are like single−dimension arrays and are described in detail in Chapter 10 of Oracle PL /SQL Programming. With this feature. of rows −− but the index−by table I fill up depends on the &2 or second argument: the Oracle7 or Oracle8 versions. Once the tables have their data, I pass them to the appropriate version. DECLARE bfiles_list DBMS_SQL.BFILE_TABLE; One very significant downside to the approach taken by Oracle to support array processing with DBMS_SQL is that you cannot use index−by tables of records.