Reversing direction in a PL/SQL range for-loop
Have you ever wondered how to decrement index values in a PL/SQL range for-loop. You’ve probably heard that you can’t. That’s part true and part false.
The true part is two fold. First, range for-loops move forward from a starting low value to high value. Second, the loop exits immediately when your starting value is high and ending value low. If you use the REVERSE keyword, the loop moves backward from an ending point to the starting point. This happens because a PL/SQL range for-loop is a guard on entry loop. The starting value is compared against the ending value and exits when the starting value is greater than the ending value.
The false part happens when you want to traverse a collection backward. Oracle collections are varrays, tables, or associative arrays. Associative arrays are also known as PL/SQL tables. You overcome the ascending only limitation by using the REVERSE keyword or leveraging the Oracle Collection API library functions.
This code works when the index is numeric:
DECLARE TYPE collection IS TABLE OF VARCHAR2(20); fellowship COLLECTION := collection('Frodo','Gandalf','Aragorn'); BEGIN -- Increasing iterator values. FOR i IN REVERSE 1..fellowship.COUNT LOOP DBMS_OUTPUT.put('Iterator ['||i||'] '); DBMS_OUTPUT.put('Value ['||fellowship(i)||']'); DBMS_OUTPUT.new_line(); END LOOP; END; / |
This code works when the index is a string in an associative array:
DECLARE CURRENT VARCHAR2(8); element VARCHAR2(20); TYPE collection IS TABLE OF VARCHAR2(20) INDEX BY VARCHAR2(8); fellowship COLLECTION; BEGIN -- Assign character indexed values. fellowship('Hobbit') := 'Frodo'; fellowship('Wizard') := 'Gandalf'; fellowship('Human') := 'Aragorn'; -- Increasing iterator values. FOR i IN 1..fellowship.COUNT LOOP IF i = 1 THEN -- Set starting point and assign value. CURRENT := fellowship.LAST; element := fellowship(CURRENT); ELSE -- Change value until you read the first element. IF fellowship.PRIOR(CURRENT) IS NOT NULL THEN CURRENT := fellowship.PRIOR(CURRENT); element := fellowship(CURRENT); ELSE -- Exit when all elements read. EXIT; END IF; END IF; DBMS_OUTPUT.put('Iterator ['||i||'] '); DBMS_OUTPUT.put('Index ['||CURRENT||'] '); DBMS_OUTPUT.put('Value ['||element||']'); DBMS_OUTPUT.new_line(); END LOOP; END; / |
Chris Neumüller suggested another example with a WHILE loop:
DECLARE TYPE collection IS TABLE OF VARCHAR2(20) INDEX BY VARCHAR2(8); fellowship COLLECTION; CURRENT VARCHAR2(20); BEGIN -- Assign character indexed values. fellowship('Hobbit') := 'Frodo'; fellowship('Wizard') := 'Gandalf'; fellowship('Human') := 'Aragorn'; -- Increasing iterator values. IF fellowship.COUNT > 0 THEN CURRENT := fellowship.LAST; WHILE CURRENT IS NOT NULL LOOP DBMS_OUTPUT.put('Iterator ['||CURRENT||'] '); DBMS_OUTPUT.put('Value ['||fellowship(CURRENT)||'] '); DBMS_OUTPUT.new_line(); CURRENT := fellowship.PRIOR(CURRENT); END LOOP; END IF; END; / |
While the former works, you should consider changing the cursor that populates the collection by adding a descending ORDER BY clause when indexing with a string. The algorithm is a bit more work but it also supports sparsely populated indexes and string-indexed associative arrays. All you need do is swap the LAST for FIRST and PRIOR for NEXT to go forward through a sparsely populated index.
UTOUG web site updates links
There were two broken links in earlier blogs because UTOUG moved their presentation download repository. I’ve updated those links for my Utah Oracle User Group Training Days 2008 presentations. You can also uses these new links: PL/SQL 11g New Features and an Oracle LOBs. You can learn more about UTOUG and prior presentations at http://www.utoug.org, including Tom Kyte’s Materialized Views presentation from the same 2008 spring conference.
Result cache functions in Oracle 11g return what?
Adrian Billington wrote an excellent article on how to use the Oracle 11g PL/SQL new cross session result cache. He uses Tom Kyte’s runstats tuning kit to show the best performance advantage of this new feature. The recommendation is converting functions that perform SQL lookups to result cache functions. A result cache function’s actual parameters and results are cached only once in the SGA and available across sessions.
A result cache function can only support SQL datatypes. This limits you to returning only a scalar or collection of a scalar datatype from your function. Collections of scalar datatypes are varray or nested table user-defined types (UDT). When you try to return an aggregate table from a result cache function, Oracle 11g Release 1 raises a PLS-00999 error. The error signals an implementation restriction that may be temporary. Let’s hope it changes in Oracle 11g Release 2.
You can cheat the limitation by returning a collection of tokenized strings but I’m not sure it’s worth the effort. If you think it’s worth the effort, you can check out the code.
PHP code to read a PL/SQL reference cursor
The following demonstrates how to read a PL/SQL reference cursor in a PHP program. The reference cursor function is defined in the Pipelined Functions & PL/SQL Tables blog page. I’ve commented it to the hilt for those new to PHP.
The reference cursor maintains a separate connection to the database to access the reference cursor. You also use the oci_fetch_assoc() function to get the data. That strip_special_characters() function lets you format your call to the PL/SQL program and remove non-parsing line returns and tabs before running the oci_parse() function.
<?php // Return successful attempt to connect to the database. if ($c = @oci_connect("plsql","plsql","orcl")) { // Declare input variables. (isset($_GET['table'])) ? $table = (int) $_GET['table'] : $table = 'ITEM'; (isset($_GET['column'])) ? $column = (int) $_GET['column'] : $column = 'ITEM_TYPE'; // Declare a PL/SQL execution command. $stmt = "BEGIN :return_cursor := get_common_cursor(:table,:column); END;"; // Strip special characters to avoid ORA-06550 and PLS-00103 errors. $stmt = strip_special_characters($stmt); // Parse a query through the connection. $s = oci_parse($c,$stmt); // Declare a return cursor for the connection. $rc = oci_new_cursor($c); // Bind PHP variables to the OCI input or in mode variables. oci_bind_by_name($s,':table',$table); oci_bind_by_name($s,':column',$column); // Bind PHP variables to the OCI output or in/out mode variable. oci_bind_by_name($s,':return_cursor',$rc,-1,OCI_B_CURSOR); // Execute the PL/SQL statement & reference cursor. oci_execute($s); oci_execute($rc); // Print the table header with known labels. print '<table border="1" cellpadding="3" cellspacing="0">'; // Set dynamic labels control variable true. $label = true; // Read the contents of the reference cursor. while($row = oci_fetch_assoc($rc)) { // Declare header and data variables. $header = ""; $data = ""; // Read the reference cursor into a table. foreach ($row as $name => $column) { // Capture labels for the first row. if ($label) { $header .= '<td class="e">'.$name.'</td>'; $data .= '<td class="v">'.$column.'</td>'; } else $data .= '<td class=v>'.$column.'</td>'; } // Print the header row once. if ($label) { print '<tr>'.$header.'</tr>'; $label = !$label; } // Print the data rows. print '<tr>'.$data.'</tr>'; } // Print the HTML table close. print '</table>'; // Disconnect from database. oci_close($c); } else { // Assign the OCI error and format double and single quotes. $errorMessage = oci_error(); print htmlentities($errorMessage['message'])."<br />"; } // Strip special characters, like carriage or line returns and tabs. function strip_special_characters($str) { $out = ""; for ($i = 0;$i < strlen($str);$i++) if ((ord($str[$i]) != 9) && (ord($str[$i]) != 10) && (ord($str[$i]) != 13)) $out .= $str[$i]; // Return character only strings. return $out; } ?> |
Pipelined Functions & PL/SQL Tables
Pipelined functions let you retrieve record structures from PL/SQL Tables as rows in SQL statements. PL/SQL tables are also known as associative arrays from 10g forward. While pipelined functions are slower than other approaches, they are easy to implement. The alternative uses a complex query. The query is complex because it typically uses selective aggregation and nested CASE logic.
Entering a colon in an NDS statement
Somebody asked me how you include a colon in a Native Dynamic SQL (NDS) statement when it’s not related to a placeholder. A colon without a placeholder raises an ORA-00947 error when parsing the statement. The error means you’ve failed to submit enough bind variables. You can substitute a CHR(58) where you need to insert the standalone colon. The NDS or DBMS_SQL parsing phase ignores a CHR(58), which translates during actual SQL statement parsing as a colon.
Let’s say you want to insert a column value with an ASIN (Amazon Standard Identification Number) code in the format: ASIN: B000VBJEEG
Using NDS, you have two choices. You can let the entry person type the full string and pass that string as a bind variable, or you can substitute CHR(58) for the colon and enter only the ASIN code. The example (Oracle Database 11g PL/SQL Programming, pp. 386-387) implements the latter:
CREATE OR REPLACE PROCEDURE insert_item ( asin VARCHAR2 , item_type VARCHAR2 , item_title VARCHAR2 , item_subtitle VARCHAR2 := '' , rating VARCHAR2 , agency VARCHAR2 , release_date DATE ) IS -- Local variable for a dynamic SQL statement. stmt VARCHAR2(2000); BEGIN -- Create a dynamic statement with bind variables. stmt := 'INSERT INTO item ' || '( item_id' || ', item_barcode' || ', item_type' || ', item_title' || ', item_subtitle' || ', item_desc' || ', item_blob' || ', item_photo' || ', item_rating' || ', item_rating_agency' || ', item_release_date' || ', created_by' || ', creation_date' || ', last_updated_by' || ', last_update_date ) ' || 'VALUES ' || '( item_s1.nextval' || ',''ASIN''||CHR(58)||:asin' || ',(SELECT common_lookup_id' || ' FROM common_lookup' || ' WHERE common_lookup_type = :item_type)' || ', :item_title' || ', :item_subtitle' || ', empty_clob()' || ', NULL, NULL' || ', :rating' || ', :agency' || ', :release_date' || ', 3, SYSDATE, 3, SYSDATE)'; -- Print debug statement. dbms_output.put_line(stmt); -- Execute dynamic statement with bind variables. EXECUTE IMMEDIATE stmt USING asin, item_type, item_title, item_subtitle, rating, agency, release_date; END insert_item; / |
There’s quite a nifty or nasty trick inside the dynamic SQL statement. You’ll notice that the colon is concatenated to the ASIN and a bind variable. It is critical that you don’t encapsulate the bind variable inside quotes, or you’ll raise an ORA-01006 exception (check Table 11-2 in the Oracle Database 11g PL/SQL Programming book for more detail). You can’t enclose a string in single quotes because the string is substituted as a string, and it trips up the parser.
You can test this dynamic SQL statement with the following anonymous PL/SQL block:
BEGIN insert_item('B00005JPO1' ,'DVD_WIDE_SCREEN' ,'Indiana Jones and the Crystal Skull' ,'2-Disc Special Edition' ,'PG-13' ,'MPAA' ,'14-OCT-08'); END; / |
Oracle PHP/AJAX Framework
While writing Oracle Database 11g PL/SQL Programming book last year, I collaborated with Lee Barney on a PHP/AJAX framework. Our Oracle Database AJAX & PHP Web Application Development book covers the framework. You can find a new and improved version at SourceForge.net. Lee maintains the code and continues to improve it. I’m sure he’d welcome suggestions and collaboration. We see it as very useful to AJAX web development, and to the Open Source community.
The IS OF operator for object type comparisons
You can do base type and subtype comparisons with the IS OF or IS NOT OF operators. They work like the typeof operator in Java. They also eliminate null values when you use them in a WHERE clause or an IF block. They work against all SQL user-defined object types. The prototypes are:
object_variable IS OF (object_type1 [, object_type2, [object_type(n+1)]]) object_variable IS NOT OF (object_type1 [, object_type2, [object_type(n+1)]]) |
The left operand should be an object column or element of a collection. The IS OF returns true when the object_variable matches the object type or a member of the list of object types. The IS NOT OF returns true when the object_variable doesn’t match, and both return false when the object_variable is a null value.
Oracle Database LOBs In-depth
Last month I presented the Oracle Database LOBs In-depth at the Utah Oracle User’s Group (UTOUG) Training Days. You can find the slides and sample programs here. The presentation illustrates how the transaction context works and how to access, maintain, and manage LOBs.
Cleaning up wasted LOB space
After replacing the contents of a BLOB or CLOB column, you will see the size grow because old indexes and segments aren’t deleted or removed from the index. The only way to get rid of the old information is to perform some simple table maintenance. The following provides an example of dumping redundant or obsolete space and indexes.
You should first check space, by using the query provided earlier in my blog to compare LOB indexes and segments. Such extremes as the regular expression in that query aren’t needed when DBAs ensure that LOBs are created with named segments. In this test case, this is the starting point:
TABLE COLUMN Segment Segment Name Name Segment Name TYPE Bytes ----- --------- ------------------------- ---------- --------- ITEM ITEM_BLOB SYS_IL0000074435C00007$$ LOBINDEX 65536 ITEM ITEM_BLOB SYS_LOB0000074435C00007$$ LOBSEGMENT 2097152 ITEM ITEM_DESC SYS_IL0000074435C00006$$ LOBINDEX 393216 ITEM ITEM_DESC SYS_LOB0000074435C00006$$ LOBSEGMENT 226492416 |
You create a temporary CLOB column in the target table. Then, you update the temporary column with the value from your real column.
ALTER TABLE item ADD (item_temp CLOB); UPDATE item SET item_temp = item_desc; |
When you requery the table’s indexes and segments, you’d find something like the following. You should note the size of the index and segments are three times larger in the real column than the temporary columns.
TABLE COLUMN Segment Segment Name Name Segment Name TYPE Bytes ----- --------- ------------------------- ---------- --------- ITEM ITEM_BLOB SYS_IL0000074435C00007$$ LOBINDEX 65536 ITEM ITEM_BLOB SYS_LOB0000074435C00007$$ LOBSEGMENT 2097152 ITEM ITEM_DESC SYS_IL0000074435C00006$$ LOBINDEX 393216 ITEM ITEM_DESC SYS_LOB0000074435C00006$$ LOBSEGMENT 226492416 ITEM ITEM_TEMP SYS_IL0000074435C00016$$ LOBINDEX 131072 ITEM ITEM_TEMP SYS_LOB0000074435C00016$$ LOBSEGMENT 65011712 |
You drop the real column and add it back, or simply rename the new table to the old column once you’ve dropped it. Then, you update the real column with the values from the temporary column.
ALTER TABLE item DROP COLUMN item_desc; ALTER TABLE item ADD (item_desc CLOB); UPDATE item SET item_desc = item_temp; |
You can now requery the table and find that you’ve eliminated extraneous space.
TABLE COLUMN Segment Segment Name Name Segment Name TYPE Bytes ----- --------- ------------------------- ---------- --------- ITEM ITEM_BLOB SYS_IL0000074435C00006$$ LOBINDEX 65536 ITEM ITEM_BLOB SYS_LOB0000074435C00006$$ LOBSEGMENT 2097152 ITEM ITEM_DESC SYS_IL0000074435C00016$$ LOBINDEX 131072 ITEM ITEM_DESC SYS_LOB0000074435C00016$$ LOBSEGMENT 65011712 ITEM ITEM_TEMP SYS_IL0000074435C00016$$ LOBINDEX 131072 ITEM ITEM_TEMP SYS_LOB0000074435C00016$$ LOBSEGMENT 65011712 |
You drop the temporary column after making the change.
ALTER TABLE item DROP COLUMN item_temp; |
You can now requery the table and find that you’ve eliminated extraneous space.
TABLE COLUMN Segment Segment Name Name Segment Name TYPE Bytes ----- --------- ------------------------- ---------- --------- ITEM ITEM_BLOB SYS_IL0000074435C00006$$ LOBINDEX 65536 ITEM ITEM_BLOB SYS_LOB0000074435C00006$$ LOBSEGMENT 2097152 ITEM ITEM_DESC SYS_IL0000074435C00016$$ LOBINDEX 131072 ITEM ITEM_DESC SYS_LOB0000074435C00016$$ LOBSEGMENT 65011712 |