Collections PLSQL
Collections PLSQL
Oracle DB
Column Row Multi-row column
Oracle PL/SQL
Constant, Literal, Simple variable Record Collection: 1) PL/SQL Table Index-By Table (name change) Associative Array (name change) 2) Nested Table 3) Varray 1) Index-By Table (Array) of Record 2) Nested Table of Record or Object 3) Varray of Record or Object 1) Nested Table of Collection(s) 2) Varray of Collection(s)
DB Version
6.? 7.0 7.0 8.0 9.1 8.0 8.0 7.3.4 8.0 8.0 9.2 9.2
Other Languages
Same Struct, Object Hash by integer Hash by integer or string Array, Queue Array, Stack Array, Bag, Vector, Tree, Heap Multi-dimensional array (extensible) Multi-dimensional array (bounded)
History
For those who wrote enterprise-class Oracle applications in PL/SQL before version 7, I salute you. I dont know how you did it. With version 7, Oracle finally introduced the PL/SQL Record and PL/SQL Table. With records, the contents of a programmer-defined structure or table row could be loaded into a variable that could be manipulated and used as a parameter to PL/SQL routines2. With PL/SQL Tables, lists of scalar values, and even lists of records, could be created and manipulated without the overhead of doing everything with tables and SQL statements. This greatly simplified programming in PL/SQL and enabled elegant, robust PL/SQL routines. Life was good.
1 2
You may download the presentation and code examples from http://www.cbdba.net/papers.htm By routines, I mean packaged or stand-alone procedures and functions
www.rmoug.org
8i and 9i Collections
Coulam
Unfortunately, PL/SQL still resembled a 3-legged chair. Oracle programmers continued to run into limitations in their quest to simplify and speed up their routines. The significant limitations and when they were finally resolved are:
Limitations and Frustrations Cannot store a list of values related to an individual row. Alternative: create a separate attributive table to hold the rows list. These temporary child tables clutter the schema, add processing and storage overhead, and make code more complex. Cannot use a collection in SQL statements. Alternative: create, populate and join to a temporary table. Could not do array-like, or bulk SQL operations on collections. Alternative: None. Limited to one-by-one row processing. Shifting from the PL/SQL to SQL engine as each row was processed was a real drag on performance. Cannot access list values using character-based keys. Alternative: create another kludge table to store the key:value pairs, or roll your own hash tables using a packaged PL/SQL table of record, hash algorithms and functions. Do-able, but not fun. Cannot insert or update a table using a record. Alternative: None. Break out a record into its individual columns for insert/update. This effectively eliminated the benefits of abstraction and de-coupling that records were meant to provide. Cannot use awesome BULK ops to get data directly into collections of record. Alternative: break every column down into its own collection of scalar. That was really unpleasant working with any more than a three-column table. Fixed 8.0 8.0 8.1 9.2 9.2 9.2
where field_type is any PL/SQL datatype3 except REF CURSOR4 and where expression yields a value whose type is equal to field_type. Heres a hairy example that covers most of the possibilities:
TYPE tr_account IS RECORD ( account_id acc_types.t_account_id NOT NULL packaged subtype, constrained , account_owner acc_types.tr_account_owner packaged record , related_accounts acc_types.tas collection of disparate account IDs , created_dtm TIMESTAMP WITH LOCAL TIMEZONE DEFAULT SYSTIMESTAMP precise time w/default , active_flg accounts.active_flg%TYPE anchored to table column type , fraud_check risk_types.to_fraud_check packaged object type );
All the scalars, including UROWID, BOOLEAN and RAW; and PL/SQL objects and REFs, collections, LOBs, and other records. 4 The PL/SQL Bible, Oracle PL/SQL Programming, 3rd Ed, claims that records can contain ref cursors (p. 326). The 9.2 Oracle docs do not support this statement, and indeed you get error PLS-00989 if you try it.
www.rmoug.org
8i and 9i Collections
Coulam
A record type is defined implicitly by the existence of a table-like object (table, view, synonym, cursor, or cursor variable). Once the record has been defined explicitly or implicitly, it simply remains to declare variables or parameters based on that underlying object or cursor:
PROCEDURE cancel_account(ir_account IN acc_types.tr_account) IS parameter based on explicit type CURSOR c_mytab IS SELECT col1, col2, FROM mytab WHERE ; --explicit cursor lr_mytab c_mytab%ROWTYPE; --based on explicit cursors ROWTYPE attribute TYPE trc_mytab IS REF CURSOR RETURN mytab%ROWTYPE; --define ref cursor lrc_mytab trc_mytab; --declare ref cursor variable lr_mytab lrc_mytab%ROWTYPE; --based on ref cursors ROWTYPE attribute lr_myview myview%ROWTYPE; --based on views ROWTYPE attribute lr_mytab mytab%ROWTYPE; --based on tables ROWTYPE attribute lr_theirtab theirtab%ROWTYPE; --based on ROWTYPE attribute of private synonyms table lr_cust_account acc_types.tr_account; --based on packaged, explicit record BEGIN FOR lr_mytab -based on implicit cursor IN (SELECT col1, col2, FROM mytab WHERE ) LOOP <do stuff> END LOOP;
Record Usage Find a complete treatment of this subject in the Oracle docs and various excellent PL/SQL programming books. Briefly, however, when working with the whole record variable, you may:
lr_account := ir_account; --copy one record to another (known as aggregate assignment) lr_old_account := NULL; --empty out a record by assigning NULL lr_new_account := lr_old_account; --empty by assigning an empty record -- Use the record as a parameter, or as the value returned by a function. lr_new_account := fr_create_checking_account(lr_cust_info, ls_cheezy_gift_choice); -- Direct SELECT or fetch into a record SELECT * INTO lr_account FROM accounts_vw WHERE account_id = in_account_id;
Individual fields within the record are handled just like regular variables, the only difference being the dot notation used to access that part of the record, e.g.
lr_account.active_flg := A; -- dot notation to records field lar_accounts(i).active_flg := I; --dot notation to one of the records in a collection
Collections
A collection is nothing more than a list, an ordered group of elements, all of the same type much like a single-dimension array in C or a one-column relational table. Each element in a collection has a unique index, or key, that determines its position in the collection. If you know the key, you are able to perform random access to the value, which is faster than iteration and even faster than SQL against the same information in an index-organized or cached table. Oracle provides three different collections: associative arrays, nested tables and varrays. Associative because the value is directly tied, or associated, with its numeric or character-based key. Nested, because as a column type, it allows you to embed, or nest, a table within a column of a row. Varray, short for variable-size array, is mystifying. The name and the Oracle docs5 suggest that varrays are more flexible than the other collection types, when in fact, the opposite is true. I have yet to find that a varray was better suited to my problem than a nested table. Collection Syntax Unlike implicit records, all collection types must be defined, either locally or globally. Locally means the collection type is only available inside PL/SQL, and unless its declared in a package specification, its only available to the current PL/SQL
5
The PL/SQL Users Guide is poorly worded. It says varrays hold a fixed number of elements (although you can change the number of elements at runtime). It also implies varrays are extensible: To increase the size of a nested table or varray, use EXTEND. It also showed a varray with a maxsize of 10, and then proceeded to explain that the uppder bound could be extended to 8, 9, 10, and so on. Um, no. You will get ORA-06532 if you try to extend beyond 10, its maximum size.
www.rmoug.org
8i and 9i Collections
Coulam
block. Globally means it is available for use within PL/SQL, SQL, object type definitions, and as a datatype for columns in database tables. If you want to perform SQL against your collection, it must be globally defined. To define a collection type locally:
TYPE type_name IS TABLE OF element_type [NOT NULL]; --nested table TYPE type_name IS {VARRAY | VARYING ARRAY} (size_limit) OF element_type [NOT NULL]; --varray where element_type is any PL/SQL datatype except REF CURSOR6 and size_limit is a positive integer indicating
the
maximum number of elements that will be allowed in the varray. Also varrays cannot contain LOBs. To define a nested table collection type globally (from the SQL*Plus command line), just start the above TYPE definitions with the CREATE OR REPLACE clause. Unlike local collections, since global collections can be used as column datatypes, they share many of the same rules as columns, e.g. global collections cannot use the datatypes: BINARY_INTEGER, PLS_INTEGER, BOOLEAN, LONG, LONG RAW, NATURAL, NATURALN, POSITIVE, POSITIVEN, REF CURSOR, SIGNTYPE, or STRING. Associative arrays cannot be used as column datatypes or in SQL statements. They can only be defined locally:
TYPE type_name IS TABLE OF element_type [NOT NULL] INDEX BY [BINARY_INTEGER | PLS_INTEGER | VARCHAR2(size_limit)];
where size_limit is a positive integer up to the VARCHAR2 datatype max size (32767); Once a collection type has been defined, it may be used, as a PL/SQL variable
lnt_callback_dtm dates.tnt_dtm := tnt_dtm(); --local nested table, initialized empty las_comments strings.tas; --local array of string (VARCHAR2) lar_accounts acc_types.tar_accounts; --local array of record
Note that nested tables and varrays must be initialized before they can be populated or read, unless the population occurs by direct or bulk fetch, or aggregate assignment. Oracle provides the following built-in functions to manipulate collections:
Built-In Function
EXISTS(n) COUNT FIRST, LAST PRIOR(n), NEXT(n) DELETE, DELETE(n), DELETE(a,z) EXTEND, EXTEND(n) TRIM, TRIM(n) LIMIT
Comments
Returns TRUE if there is an element at n, otherwise returns FALSE. Returns 0 if the collection hasnt been initialized, or if it simply has no elements. Does not include elements that have been DELETEd in the count. Once the collection is sparse, COUNT no longer equals LAST. Returns NULL if collection is empty, otherwise returns the smallest/largest index numbers in the collection. If an associative array indexed by strings, uses the binary values of the strings to determine order. Returns the index preceding/following the element at n. Returns NULL if n is the first or last element, respectively. DELETE removes all elements from a collection. DELETE(n) removes the nth element from the collection. n can be a string key. May not use this form with varrays. DELETE(a,z) removes all elements between the range from a to z. a and z can be string keys. May not use this form with varrays. Crucial for nested tables and varrays. Associative arrays automatically increase in size when you assign a new key:value pair to them. Nested table and varrays do not. You must append a new null element to the collection with EXTEND before you can refer to the new subscript or a assign a value to its position. EXTEND(n) appends n null elements. TRIM removes one element from the end of the collection. The other form removes n elements from the end. TRIM includes deleted elements in its successfully trimmed count, so be careful. Treat nested tables as an array and use only DELETE, or treat them as a stack and use only TRIM and EXTEND. For varrays. Returns the maximum size of the varray. Returns NULL for nested tables and associative arrays.
6 7
Local nested tables and varrays were still heavily limited even in Oracle 8.1.7. All the restrictions were lifted in 9.2. Specify the STORE AS table for ANY nested tables in columns, even if it is buried inside a columns object type attributes.
www.rmoug.org
8i and 9i Collections
Coulam
What if you could virtually include the attributive Clingons in the parent record? It would avoid excessive joins, eliminate table clutter, and still preserve normalization. Cascading deletes would be a breeze too: delete the parent row, and Poof! all the child rows are gone too. Updating would be easier too since you wouldnt have to traverse several physical tables to update a table at the bottom of a hierarchy. In OO all this is accomplished using a technique known as object composition. In the relational world, its known as heresy. Heresy or not, since Oracle 8, you can include collections with the parent row to which they belong. Tables like TROUBLE_CALL(3) are easy to create, and make a lot of sense for certain scenarios.
www.rmoug.org
8i and 9i Collections
Coulam
Types
Make a package, perhaps named T which contains all the most generic, enterprise-wide, common subtypes and collections. Include global variables in the T package spec that are based on those collection types. This provides empty collections that are useful for variable re-initialization and DEFAULT values for collection parameters. Then give it a public synonym and grant everyone execute privilege. You will also need to create some generic, global collection types upon which you can build collection utilities that require SQL statements, or object types. You can create public synonyms for these as well, and grant execute privileges, but if youre on 8i, you will need to create private copies of the types in each schema (bug disallowing synonyms to other schemas types.) If yours is a shop that builds multiple applications in a single schema or instance, it is now time to build more specific packages for storing types. For each subsystem, you will want another package specification dedicated to defining a number of subtypes, record types and collections of record based on the columns and tables in the subsystem. I think of this as the end of the database design phase. Now you can move on to the detailed design of your applications isolation layers, using your packaged types for most of the interfaces.
Records
Interfaces As long as you are writing lower-level PL/SQL routines that will be called only by other PL/SQL routines, you should generally use records as the primary parameters. This is especially true and beneficial if you are working on the data access layer. Some of you dont have to imagine a table with 50, 80, even 100 columns or more. You live with such modeling horrors every day. Now imagine an INSERT procedure that uses 50 individual parameters. Consider the very real possibility that Marketing gets an awful promotional approved, and youre stuck figuring out how to model and codify that which defies logic. Theres no budget to refactor your legacy model, so you end up tacking 5 more columns onto the end of that monster table. Now youve got to hunt down all dependents on that table and paste in a bunch more parameters and code. Using a record reduces that entire headache to nothing. Since the records are tied to the table using %ROWTYPE, the code automatically adjusts. No parameters to add, delete or change type length. If youve followed best practices, youll only have the insert and update statements to modify. If youre on 9i, you can even avoid that! 8 If the callers of your PL/SQL routine are written in other languages, avoid records. In some, like PERL, you can write anonymous blocks that can take advantage of PL/SQL-only datatypes, but that is the exception. Java, for example, cannot handle records, period. Your only alternative that comes close is to use object types instead, then convert the objects attributes to a record once the call has reached the PL/SQL level. Then theres the issue of driver support. Even if your driver supports protocol Z, version X, it doesnt mean they did it right. We ended up having to abandon the idea of passing objects or arrays to PL/SQL thanks to our application servers JDBC driver9.
8 9
9.2 finally permits DML using entire records, and bulk operations on collections of records. The workaround was to pass in a delimited string of values. There are overloaded copies of the intended interface. The one matching the Java call is just a wrapper that tokenizes the strings into records and collections, which it passes along to the original interface.
www.rmoug.org
8i and 9i Collections
Coulam
DML If youre on 9i, theres no question. Most of your DML should use records and collections of record. If youre on 8i, I still find records the only way to go for most every DML statement. Single-column SELECT statements are the only exception.
Collections
Which collection type? There are a number of differences between the three collection types. The key differences to know are:
Nested Tables
Globals available for SQL statements and DDL Order not preserved in the store table. Sequential (1..N), but may become sparse. Initialized by explicit or implicit constructor. Can increase in size via EXTEND. Use DML from SQL or PL/SQL to operate on whole lists, or individual elements.
Associative Arrays
May not be used in SQL statements or DDL No order imposed unless loaded with a fetch. Index can be negative and arbitrary. May be created sparsely or become sparse. Initialized on declaration. Useable immediately. Can increase in size by assignment using new key N/A
Varrays
Globals available for SQL statements and DDL Order preserved in the row or out of line LOB store . Sequential (1..N). May not DELETE individual elements. Initialized by explicit or implicit constructor. Cannot increase in size beyond defined maxsize. Can only use DML from PL/SQL to operate on the entire varray, NOT individual elements.
Unless filling them from a SQL statement which handles the initialization and EXTEND calls for you.
www.rmoug.org
8i and 9i Collections
Coulam
Bi-Directional Access Certain customized, in-house PL/SQL algorithms could benefit from the ability to repeatedly go back and forth over a static set of keys and values. I recall writing one such procedure that for every value in a query, had to find that value in an array, then search backwards and forwards, deleting certain elements, and searching for other values and how far apart they were in relation to it. Maybe a programming god could have written that in SQL, but I doubt it. If you have a similar need, then collections are your ticket.
Collections
When working with collections, youre either dealing with the whole collection at once (nesting) or individual entries (unnesting). You use the TABLE, CURSOR, CAST, and MULTISET expressions to nest and unnest. As you look at the following code, think of unnesting the same as you would joining to a child table. It makes the TABLE() expression a little easier to digest. Refer to the presentation for more complete examples.
CREATE OR REPLACE TYPE tnt_str AS TABLE OF VARCHAR2(60); CREATE OR REPLACE TYPE tnt_num AS TABLE OF NUMBER; CREATE TABLE soul (soul_id NUMBER PRIMARY KEY, soul_nm VARCHAR2(60),angel_of_death VARCHAR2(60), bodies tnt_str) NESTED TABLE bodies STORE AS soul_bodies_nt; --STORE AS required for every nested table col/attribute DECLARE lr_soul soul%ROWTYPE; lnt_bodies tnt_str := tnt_str('Lungfish','Human'); --declare and initialize with values lnt_bodies2 tnt_str := tnt_str(); --declare and just initialize TYPE tant IS TABLE OF tnt_str INDEX BY PLS_INTEGER; /*9i ONLY!*/ --multi-level collection type lant_all_bodies tant; --multi-level collection variable CURSOR cur_similar(int_tab1 IN tnt_str,int_tab2 IN tnt_str) IS SELECT o.column_value val FROM TABLE(CAST(int_tab1 AS tnt_str)) o INTERSECT SELECT n.column_value val FROM TABLE(CAST(int_tab2 AS tnt_str)) n; BEGIN lnt_bodies2.EXTEND(12); lnt_bodies2 := tnt_str( 'Housefly','Rabbit','Newt','Frog','Oyster','Cow','Fish','Flea','Ant','Human', 'Bowl of Petunias','Bat'); --can initiliae like this, or one at a time, or in a loop -- whole nested table inserts INSERT INTO soul VALUES (1,'Beldar','Afkhan',tnt_str('Catfish','Conehead')); --initialized here INSERT INTO soul VALUES (2,'Agrajag','Arthur Dent',lnt_bodies2); INSERT INTO soul VALUES (3,'Bill','TBD',lnt_bodies); -- single nested table entry insert INSERT INTO TABLE(SELECT bodies FROM soul WHERE soul_nm = 'Beldar') VALUES(Eel); lnt_bodies.DELETE; lnt_bodies2.DELETE; --reinitialized -- whole nested table updates UPDATE soul SET bodies = tnt_str(Newt,'Baboon','Conehead') WHERE soul_id = 1; UPDATE soul SET bodies = tnt_str() WHERE soul_nm = 'Bill'; -- single nested table update UPDATE TABLE(SELECT bodies FROM soul WHERE soul_nm = 'Agrajag') b SET b.COLUMN_VALUE = 'Bat-like Atrocity' WHERE b.COLUMN_VALUE = 'Bat'; DELETE TABLE(SELECT bodies FROM soul WHERE soul_nm = 'Bill') b WHERE b.COLUMN_VALUE != 'Human'; -- delete of single entry in a nested table SELECT bodies INTO lnt_bodies FROM soul WHERE soul_id = 1; --direct select SELECT bodies INTO lnt_bodies FROM soul WHERE soul_id = 2;
www.rmoug.org
8i and 9i Collections
Coulam
OPEN cur_similar(lnt_bodies, lnt_bodies2); FETCH cur_similar BULK COLLECT INTO lnt_bodies; --direct select CLOSE cur_similar; SELECT bodies BULK COLLECT INTO lant_all_bodies FROM soul; /*9i ONLY!*/ SELECT * INTO lr_soul FROM soul WHERE soul_id = 2; FOR i in 1..lr_soul.bodies.COUNT LOOP msg.p(lr_soul.bodies(i)); --access to unnamed column in parent record END LOOP; EXCEPTION WHEN OTHERS THEN IF (cur_similar%ISOPEN) THEN CLOSE cur_similar; END IF; RAISE; END; DECLARE lan tnt_num := tnt_num(1,1,2,3,4,5,6,6,6,7,8,9); BEGIN FOR lr IN (SELECT a.COLUMN_VALUE MY_id, COUNT(*) id_count FROM TABLE(SELECT CAST(lan AS tnt_num) FROM dual) a GROUP BY a. COLUMN_VALUE --added group by and order by for variety ORDER BY 1) LOOP msg.p(TO_CHAR(lr.MY_id), TO_CHAR(lr.id_count)); END LOOP; END;
Records
The ability to do record-based inserts and updates has been sorely needed since 7.3. Its so wonderful, I feel like a stock trader thats been given a chance to travel back in time to 1994. The only significant restrictions are: The number and type of fields in the record must equal the number and type of columns listed in the INTO clause. It is not supported in Dynamic SQL. The record you feed the insert/update may not be a function that returns a record. Insert
INSERT INTO accounts VALUES ir_account; --that saved me about 40 lines of code over 8i
Update
UPDATE accounts SET ROW = tr_account; --again, 40 lines saved over same routine in 8i
where rec_array is your collection of records, cursor_var could be a host cursor as well, lower_bound and upper_bound are usually rec_array.FIRST..rec_array.LAST, and table_ref could mean a table, view, synonym, even a nested table returned by the TABLE operator.
Tips
Always use aliases for the tables and nested tables when unnesting in SQL queries. Prevents known bugs.
www.rmoug.org
8i and 9i Collections
Coulam
Since the store table for a nested table column is a child table, treat it as such. For example, when retrieving rows that might not/dont have values in the collection, use the outer join. Thats not intuitive unless you think of the nested table column as the child table that it really is. Always use coll.FIRST..coll.LAST when writing FORALL statements and FOR loops, not 1..coll.COUNT. Always check your collection first to see if it has any values before working on it (col.COUNT > 0). If the consumer of the table containing the nested table doesnt need to see the nested table data very often, and the nested table takes a large amount of resources to transmit to the data consumer, use RETURN AS LOCATOR which just returns a pointer to the data. When needed, the pointer can be dereferenced to get the nested table data. The keyword COLUMN_VALUE is provided as the pseudonym of the column in a simple collection. Create an alias for this column when unnesting. Index associative arrays using PLS_INTEGER (for numeric) and %TYPE (tying it to column or subtype definition).
Tricks
When viewing the structure of a table in SQL*Plus, use SET DESCRIBE DEPTH ALL to allow DESC[RIBE] to show all the columns in the composite column types. Unsupported, undocumented /*+ NESTED_TABLE_GET_REFS */ hint to directly query and manipulate a nested store table.
Traps
Cant use RETURNING with INSERT, despite what the docs say. Prior to 9.2, ORA-22863 if you reference a synonym to a collection type created in another schema. Bug 1879471. Be careful doing SQL against PL/SQL collections, often used with the IN and NOT IN operator. If any member of NOT IN list is NULL, the result in NULL. No error. No result set for you! Outside PL/SQL, support for querying and updating collections is minimal. If your software is two-tier, directly connecting to and accessing tables, you may very well break your applications by introducing columns that arent single values11. If you do have the proper isolation layers in place, youll need to augment the data layer to unnest and convert collections into a data structure supported by your DB access technology. PL/SQL objects and global collections cannot have an attribute whose type is RECORD. Record-based DML cannot be done using nested records, functions that return a record type, or dynamic SQL. You may not do bulk RETURNING * into a collection of records, yet. Must list out all columns. May not bulk collect into associative arrays indexed by strings.
References
Expert One-on-One Oracle, Thomas Kyte, 2001 Oracle PL/SQL Programming, 3rd Ed, Steven Feuerstein and Bill Pribyl, 2002
11
That could be a good thing. You could convince management to buy or build an n-tier application framework that isnt so fragile.
www.rmoug.org