Chapter 12. Collections

A collection is a data structure that acts like a list or a single-dimensional array. Collections are, in fact, the closest you can get to traditional arrays in the PL/SQL language. This chapter will help you decide which of the three different types of collections (associative array, nested table, and VARRAY) best fit your program requirements and show you how to define and manipulate those structures.

Here are some of the ways I’ve found collections handy:

Keep track of lists

Most generally, I use collections to keep track of lists of data elements within my programs. Yes, you could use relational tables or global temporary tables or delimited strings, but collections are very efficient structures that can be manipulated with very clean, maintainable code.

Emulate bidirectional or random-access cursors

PL/SQL only allows you to fetch forward through a cursor’s result set. But if I load the result set of a cursor into a collection, I can move back and forth through that set and can instantly (and repetitively) access any particular row in the set.

Improve performance of lookups

Performance improves when you store lists of subordinate information directly in the column of a table (as a nested table or VARRAY), rather than normalizing that data into a separate relational table. (Nested tables, VARRAYs, and associative arrays are collection types described in the upcoming section “Types of Collections.”)

Cache database information

Collections are appropriate for caching database information that is static and frequently queried in a single session (or simply queried repeatedly in a single program) to speed up performance of those queries.

I have noticed over the years that relatively few developers know about and use collections. This always comes as a surprise, because I find them to be so handy. A primary reason for this limited usage is that collections are relatively complicated. Three different types of collections, multiple steps involved in defining and using them, usage in both PL/SQL programs and database objects, more complex syntax than simply working with individual variables: all of these factors conspire to limit usage of collections.

I have organized this chapter to be comprehensive in my treatment of collections, avoid redundancy in treatment of similar topics across different collection types, and offer guidance in your usage of collections. The resulting chapter is rather long, but I’m confident you will get lots out of it. Here is a quick guide to the remainder of its contents:

Collections overview

I start by providing an introduction to collections and some orientation: a description of the different types, an explanation of the terminology specific to collections, a robust example of each type of collection, and guidelines for deciding which type of collection to use. If you read no further than this section, you will likely be able to start writing some basic collection logic. I strongly suggest, however, that you do read more than this section!

Collection methods

Next, I explore the many methods (procedures and functions) that Oracle provides to help you examine and manipulate the contents of a collection. Virtually every usage of collections requires usage of these methods, so you want to make sure you are comfortable with what they do, and how they work.

Working with collections

Now it is time to build on all those “preliminaries” to explore some of the nuances of working with collections, including the initialization process necessary for nested tables and VARRAYs, different ways to populate and access collection data, and the manipulation of collection columns through the SQL language.

Nested table multiset operations

Oracle Database 10g Release 1 added a number of MULTISET operators that give us the ability to manipulate the contents of nested tables as sets (union, intersection, minus, etc.). You can also compare two nested tables for equality.

Maintaining schema-level collections

You can define nested table and VARRAY types within the database itself. Oracle provides a number of data dictionary views you can use to maintain those types.

Collections Overview

We will start with a description of the different types of collections and a number of examples to get you started.

Types of Collections

Oracle supports three different types of collections. While these different types have much in common, they also each have their own particular characteristics. Many of the terms mentioned in the definitions below are further explained in the “Collection Concepts and Terminology” section, immediately following.

Associative arrays

These are single-dimensional, unbounded, sparse collections of homogeneous elements that are available only in PL/SQL. They were called PL/SQL tables in PL/SQL 2 and index-by tables in Oracle8 Database and Oracle8i Database (because when you declare such a collection, you explicitly state that they are “indexed by” the row number). In Oracle9i Database Release 1, the name was changed to associative arrays . The motivation for the name change was that starting with that release, the INDEX BY syntax could be used to “associate” or index contents by VARCHAR2 or PLS_INTEGER.

Nested tables

These are also single-dimensional, unbounded collections of homogeneous elements. They are initially dense but can become sparse through deletions. Nested tables can be defined in both PL/SQL and the database (for example, as a column in a table). Nested tables are multisets, which means that there is no inherent order to the elements in a nested table.

VARRAYs

Like the other two collection types, VARRAYs (variable-sized arrays) are also single-dimensional collections of homogeneous elements. However, they are always bounded and never sparse. When you define a type of VARRAY, you must also specify the maximum number of elements it can contain. Like nested tables , they can be used in PL/SQL and in the database. Unlike nested tables, when you store and retrieve a VARRAY, its element order is preserved.

Collections Concepts and Terminology

The following explanations will help you understand collections and more rapidly establish a comfort level with these data structures.

Collection type

Each collection variable in your program must be declared based on a pre-defined collection type. As I mentioned earlier, there are, very generally, three types of collections: associative arrays, nested tables, and VARRAYs. Within those generic types, there are specific types that you define with a TYPE statement in a block’s declaration section. You can then declare and use instances of those types in your programs.

Collection or collection instance

The term “collection” may refer to any of the following:

  • A PL/SQL variable of type associative array, nested table, or VARRAY

  • A table column of type nested table or VARRAY

Regardless of the particular type or usage, however, a collection is at its core a single-dimensional list of homogeneous elements.

A collection instance is an instance of a particular type of collection.

Partly due to the syntax and names Oracle has chosen to support collections, you will also find them referred to as arrays and tables.

Homogeneous elements

The datatype of each row in a collection is the same; thus, its elements are homogeneous. This datatype is defined by the type of collection used to declare the collection itself. This datatype can, however, be a composite or complex datatype itself; you can declare a table of records, for example. And starting in Oracle9i Database Release 1, you can even define multilevel collections, in which the datatype of one collection is itself a collection type, or a record or object whose attribute contains a collection.

One-dimensional or single-dimensional

A PL/SQL collection always has just a single column of information in each row, and is in this way similar to a one-dimensional array. You cannot define a collection so that it can be referenced as follows:

    my_collection (10, 44)

This is a two-dimensional structure and not currently supported with that traditional syntax. Instead, you can create multidimensional arrays by declaring collections of collections, in which case the syntax you use will be something like this:

    my_collection (44) (10)
Unbounded versus bounded

A collection is said to be bounded if there are predetermined limits to the possible values for row numbers in that collection. It is unbounded if there are no upper or lower limits on those row numbers. VARRAYs or variable-sized arrays are always bounded; when you define them, you specify the maximum number of rows allowed in that collection (the first row number is always 1). Nested tables and associative arrays are only theoretically bounded. We describe them as unbounded, because from a theoretical standpoint, there is no limit to the number of rows you can define in them.

Sparse versus dense

A collection (or array or list) is called dense if all rows between the first and last row are defined and given a value (including NULL). A collection is sparse if rows are not defined and populated sequentially; instead, there are gaps between defined rows, as demonstrated in the associative array example in the next section. VARRAYs are always dense. Nested tables always start as dense collections but can be made sparse. Associative arrays can be sparse or dense, depending on how you fill the collection.

Sparseness is a very valuable feature, as it gives you the flexibility to populate rows in a collection using a primary key or other intelligent key data as the row number. By doing so, you can define an order on the data in a collection or greatly enhance the performance of lookups.

Indexed by integers

All collections support the ability to reference a row via the row number, an integer value. The associative array TYPE declaration makes that explicit with its INDEX BY clause, but the same rule holds true for the other collection types.

Indexed by strings

Starting with Oracle9i Database Release 2, it is possible to index an associative array by string values (currently up to 32K in length) instead of by numeric row numbers. This feature is not available for nested tables or VARRAYs.

Outer table

This refers to the enclosing table in which you have used a nested table or VARRAY as a column’s datatype.

Inner table

This is the enclosed collection that is implemented as a column in a table; it is also known as a nested table column.

Store table

This is the physical table that Oracle creates to hold values of the inner table (a nested table column).

Collection Examples

This section provides relatively simple examples of each different type of collection with explanations of the major characteristics.

Using an associative array

In the following example, I declare an associative array type and then a collection based on that type. I populate it with four rows of data and then iterate through the collection, displaying the strings in the collection. A more thorough explanation appears after the code.

      1  DECLARE
      2     TYPE list_of_names_t IS TABLE OF person.first_name%TYPE
      3        INDEX BY PLS_INTEGER;
      4     happyfamily   list_of_names_t;
      5     l_row PLS_INTEGER;
      6  BEGIN
      7     happyfamily (2020202020) := 'Eli';
      8     happyfamily (-15070) := 'Steven';
      9     happyfamily (-90900) := 'Chris';
     10     happyfamily (88) := 'Veva';
     11
     12     l_row := happyfamily.FIRST;
     13
     14     WHILE (l_row IS NOT NULL)
     15     LOOP
     16        DBMS_OUTPUT.put_line (happyfamily (l_row));
     17        l_row := happyfamily.NEXT (l_row);
     18     END LOOP;
     19* END;
    SQL> /
    Chris
    Steven
    Veva
    Eli

Line(s)

Description

2-3

Declare the associative array TYPE, with its distinctive INDEX BY clause. A collection based on this type contains a list of strings, each of which can be as long as the first_name column in the person table.

4

Declare the happyfamily collection from the list_of_names_t type.

9 -10

Populate the collection with four names. Notice that I can use virtually any integer value that I like. The row numbers don’t have to be sequential in an associative array; they can even be negative!

12

Call the FIRST method (a function that is “attached” to the collection) to get the first or lowest defined row number in the collection.

14-18

Use a WHILE loop to iterate through the contents of the collection, displaying each row. Line 17 show the NEXT method, which is used to move from the current defined row to the next defined row, “skipping over” any gaps.

Using a nested table

In the following example, I first declare a nested table type as a schema-level type. In my PL/SQL block, I declare three nested tables based on that type. I put the names of everyone in my family into the happyfamily nested table. I put the names of my children in the children nested table. I then use the Oracle Database 10g set operator, MULTISET EXCEPT, to extract just the parents from the happyfamily nested table; finally, I display the names of the parents. A more thorough explanation appears after the code.

    REM Section A
    SQL> CREATE TYPE list_of_names_t IS TABLE OF VARCHAR2 (100);
      2  /
    Type created.
     
    REM Section B
    SQL>
      1  DECLARE
      2     happyfamily   list_of_names_t := list_of_names_t ();
      3     children      list_of_names_t := list_of_names_t ();
      4     parents       list_of_names_t := list_of_names_t ();
      5  BEGIN
      6     happyfamily.EXTEND (4);
      7     happyfamily (1) := 'Eli';
      8     happyfamily (2) := 'Steven';
      9     happyfamily (3) := 'Chris';
     10     happyfamily (4) := 'Veva';
     11
     12     children.EXTEND;
     13     children (1) := 'Chris';
     14     children.EXTEND;
     15     children (2) := 'Eli';
     16
     17     parents := happyfamily MULTISET EXCEPT children;
     18
     19     FOR l_row IN parents.FIRST .. parents.LAST
     20     LOOP
     21        DBMS_OUTPUT.put_line (parents (l_row));
     22     END LOOP;
     23* END;
    SQL> /
    Steven
    Veva

Line(s)

Description

Section A

The CREATE TYPE statement creates a nested table type in the database itself. By taking this approach, I can declare nested tables in any PL/SQL block that has SELECT authority on the type. I can also declare columns in relational tables of this type.

2-4

Declare three different nested tables based on the schema-level type. Notice that in each case I also call a constructor function to initialize the nested table. This function always has the same name as the type and is created for us by Oracle. You must initialize a nested table before it can be used.

6

Call the EXTEND method to “make room” in my nested table for the members of my family. Here, in contrast to associative arrays, I must explicitly ask for a row in a nested table before I can place a value in that row.

7-10

Populate the happyfamily collection with our names.

12-15

Populate the children collection. In this case, I extend a single row at a time.

17

To obtain the parents in this family, I simply take the children out of the happyfamily. This is transparently easy to do in releases from Oracle Database 10g onwards, where we have high-level set operators like MULTISET EXCEPT (very similar to the SQL MINUS).

19-22

Because I know that my parents collection is densely filled from the MULTISET EXCEPT operation, I can use the numeric FOR loop to iterate through the contents of the collection. This construct will raise a NO_DATA_FOUND exception if used with a sparse collection.

Using a VARRAY

In the following example, I demonstrate the use of VARRAYs as columns in a relational table. First, I declare two different schema-level VARRAY types. I then create a relational table, family, that has two VARRAY columns. Finally, in my PL/SQL code, I populate two local collections and then use them in an INSERT into the family table. A more thorough explanation appears after the code.

    REM Section A
    SQL> CREATE TYPE first_names_t IS VARRAY (2) OF VARCHAR2 (100);
      2  /
    Type created.
     
    SQL> CREATE TYPE child_names_t IS VARRAY (1) OF VARCHAR2 (100);
      2  /
    Type created.
     
    REM Section B
    SQL> CREATE TABLE family (
      2     surname VARCHAR2(1000)
      3   , parent_names first_names_t
      4   , children_names child_names_t
      5   );
     
    Table created.
     
    REM Section C
    SQL>
      1  DECLARE
      2     parents    first_names_t := first_names_t ();
      3     children   child_names_t := child_names_t ();
      4  BEGIN
      5     parents.EXTEND (2);
      6     parents (1) := 'Samuel';
      7     parents (2) := 'Charina';
      8     --
      9     children.EXTEND;
     10     children (1) := 'Feather';
     11
     12     --
     13     INSERT INTO family
     14                 (surname, parent_names, children_names
     15                 )
     16          VALUES ('Assurty', parents, children
     17                 );
     18  END;
    SQL> /
     
    PL/SQL procedure successfully completed.
     
    SQL> SELECT * FROM family
      2  /
     
    SURNAME
    PARENT_NAMES
    CHILDREN_NAMES
    --------------------------------------------
    Assurty
    FIRST_NAMES_T('Samuel', 'Charina')
    CHILD_NAMES_T('Feather')

Line(s)

Description

Section A

Use CREATE TYPE statements to declare two different VARRAY types. Notice that with a VARRAY, I must specify the maximum length of the collection. Thus, my declarations in essence dictate a form of social policy: you can have at most two parents and at most one child.

Section B

Create a relational table, with three columns: a VARCHAR2 column for the surname of the family and two VARRAY columns, one for the parents and another for the children.

Section C, lines 2-3

Declare two local VARRAYs based on the schema-level type. As with nested tables (and unlike with associative arrays), I must call the constructor function of the same name as the TYPE to initialize the structures.

5 -10

Extend and populate the collections with the names of parents and then the single child. If I try to extend to a second row, Oracle will raise the ORA-06532: Subscript outside of limit error.

13-17

Insert a row into the family table, simply providing the VARRAYs in the list of values for the table. Oracle certainly makes it easy for us to insert collections into a relational table!

Where You Can Use Collections

The following sections describe the different places in your code where a collection can be declared and used. Because a collection type can be defined in the database itself (nested tables and VARRAYs only), you can find collections not only in PL/SQL programs but also inside tables and object types.

Collections as components of a record

Using a collection type in a record is similar to using any other type. You can use associative arrays, nested tables, VARRAYs, or any combination thereof in RECORD datatypes. For example:

    DECLARE
       TYPE toy_rec_t IS RECORD (
          manufacturer INTEGER,
          shipping_weight_kg NUMBER,
          domestic_colors Color_array_t,
          international_colors Color_tab_t
       );

Collections as program parameters

Collections can also serve as parameters in functions and procedures. The format for the parameter declaration is the same as with any other:

parameter_name [ IN | IN OUT | OUT ] parameter_type
       [ DEFAULT | := <default_value> ]

PL/SQL does not offer any predefined collection types. This means that before you can pass a collection as an argument, you must have already defined the collection type that will serve as the parameter type. You can do this by:

  • Defining a schema-level type with CREATE TYPE

  • Declaring the collection type in a package specification

  • Declaring that type in an outer scope from the definition of the module

Here is an example of using a schema-level type:

    CREATE TYPE yes_no_t IS TABLE OF CHAR(1);
    /
    CREATE OR REPLACE PROCEDURE act_on_flags (flags_in IN yes_no_t)
    IS
    BEGIN
        ...
    END act_on_flags;
    /

Here is an example of using a collection type defined in a package specification: there is only one way to declare an associative array of Booleans (and all other base datatypes), so why not define them once in a package specification and reference them throughout my application?

    /* File on web: aa_types.pks */
    CREATE OR REPLACE PACKAGE aa_types
    IS
       TYPE boolean_aat IS TABLE OF BOOLEAN
        INDEX BY BINARY_INTEGER;
       ...
    END aa_types;
    /

Notice that when I reference the collection type in my parameter list, I must qualify it with the package name:

    CREATE OR REPLACE PROCEDURE act_on_flags (
       flags_in IN aa_types.boolean_aat)
    IS
    BEGIN
        ...
    END act_on_flags;
    /

Finally, here is an example of declaring a collection type in an outer block and then using it in an inner block:

    DECLARE
       TYPE birthdates_aat IS VARRAY (10) OF DATE;
       l_dates   birthdates_aat := birthdates_aat ();
    BEGIN
       l_dates.EXTEND (1);
       l_dates (1) := SYSDATE;
     
       DECLARE
          FUNCTION earliest_birthdate (list_in IN birthdates_aat)
             RETURN DATE
          IS
          BEGIN
            ...
          END earliest_birthdate;
       BEGIN
          DBMS_OUTPUT.put_line (earliest_birthdate (l_dates));
       END;
    END;

Collections as datatypes of a function’s return value

In the next example, we have defined Color_tab_t as the type of a function return value, and also used it as the datatype of a local variable. The same restriction about scope applies to this usage: types must be declared outside the module’s scope.

    CREATE FUNCTION true_colors (whose_id IN NUMBER) RETURN Color_tab_t
    AS
       l_colors Color_tab_t;
    BEGIN
       SELECT favorite_colors INTO l_colors
         FROM personality_inventory
        WHERE person_id = whose_id;
       RETURN l_colors;
    EXCEPTION
       WHEN NO_DATA_FOUND
       THEN
          RETURN NULL;
    END;

How would you use this function in a PL/SQL program? Because it acts in the place of a variable of type Color_tab_t, you can do one of two things with the returned data:

  1. Assign the entire result to a collection variable.

  2. Assign a single element of the result to a variable (as long as the variable is of a type compatible with the collection’s elements).

Option #1 is easy. Notice, by the way, that this is another circumstance where you don’t have to initialize the collection variable explicitly:

    DECLARE
       color_array Color_tab_t;
    BEGIN
       color_array := true_colors (8041);
    END;

With Option #2, we actually give the function call a subscript. The general form is:

variable_of_element_type := function() (subscript);

Or, in the case of the true_colors function:

    DECLARE
       one_of_my_favorite_colors VARCHAR2(30);
    BEGIN
       one_of_my_favorite_colors := true_colors (whose_id=>8041) (1);
    END;

Note that this code has a small problem: if there is no record in the database table where person_id is 8041, the attempt to read its first element will raise a COLLECTION_IS_NULL exception. We should trap and deal with this exception in a way that makes sense to the application.

In the previous example, I’ve used named parameter notation (whose_id=>) for readability, although it is not strictly required. (See Chapter 17 for more details.)

Collection as “columns” in a database table

Using a nested table or VARRAY, you can store and retrieve nonatomic data in a single column of a table. For example, the employee table used by the HR department could store the date of birth for each employee’s dependents in a single column, as shown in Table 12-1.

Table 12-1. Storing a column of dependents as a collection in a table of employees

Id (NUMBER)

Name (VARCHAR2)

Dependents_ages (Dependent_birthdate_t)

10010

Zaphod Beeblebrox

12-JAN-1763

  

4-JUL-1977

  

22-MAR-2021

10020

Molly Squiggly

15-NOV-1968

  

15-NOV-1968

10030

Joseph Josephs

 

10040

Cepheus Usrbin

27-JUN-1995

  

9-AUG-1996

  

19-JUN-1997

10050

Deirdre Quattlebaum

21-SEP-1997

It’s not terribly difficult to create such a table. First we define the collection type:

    CREATE TYPE Dependent_birthdate_t AS VARRAY(10) OF DATE;

Now we can use it in the table definition:

    CREATE TABLE employees (
       id NUMBER,
       name VARCHAR2(50),
       ...other columns...,
       Dependents_ages Dependent_birthdate_t
    );

We can populate this table using the following INSERT syntax, which relies on the type’s default constructor to transform a list of dates into values of the proper datatype:

    INSERT INTO employees VALUES (42, 'Zaphod Beeblebrox', ...,
       Dependent_birthdate_t( '12-JAN-1765', '4-JUL-1977', '22-MAR-2021'));

Now let’s look at an example of a nested table datatype as a column. When we create the outer table personality_inventory, we must tell Oracle what we want to call the “store table.”

    CREATE TABLE personality_inventory (
       person_id NUMBER,
       favorite_colors Color_tab_t,
       date_tested DATE,
       test_results BLOB)
    NESTED TABLE favorite_colors STORE AS favorite_colors_st;

The NESTED TABLE ... STORE AS clause tells Oracle that we want the store table for the favorite_colors column to be called favorite_colors_st. There is no preset limit on how large this store table, which is located “out of line” (or separate from the rest of that row’s data to accommodate growth) can grow.

You cannot directly manipulate data in the store table, and any attempt to retrieve or store data directly into favorite_colors_st will generate an error. The only path by which you can read or write the store table’s attributes is via the outer table. (See the discussion of collection pseudo-functions in the later section, "Working with Collections in SQL,” for a few examples of doing so.) You cannot even specify storage parameters for the store table; it inherits the physical attributes of its outermost table.

One chief difference between nested tables and VARRAYs surfaces when we use them as column datatypes . Although using a VARRAY as a column’s datatype can achieve much the same result as a nested table, VARRAY data must be predeclared to be of a maximum size, and is actually stored “inline” with the rest of the table’s data. For this reason, Oracle Corporation says that VARRAY columns are intended for “small” arrays, and that nested tables are appropriate for “large” arrays.

Collections as attributes of an object type

In this example, we are modeling automobile specifications. Each Auto_spec_t object will include a list of manufacturer’s colors in which you can purchase the vehicle.

    CREATE TYPE Auto_spec_t AS OBJECT (
       make VARCHAR2(30),
       model VARCHAR2(30),
       available_colors Color_tab_t
    );

Because there is no data storage required for the object type, it is not necessary to designate a name for the companion table at the time we issue the CREATE TYPE ... AS OBJECT statement.

When the time comes to implement the type as, say, an object table, you could do this:

    CREATE TABLE auto_specs OF Auto_spec_t
       NESTED TABLE available_colors STORE AS available_colors_st;

This statement requires a bit of explanation. When you create a “table of objects,” Oracle looks at the object type definition to determine what columns you want. When it discovers that one of the object type’s attributes, available_colors, is in fact a nested table, Oracle treats this table as it did in earlier examples; in other words, it wants to know what to name the store table. So the phrase:

    ...NESTED TABLE available_colors STORE AS available_colors_st

says that you want the available_colors column to have a store table named available_colors_st.

See Chapter 25 for more information about Oracle object types.

Choosing a Collection Type

Which collection type makes sense for your application? In some cases, the choice is obvious. In others, there may be several acceptable choices. This section provides some guidance. Table 12-2 illustrates many of the differences between associative arrays, nested tables, and VARRAYs.

As a PL/SQL developer, I find myself leaning toward using associative arrays as a first instinct. Why is this? They involve the least amount of coding. You don’t have to initialize or extend them. They have historically been the most efficient collection type (although this distinction will probably fade over time). However, if you want to store your collection within a database table, you cannot use an associative array. The question then becomes: nested table or VARRAY?

The following guidelines will help you make your choice; we recommend, however, that you read the rest of the chapter first if you are not very familiar with collections already.

  • If you need sparse associative arrays (for example, for “data-smart” storage), your only practical option is an associative array. True, you could allocate and then delete elements of a nested table variable (as illustrated in the later section on NEXT and PRIOR methods), but it is inefficient to do so for anything but the smallest collections.

  • If your PL/SQL application requires negative subscripts, you also have to use associative arrays.

  • If you are running Oracle Database 10g and would find it useful to perform high-level set operations on your collections, choose nested tables over associative arrays.

  • If you want to enforce a limit to the number of rows stored in a collection, use VARRAYs.

  • If you intend to store large amounts of persistent data in a column collection, your only option is a nested table. Oracle will then use a separate table behind the scenes to hold the collection data, so you can allow for almost limitless growth.

  • If you want to preserve the order of elements stored in the collection column and if your dataset will be small, use a VARRAY. What is “small?” I tend to think in terms of how much data you can fit into a single database block; if you span blocks, you get row chaining, which decreases performance. The database block size is established at database creation time and is typically 2K, 4K, or 8K.

  • Here are some other indications that a VARRAY would be appropriate: you don’t want to worry about deletions occurring in the middle of the data set; your data has an intrinsic upper bound; or you expect, in general, to retrieve the entire collection simultaneously.

Table 12-2. Comparing Oracle collection types

Characteristic

Associative array

Nested table

VARRAY

Dimensionality

Single

Single

Single

Usable in SQL?

No

Yes

Yes

Usable as column datatype in a table?

No

Yes; data stored “out of line” (in separate table)

Yes; data stored “in line” (in same table)

Uninitialized state

Empty (cannot be null); elements undefined

Atomically null; illegal to reference elements

Atomically null; illegal to reference elements

Initialization

Automatic, when declared

Via constructor, fetch, assignment

Via constructor, fetch, assignment

In PL/SQL, elements

   

referenced via

BINARY_INTEGER (-2,147,483,647 .. 2,147,483,647)

VARCHAR2 (Oracle9i Database Release 2 and above)

Positive integer between 1 and 2,147,483,647

Positive integer between 1 and 2,147,483,647

Sparse?

Yes

Initially, no; after deletions, yes

No

Bounded?

No

Can be extended

Yes

Can assign value to any

   

element at any time?

Yes

No; may need to EXTEND first

No; may need to EXTEND first, and cannot EXTEND past upper bound

Means of extending

Assign value to element with a new subscript

Use built-in EXTEND procedure (or TRIM to condense), with no predefined

 

maximum

EXTEND (or TRIM), but only up to declared maximum size

  

Can be compared for equality?

No

Yes, in Oracle Database 10g

No

Can be manipulated with set operators

No

Yes, in Oracle Database 10g

No

Retains ordering and subscripts when stored in and retrieved from database?

N/A

No

Yes

Collection Methods (Built-Ins)

PL/SQL offers a number of built-in functions and procedures, known as collection methods, that let you obtain information about and modify the contents of collections. Table 12-3 contains the complete list of these programs.

Table 12-3. Collection methods

Method (function or procedure)

Description

COUNT function

Returns the current number of elements in a collection.

DELETE procedure

Removes one or more elements from the collection. Reduces COUNT if the element is not already DELETEd. With VARRAYS, you can delete only the entire contents of the collection.

EXISTS function

Returns TRUE or FALSE to indicate whether the specified element exists.

EXTEND procedure

Increases the number of elements in a nested table or VARRAY. Increases COUNT.

FIRST, LAST

Returns the smallest (FIRST) and largest (LAST) subscripts in use.

functions

 

LIMIT function

Returns the maximum number of elements allowed in a VARRAY.

PRIOR, NEXT

Returns the subscript immediately before (PRIOR) or after (NEXT) a specified subscript. You should always use PRIOR and NEXT to traverse a collection, especially if you are working with sparse (or potentially sparse) collections.

functions

 

TRIM procedure

Removes collection elements from the end of the collection (highest defined subscript). Reduces COUNT if elements are not DELETEd.

These programs are referred to as methods because the syntax for using the collection built-ins is different from the normal syntax used to call procedures and functions. Collection methods employ a member method syntax that’s common in object-oriented languages such as C++.

To give you a feel for member-method syntax, consider the LAST function. It returns the greatest index value in use in the associative array. Using standard function syntax, you might expect to call LAST as follows:

    IF LAST (company_table) > 10 THEN ... /* Invalid syntax */

In other words, you’d pass the associative array as an argument. In contrast, by using the member-method syntax, the LAST function is a method that “belongs to” the object—in this case, the associative array. So the correct syntax for using LAST is:

    IF company_table.LAST > 10 THEN ... /* Correct syntax */

The general syntax for calling these associative array built-ins is either of the following:

  • An operation that takes no arguments:

    table_name.operation
  • An operation that takes a row index for an argument:

    table_name.operation(index_number [, index_number])

The following statement, for example, returns TRUE if the 15th row of the company_tab associative array is defined:

    company_tab.EXISTS(15)

The collection methods are not available from within SQL; they can be used only in PL/SQL programs.

The COUNT Method

Use COUNT to compute the number of elements defined in an associative array, nested table, or VARRAY. If elements have been DELETEd or TRIMmed from the collection, they are not included in COUNT.

The specification for COUNT is:

    FUNCTION COUNT RETURN PLS_INTEGER;

Let’s look at an example. Before I do anything with my collection, I verify that it contains some information:

    DECLARE
      volunteer_list volunteer_list_ar := volunteer_list_ar('Steven'),
    BEGIN
       IF volunteer_list.COUNT > 0
       THEN
          assign_tasks (volunteer_list);
       END IF;
    END;

Boundary considerations

If COUNT is applied to an initialized collection with no elements, it returns zero. It also returns zero if it’s applied to an empty associative array.

Exceptions possible

If COUNT is applied to an uninitialized nested table or a VARRAY, it raises the COLLECTION_IS_NULL predefined exception. Note that this exception is not possible for associative arrays, which do not require initialization.

The DELETE Method

Use DELETE to remove one, some, or all elements of an associative array, nested table, or VARRAY. DELETE without arguments removes all of the elements of a collection. DELETE(i) removes the ith element from the nested table or associative array. DELETE(i,j) removes all elements in an inclusive range beginning with i and ending with j. When you use parameters, DELETE actually keeps a placeholder for the “removed” element, and you can later reassign a value to that element.

In physical terms, PL/SQL actually releases the memory only when your program deletes a sufficient number of elements to free an entire page of memory (unless you DELETE all the elements, which frees all the memory immediately). This deallocation happens automatically and requires no accommodations or devices in your code.

Tip

When DELETE is applied to VARRAYs, you can issue DELETE only without arguments (i.e., remove all rows). In other words, you cannot delete individual rows of a VARRAY, possibly making it sparse. The only way to remove a row from a VARRAY is to TRIM from the end of the collection.

The overloaded specification for this method is as follows:

    PROCEDURE DELETE;
    PROCEDURE DELETE (i [BINARY_INTEGER | VARCHAR2(size_limit)]);
    PROCEDURE DELETE (i [BINARY_INTEGER | VARCHAR2(size_limit)],
        j [BINARY_INTEGER | VARCHAR2(size_limit)]);

The following procedure removes everything but the last element in the collection. It actually uses four collection methods: FIRST, to obtain the first defined row; LAST, to obtain the last defined row; PRIOR, to determine the next-to-last row; and DELETE to remove all but the last.

    CREATE PROCEDURE keep_last (the_list IN OUT List_t)
    AS
       first_elt PLS_INTEGER := the_list.FIRST;
       next_to_last_elt PLS_INTEGER := the_list.PRIOR(the_list.LAST);
    BEGIN
       the_list.DELETE(first_elt, next_to_last_elt);
    END;

Here are some additional examples:

  • Delete all the rows from the names table:

        names.DELETE;
  • Delete the 77th row from the globals table:

        globals.DELETE (77);
  • Delete all the rows in the temperature readings table between the 0th row and the -15,000th row, inclusively:

        temp_readings.DELETE (-15000, 0);

Boundary considerations

If i and/or j refer to nonexistent elements, DELETE attempts to “do the right thing” and will not raise an exception. For example, if you have three elements in a TABLE item and DELETE(-5,1), the first element will be deleted. However, DELETE(-5) will do nothing.

Exceptions possible

If DELETE is applied to an uninitialized nested table or a VARRAY, it raises the COLLECTION_ IS_NULL predefined exception.

The EXISTS Method

Use the EXISTS method with nested tables, associative arrays, and VARRAYs to determine if the specified row exists within the collection. It returns TRUE if the element exists, FALSE otherwise. It never returns NULL. If you have used TRIM or DELETE to remove a row that existed previously, EXISTS for that row number returns FALSE.

The specification of this method is:

    FUNCTION EXISTS (i IN [BINARY_INTEGER | VARCHAR2(size_limit)]) RETURN BOOLEAN;

In the following block, I check to see if my row exists, and if so I set it to NULL.

    DECLARE
       my_list Color_tab_t := Color_tab_t();
       element INTEGER := 1;
    BEGIN
       ...
       IF my_list.EXISTS(element)
       THEN
         my_list(element) := NULL;
       END IF;
    END;

Boundary considerations

If EXISTS is applied to an uninitialized (atomically null) nested table or a VARRAY, or an initialized collection with no elements, it simply returns FALSE. You can use EXISTS beyond the COUNT without raising an exception.

Exceptions possible

For integer-indexed collections, if i is not an integer and cannot be converted to an integer, EXISTS will raise the VALUE_ERROR exception. This exception is possible for any collection method that accepts an argument.

The EXTEND Method

Adding an element to a nested table or VARRAY requires a separate allocation step. Making a “slot” in memory for a collection element is independent from assigning a value to it. If you haven’t initialized the collection with a sufficient number of elements (null or otherwise), you must first use the EXTEND procedure on the variable. Do not use EXTEND with associative arrays.

EXTEND appends element(s) to a collection. EXTEND with no arguments appends a single null element. EXTEND(n) appends n null elements. EXTEND(n,i) appends n elements and sets each to the same value as the ith element; this form of EXTEND is required for collections with NOT NULL elements.

Here is the overloaded specification of EXTEND:

    PROCEDURE EXTEND (n PLS_INTEGER:=1);
    PROCEDURE EXTEND (n PLS_INTEGER, i PLS_INTEGER);

In the following example, the push procedure extends my list by a single row and populates it:

    CREATE PROCEDURE push (the_list IN OUT List_t, new_value IN VARCHAR2)
    AS
    BEGIN
       the_list.EXTEND;
       the_list(the_list.LAST) := new_value;
    END;

I can also use EXTEND to add 10 new rows to my list, all with the same value. First I extend a single row and populate explicitly. Then I extend again, this time by 9 rows, and specify the row number with new_value as the initial value for all my new rows.

    CREATE PROCEDURE push_ten (the_list IN OUT List_t, new_value IN VARCHAR2)
    AS
       l_copyfrom PLS_INTEGER;
    BEGIN
       the_list.EXTEND;
       l_copyfrom := the_list.LAST;
       the_list(l_copyfrom) := new_value;
       the_list.EXTEND (9, l_copyfrom);
    END;

Boundary considerations

If you have DELETEd or TRIMmed from the end of a collection, EXTEND will “jump over” (skip) the deleted elements when it assigns a new index. If n is null, EXTEND will do nothing.

Exceptions possible

If EXTEND is applied to an uninitialized nested table or a VARRAY, it raises the COLLECTION_IS_NULL predefined exception. An attempt to EXTEND a VARRAY beyond its declared limit raises the SUBSCRIPT_BEYOND_LIMIT exception.

The FIRST and LAST Methods

Use the FIRST and LAST methods with nested tables, associative arrays, and VARRAYs to return, respectively, the lowest and highest index in use in the collection. The specifications for these functions follow.

    FUNCTION FIRST RETURN PLS_INTEGER;
    FUNCTION LAST RETURN PLS_INTEGER;

For example, the following code scans from the start to the end of my collection:

    FOR indx IN holidays.FIRST .. holidays.LAST
    LOOP
       send_everyone_home (indx);
    END LOOP;

Please remember that this kind of loop will only work (i.e., not raise a NO_DATA_FOUND exception) if the collection is densely populated.

In the next example, I use LAST to concisely specify that I want to add a row onto the end of an associative array. I use a cursor FOR loop to transfer data from the database to an associative array of records. When the first record is fetched, the companies collection is empty, so the LAST operator will return NULL. I then use NVL to produce a starting row of 1.

    FOR company_rec IN company_cur
    LOOP
       companies (NVL (companies.LAST, 0) + 1).company_id :=
          company_rec.company_id;
    END LOOP;

Alternatively, I can rely on COUNT for the same effect—if the collection has been filled sequentially from 1 (if, that is, the COUNT of a collection is equal to the value returned by LAST):

    FOR company_rec IN company_cur
    LOOP
       companies (companies.COUNT + 1).company_id :=
          company_rec.company_id;
    END LOOP;

Boundary considerations

FIRST and LAST return NULL when they are applied to initialized collections that have no elements. For VARRAYs, which have at least one element, FIRST is always 1, and LAST is always equal to COUNT.

Exceptions possible

If FIRST and LAST are applied to an uninitialized nested table or a VARRAY, they raise the COLLECTION_ IS_NULL predefined exception.

The LIMIT Method

Use the LIMIT method to determine the maximum number of elements that can be defined in a VARRAY. This function will return NULL if it is applied to nested tables or to associative arrays. The specification for LIMIT is:

    FUNCTION LIMIT RETURN PLS_INTEGER;

The following conditional expression makes sure that there is still room in my VARRAY before extending:

    IF my_list.LAST < my_list.LIMIT
    THEN
       my_list.EXTEND;
    END IF;

Boundary considerations

There are no boundary considerations for LIMIT.

Exceptions possible

If LIMIT is applied to an uninitialized nested table or a VARRAY, it raises the COLLECTION_ IS_NULL predefined exception.

The PRIOR and NEXT Methods

Use the PRIOR and NEXT methods with nested tables, associative arrays, and VARRAYs to navigate through the contents of a collection. The specifications for these functions are:

    FUNCTION PRIOR (i [BINARY_INTEGER | VARCHAR2(size_limit)])
        RETURN [BINARY_INTEGER | VARCHAR2(size_limit)];
     
    FUNCTION NEXT (i [BINARY_INTEGER | VARCHAR2(size_limit)])
        RETURN [BINARY_INTEGER | VARCHAR2(size_limit)];

PRIOR returns the next-lower index in use relative to i; NEXT returns the next higher. In the following example, this function returns the sum of elements in a List_t collection of numbers:

    CREATE FUNCTION compute_sum (the_list IN List_t) RETURN NUMBER
    AS
       row_index PLS_INTEGER := the_list.FIRST;
       total NUMBER := 0;
    BEGIN
       LOOP
          EXIT WHEN row_index IS NULL;
          total := total + the_list(row_index);
          row_index := the_list.NEXT(row_index);
       END LOOP;
       RETURN total;
    END;

Here is that same program working from the last to the very first defined row in the collection:

    CREATE FUNCTION compute_sum (the_list IN List_t) RETURN NUMBER
    AS
       row_index PLS_INTEGER := the_list.LAST;
       total NUMBER := 0;
    BEGIN
       LOOP
          EXIT WHEN row_index IS NULL;
          total := total + the_list(row_index);
          row_index := the_list.PRIOR(row_index);
       END LOOP;
       RETURN total;
    END;

In this case, it doesn’t matter which direction you move through the collection. In other programs, though, it can make a big difference.

Boundary considerations

If PRIOR and NEXT are applied to initialized collections that have no elements, they return NULL. If i is greater than or equal to COUNT, NEXT returns NULL; if i is less than or equal to FIRST, PRIOR returns NULL.

Tip

Currently, if the collection has elements, and i is greater than COUNT, PRIOR returns LIMIT; if i is less than FIRST, NEXT returns FIRST. However, do not rely on this behavior in future Oracle versions.

Exceptions possible

If PRIOR and NEXT are applied to an uninitialized nested table or a VARRAY, they raise the COLLECTION_ IS_NULL predefined exception.

The TRIM Method

Use TRIM to remove n elements from the end of a nested table or VARRAY. Without arguments, TRIM removes exactly one element. As we’ve already mentioned, confusing behavior occurs if you combine DELETE and TRIM actions on a collection; for example, if an element that you are trimming has previously been DELETEd, TRIM “repeats” the deletion but counts this as part of n, meaning that you may be TRIMming fewer actual elements than you think.

Warning

Attempting to TRIM an associative array will produce a compile-time error.

The specification for TRIM is:

    PROCEDURE TRIM (n PLS_INTEGER:=1);

The following function pops the last value off of a list and returns it to the invoking block. The “pop” action is implemented by trimming the collection by a single row after extracting the value.

    CREATE FUNCTION pop (the_list IN OUT List_t) RETURN VARCHAR2
    AS
       l_value VARCHAR2(30);
    BEGIN
       IF the_list.COUNT >= 1
       THEN
          /* Save the value of the last element in the collection
          || so it can be returned
          */
          l_value := the_list(the_list.LAST);
          the_list.TRIM;
       END IF;
       RETURN l_value;
    END;

Boundary considerations

If n is null, TRIM will do nothing.

Exceptions possible

The TRIM method will raise the SUBSCRIPT_BEYOND_COUNT predefined exception if you attempt to TRIM more elements than actually exist. If TRIM is applied to an uninitialized nested table or a VARRAY, it raises the COLLECTION_IS_NULL predefined exception.

Warning

If you use TRIM and DELETE on the same collection, you can get some very surprising results. Consider this scenario: if you DELETE an element at the end of a nested table variable and then do a TRIM on the same variable, how many elements have you removed? You might think that you have removed two elements, but, in fact, you have removed only one. The placeholder that is left by DELETE is what TRIM acts upon. To avoid confusion, Oracle Corporation recommends using either DELETE or TRIM, but not both, on a given collection.

Working with Collections

You now know about the different types of collections and the collection methods. You have seen some examples of working with associative arrays, nested tables and VARRAYs. Now it is time to dive into the details of manipulating collections in your programs. Topics in this section include:

  • Exception handling with collections

  • Declaring collection types

  • Declaring and initializing collection variables

  • Assigning values to collections

  • Using collections of complex datatypes, such as collections of collections

  • Working with sequential and nonsequential associative arrays

Declaring Collection Types

Before you can work with a collection, you must declare it, and that declaration must be based on a collection type. So the first thing you must learn to do is define a collection type.

There are two ways to create user-defined collection types :

  • You can declare the collection type within a PL/SQL program using the TYPE statement . This collection type will then be available for use within the block in which the TYPE is defined. If the TYPE is defined in a package specification, then it is available to any program whose schema has EXECUTE authority on the package.

  • You can define a nested table type or VARRAY type as a schema-level object within the Oracle database by using the CREATE TYPE command . This TYPE can then be used as the datatype for columns in database tables and attributes of object types, and to declare variables in PL/SQL programs. Any program in a schema with SELECT authority on the TYPE can reference the TYPE.

Declaring an associative array collection type

The TYPE statement for an associative array has the following format:

    TYPEtable_type_name IS TABLE OF datatype [ NOT NULL ]
       INDEX BY index_type;

where table_type_name is the name of the collection you are creating, datatype is the datatype of the single column in the collection, and index_type is the datatype of the index used to organize the contents of the collection. You can optionally specify that the collection be NOT NULL, meaning that every row in the table must have a value.

The datatype of the collection’s single column can be any of the following:

Scalar datatype

Any PL/SQL-supported scalar datatype, such as VARCHAR2, CLOB, POSITIVE, DATE, or BOOLEAN.

Anchored datatype

A datatype inferred from a column, previously defined variable, or cursor expression using the %TYPE attribute. You can also define collections of records with the %ROWTYPE declaration or with a user-defined record type.

Complex datatype

Starting with Oracle9i Database Release 2, you can also use object types and collection types as the datatype of a collection. This means you can nest collections within collections. This topic is covered in more detail in the later section, “Collections of Complex Datatypes.”

The index_type of the collection determines the type of data you can use to specify the location of the data you are placing in the collection. Prior to Oracle9i Database Release 2, the only way you could specify an index for an associative array (a.k.a. index-by table) was:

    INDEX BY BINARY_INTEGER

Starting with Oracle9i Database Release 2, the INDEX BY datatype can be BINARY_INTEGER, any of its subtypes, VARCHAR2(N) or %TYPE against a VARCHAR2 column or variable. In other words, any of the following INDEX BY clauses are now accepted:

    INDEX BY BINARY_INTEGER;
    INDEX BY PLS_INTEGER;
    INDEX BY POSITIVE;
    INDEX BY NATURAL;
    INDEX BY SIGNTYPE;
    INDEX BY VARCHAR2(32767);
    INDEX BYtable.column%TYPE;
    INDEX BY cursor.column%TYPE;
    INDEX BY package.variable%TYPE;
    INDEX BY package.subtype;

The rules for the table type name are the same as for any identifier in PL/SQL: the name can be up to 30 characters in length, it must start with a letter, and it can include some special characters such as underscore (_) and dollar sign ($).

Here are some examples of associative array type declarations:

    -- A list of dates
    TYPE birthdays_tt IS TABLE OF DATE INDEX BY PLS_INTEGER;

    -- A list of company IDs
    TYPE company_keys_tt IS TABLE OF company.company_id%TYPE NOT NULL
       INDEX BY BINARY_INTEGER;

    -- A list of book records; this structure allows you to make a "local"
    -- copy of the book table in your PL/SQL program.
    TYPE booklist_tt IS TABLE OF books%ROWTYPE
       INDEX BY NATURAL;

    -- Each collection is organized by the author name.
    TYPE books_by_author_tt IS TABLE OF books%ROWTYPE
       INDEX BY books.author%TYPE;
     
    -- A collection of collections
    TYPE private_collection_tt IS TABLE OF books_by_author_tt
       INDEX BY VARCHAR2(100);

Notice that in the above example I declared a very generic type of collection (list of dates), but gave it a very specific name: birthdays_tt. There is, of course, just one way to declare an associative array type of dates. Rather than have a plethora of collection TYPE definitions that differ only by name scattered throughout your application, you might consider creating a single package that offers a set of predefined, standard collection types. Here is an example, available in the colltypes.sql file on the book’s web site:

    /* file on web: colltypes.sql */
    CREATE OR REPLACE PACKAGE collection_types
    IS
       -- Associative array types
       TYPE boolean_aat IS TABLE OF BOOLEAN INDEX BY BINARY_INTEGER;
     
       TYPE date_aat IS TABLE OF DATE INDEX BY BINARY_INTEGER;
     
       TYPE pls_integer_aat IS TABLE OF PLS_INTEGER INDEX BY BINARY_INTEGER;
     
       TYPE number_aat IS TABLE OF NUMBER INDEX BY BINARY_INTEGER;
     
       TYPE identifier_aat IS TABLE OF VARCHAR2(30)
        INDEX BY BINARY_INTEGER;
     
       TYPE vcmax_aat IS TABLE OF VARCHAR2(32767)
        INDEX BY BINARY_INTEGER;
     
       -- Nested table types
       TYPE boolean_ntt IS TABLE OF BOOLEAN;
       TYPE date_ntt IS TABLE OF DATE;
       TYPE pls_integer_ntt IS TABLE OF PLS_INTEGER;
       TYPE number_ntt IS TABLE OF NUMBER;
       TYPE identifier_ntt IS TABLE OF VARCHAR2(30);
       TYPE vcmax_ntt IS TABLE OF VARCHAR2(32767)
    END collection_types;
    /

With such a package in place, you can grant EXECUTE authority to PUBLIC, and then all developers can use the packaged TYPEs to declare their own collections. Here is an example:

    DECLARE
       family_birthdays collection_types.date_aat;

Declaring a nested table or VARRAY

As with associative arrays, you must define a type before you can declare an actual nested table or VARRAY. You can define these types either in the database or in a PL/SQL block.

To create a nested table datatype that lives in the database (and not just your PL/SQL code), specify:

    CREATE [ OR REPLACE ] TYPEtype_name AS | IS
       TABLE OF element_datatype [ NOT NULL ];

To create a VARRAY datatype that lives in the database (and not just your PL/SQL code), specify:

    CREATE [ OR REPLACE ] TYPEtype_name AS | IS
       VARRAY (max_elements) OF element_datatype [ NOT NULL ];

To drop a type, specify:

    DROP TYPEtype_name [ FORCE ];

To declare a nested table datatype in PL/SQL, use the declaration:

    TYPEtype_name IS TABLE OF element_datatype [ NOT NULL ];

To declare a VARRAY datatype in PL/SQL, use the declaration:

    TYPEtype_name IS VARRAY (max_elements)
       OF element_datatype [ NOT NULL ];

where:

OR REPLACE

Allows you to rebuild an existing type as long as there are no other database objects that depend on it. This is useful primarily because it preserves grants.

type_name

Is a legal SQL or PL/SQL identifier. This will be the identifier to which you refer later when you use it to declare variables or columns.

element_datatype

Is the type of the collection’s elements. All elements are of a single type, which can be most scalar datatypes, an object type, or a REF object type. If the elements are objects, the object type itself cannot have an attribute that is a collection. In PL/SQL, if you are creating a collection with RECORD elements, its fields can be only scalars or objects. Explicitly disallowed collection datatypes are BOOLEAN, NCHAR, NCLOB, NVARCHAR2, REF CURSOR, TABLE, and VARRAY (non-SQL datatype).

NOT NULL

Indicates that a variable of this type cannot have any null elements. However, the collection can be atomically null (uninitialized).

max_elements

Is the maximum number of elements allowed in the VARRAY. Once declared, this cannot be altered.

FORCE

Tells Oracle to drop the type even if there is a reference to it in another type. For example, if an object type definition uses a particular collection type, you can still drop the collection type using the FORCE keyword.

Note that the only syntactic difference between declaring nested table types and declaring associative array types in a PL/SQL program is the absence of the INDEX BY clause for nested table types.

The syntactic differences between nested table and VARRAY type declarations are:

  • The use of the keyword VARRAY

  • The limit on VARRAY’s number of elements

Changing nested table of VARRAY characteristics

If you have created a nested table or VARRAY type in the database, you can use the ALTER TYPE command to change several of the type’s characteristics.

Use the ALTER TYPE ... MODIFY LIMIT syntax to increase the number of elements of a VARRAY type. Here is an example:

    ALTER TYPE list_t MODIFY LIMIT 100 INVALIDATE;
    /

When the element type of a VARRAY or nested table type is a variable character, RAW, or numeric, you can increase the size of the variable character or RAW type or increase the precision of the numeric type. Here is an example:

    CREATE TYPE list_t AS VARRAY(10) OF VARCHAR2(80);
    /
    ALTER TYPE list_t MODIFY ELEMENT TYPE VARCHAR2(100) CASCADE;
    /

The INVALIDATE and CASCADE options are provided to either invalidate all dependent objects or propagate the change to both the type and any table dependents.

Declaring and Initializing Collection Variables

Once you have created your collection type, you can reference that collection type to declare an instance of that type: the actual collection variable. The general format for a collection declaration is:

collection_name collection_type [:= collection_type (...)];

where collection_name is the name of the collection, and collection_type is the name of both the previously declared collection type and (if nested table or VARRAY) a constructor function of the same name.

A constructor has the same name as the type, and accepts as arguments a comma-separated list of elements. When you are declaring a nested table or VARRAY, you must initialize the collection before using it. Otherwise, you will receive this error:

    ORA-06531: Reference to uninitialized collection

In the following example I create a general collection type to emulate the structure of the company table. I then declare two different collections based on that type.

    DECLARE
       TYPE company_aat IS TABLE OF company%ROWTYPE INDEX BY PLS_INTEGER;

       premier_sponsor_list company_aat;
       select_sponsor_list company_aat;
    BEGIN
       ...
    END;

If I declare a nested table or VARRAY, I can also immediately initialize the collection by calling its constructor function. Here is an example:

    DECLARE
       TYPE company_aat IS TABLE OF company%ROWTYPE;
       premier_sponsor_list company_aat := company_aat();
    BEGIN
       ...
    END;

I could also choose to initialize the nested table in my executable section:

    DECLARE
       TYPE company_aat IS TABLE OF company%ROWTYPE;
       premier_sponsor_list company_aat;
    BEGIN
       premier_sponsor_list:= company_aat();
    END;

I simply must ensure that it is initialized before I try to use the collection. Associative arrays do not need to be initialized before you assign values to them.

As you can see, declaring collection variables, or instances of a collection type, is no different from declaring other kinds of variables: simply provide a name, type, and optional default or initial value.

Let’s take a closer look at nested table and VARRAY initialization.

The previous example showed you how to initialize a collection by calling a constructor function without any parameters. You can also provide an initial set of values. Suppose now that I create a schema level type named color_tab_t:

    CREATE OR REPLACE TYPE Color_tab_t AS TABLE OF VARCHAR2(30);

Next, I declare some PL/SQL variables based on that type.

    DECLARE
       my_favorite_colors Color_tab_t := Color_tab_t();
       his_favorite_colors Color_tab_t := Color_tab_t('PURPLE'),
       her_favorite_colors Color_tab_t := Color_tab_t('PURPLE', 'GREEN'),
    END;

In the first declaration, the collection is initialized as empty; it contains no rows. The second declaration assigns a single value, “PURPLE,” to row 1 of the nested table. The third declaration assigns two values, “PURPLE” and “GREEN,” to rows 1 and 2 of that nested table.

Because I have not assigned any values to my_favorite_colors in the call to the constructor, I will have to extend it before I can put elements into it. The his and her collections already have been extended implicitly as needed by the constructor values list.

Initializing implicitly during direct assignment

You can copy the entire contents of one collection to another as long as both are built from the exact same collection type (two different collection types based on the same datatype will not work). When you do so, initialization comes along “for free.”

Here’s an example illustrating the implicit initialization that occurs when we assign wedding_colors to be the value of earth_colors.

    DECLARE
       earth_colors Color_tab_t := Color_tab_t('BRICK', 'RUST', 'DIRT'),
       wedding_colors Color_tab_t;
    BEGIN
       wedding_colors := earth_colors;
       wedding_colors(3) := 'CANVAS';
    END;

This code initializes wedding_colors and creates three elements that match those in earth_colors. These are independent variables rather than pointers to identical values; changing the third element of wedding_colors to CANVAS does not have any effect on the third element of earth_colors.

This kind of direct assignment is not possible when datatypes are merely “type-compatible.” Even if you have created two different types with the exact same definition, the fact that they have different names makes them different types. Thus, the following block of code fails to compile:

    DECLARE
       TYPE tt1 IS TABLE OF employee%ROWTYPE;
       TYPE tt2 IS TABLE OF employee%ROWTYPE;
       t1   tt1 := tt1();
       t2   tt2 := tt2();
    BEGIN
       t1 := t2;
    END;

Initializing implicitly via FETCH

If you use a collection as a type in a database table, Oracle provides some very elegant ways of moving the collection between PL/SQL and the table. As with direct assignment, when you use FETCH or SELECT INTO to retrieve a collection and drop it into a collection variable, you get automatic initialization of the variable. Collections can turn out to be incredibly useful!

Although we mentioned this briefly in an earlier example, let’s take a closer look at how you can read an entire collection in a single fetch. First, we want to create a table containing a collection and populate it with a couple of values:

    CREATE TABLE color_models (
         model_type VARCHAR2(12)
       , colors Color_tab_t
       )
       NESTED TABLE colors STORE AS color_model_colors_tab;

    INSERT INTO color_models
       VALUES ('RGB', Color_tab_t('RED','GREEN','BLUE'));

Now we can show off the neat integration features. With one trip to the database, we can retrieve all the values of the colors column for a given row and deposit them into a local variable:

    DECLARE
       l_colors Color_tab_t;
    BEGIN
       /* Retrieve all the nested values in a single fetch.
       || This is the cool part.
       */
       SELECT colors INTO l_colors FROM color_models
          WHERE model_type = 'RGB';
       ...
    END;

Pretty neat, huh? Here are a few important things to notice:

  • Oracle, not the programmer, assigns the subscripts of l_colors when fetched from the database.

  • Oracle’s assigned subscripts begin with 1 (as opposed to 0, as in some other languages) and increment by 1.

  • Fetching satisfies the requirement to initialize the local collection variable before assigning values to elements. We didn’t initialize l_colors with a constructor, but PL/SQL knew how to deal with it.

You can also make changes to the contents of the nested table and just as easily move the data back into a database table. Just to be mischievous, let’s create a Fuschia-Green-Blue color model:

    DECLARE
       color_tab Color_tab_t;
    BEGIN
       SELECT colors INTO color_tab FROM color_models
          WHERE model_type = 'RGB';
     
       FOR element IN 1..color_tab.COUNT
       LOOP
          IF color_tab(element) = 'RED'
          THEN
             color_tab(element) := 'FUSCHIA';
          END IF;
       END LOOP;
     
       /* Here is the cool part of this example. Only one insert
       || statement is needed -- it sends the entire nested table
       || back into the color_models table in the database. */
     
       INSERT INTO color_models VALUES ('FGB', color_tab);
    END;

VARRAY integration

Does this database-to-PL/SQL integration work for VARRAYs too? You bet, although there are a couple of differences.

First of all, realize that when you store and retrieve the contents of a nested table in the database, Oracle makes no promises about preserving the order of the elements. This makes sense because the server is just putting the nested data into a store table behind the scenes, and we all know that relational databases don’t give two hoots about row order. By contrast, storing and retrieving the contents of a VARRAY do preserve the order of the elements.

Preserving the order of VARRAY elements is a fairly useful capability. It makes it possible to embed meaning in the order of the data, which is something you cannot do in a pure relational database. For example, if you want to store someone’s favorite colors in rank order, you can do it with a single VARRAY column. Every time you retrieve the column collection, its elements will be in the same order as when you last stored it. In contrast, abiding by a pure relational model, you would need two columns: one for an integer corresponding to the rank and one for the color.

This order-preservation of VARRAYs suggests some possibilities for interesting utility functions. For example, you could fairly easily code a tool that would allow the insertion of a new “favorite” at the low end of the list by “shifting up” all the other elements.

A second difference between integration of nested tables and integration of VARRAYs with the database is that some SELECT statements you could use to fetch the contents of a nested table will have to be modified if you want to fetch a VARRAY. (See the later section "Working with Collections in SQL" for some examples.)

Populating Collections with Data

A collection is empty after initialization. No rows are defined within it. A collection is, in this way, very much like a relational table. A row is defined by assigning a value to that row. This assignment can be done through the standard PL/SQL assignment operation, by fetching data from one or more relational tables into a collection, or by performing an aggregate assignment (in essence, copying one collection to another).

If you are working with associative arrays, you can assign a value (of the appropriate type) to any valid row number in the collection. If the index type of the associative array is an integer, then the row number must be between −231 + 1 and 231 - 1. The simple act of assigning the value creates the row and deposits the value in that row.

In contrast to associative arrays, you can’t assign values to arbitrarily numbered subscripts of nested tables and VARRAYs; instead, the indexes (at least initially) are monotonically increasing integers, assigned by the PL/SQL engine. That is, if you initialize n elements, they will have subscripts 1 through n—and those are the only rows to which you can assign a value.

Before you try to assign a value to a row in a nested table or VARRAY, you must make sure that it has been extended to include or initialize that row. Use the EXTEND operator, discussed earlier in this chapter, to make new rows available in nested tables and VARRAYs.

Using the assignment operator

You can assign values to rows in a collection with the standard assignment operator of PL/SQL, as shown here:

    countdown_test_list (43) := 'Internal pressure';
    company_names_table (last_name_row+10) := 'Johnstone Clingers';

You can use this same syntax to assign an entire record or complex datatype to a row in the collection, as you see here:

    DECLARE
       TYPE emp_copy_t IS TABLE OF emp%ROWTYPE;
       l_emps emp_copy_t := emp_copy_t();
       l_emprec emp%ROWTYPE;
    BEGIN
       l_emprec.ename := 'Steven';
       l_emprec.salary := 10000;
       l_emps.EXTEND
       l_emps (l_emps.LAST) := l_emprec;
    END;

As long as the structure of data on the right side of the assignment matches that of the collection type, the assignment will complete without error.

What row values can I use?

When you assign data to a row in an associative array, you must specify the location in the collection. The type of value, and valid range of values, you use to indicate this location depend on the how you defined the INDEX BY clause of the associative array, and are explained in the following table:

INDEX BY clause

Minimum value

Maximum value

    INDEX BY BINARY_INTEGER

−231 + 1

231 - 1

    INDEX BY PLS_INTEGER

−231 + 1

231 - 1

    INDEX BY NATURAL

0

231 - 1

    INDEX BY POSITIVE

1

231 - 1

    INDEX BY SIGNTYPE

-1

1

    INDEX BY VARCHAR(N)

Any string within specified length

Any string within specified length

Aggregate assignments

You can also perform an “aggregate assignment” of the contents of an entire collection to another collection of exactly the same type. Here is an example of such a transfer:

      1  DECLARE
      2     TYPE name_t IS TABLE OF VARCHAR2(100) INDEX BY PLS_INTEGER;
      3     old_names name_t;
      4     new_names name_t;
      5  BEGIN
      6     /* Assign values to old_names table */
      7     old_names(1) := 'Smith';
      8     old_names(2) := 'Harrison';
      9
     10     /* Assign values to new_names table */
     11     new_names(111) := 'Hanrahan';
     12     new_names(342) := 'Blimey';
     13
     14     /* Transfer values from new to old */
     15     old_names := new_names;
     16
     17     /* This statement will display 'Hanrahan' */
     18     DBMS_OUTPUT.PUT_LINE (
     19       old_names.FIRST || ': ' || old_names(old_names.FIRST));
     20* END;
    SQL> /
    111: Hanrahan

A collection-level assignment completely replaces the previously defined rows in the collection. In the preceding example, rows 1 and 2 in old_names are defined before the last, aggregate assignment.

After the assignment, only rows 111 and 342 in the old_names collection have values.

Assigning rows from a relational table

You can also populate rows in a collection by querying data from a relational table. The assignment rules described earlier in this section apply to SELECT-driven assignments. The following example demonstrates various ways you can copy data from a relational table into a collection

I can use an implicit SELECT INTO to populate a single row of data in a collection:

    DECLARE
       TYPE emp_copy_t IS TABLE OF emp%ROWTYPE;
       l_emps emp_copy_t := emp_copy_t();
    BEGIN
       l_emps.EXTEND;
       SELECT *
         INTO l_emps (1)
         FROM emp
        WHERE empno = 7521;
    END;

I can use a cursor FOR loop to move multiple rows into a collection, populating those rows sequentially:

    DECLARE
       TYPE emp_copy_t IS TABLE OF emp%ROWTYPE;
       l_emps emp_copy_t := emp_copy_t();
    BEGIN
       FOR emp_rec IN (SELECT * FROM emp)
       LOOP
          l_emps.EXTEND;
          l_emps (l_emps.LAST) := emp_rec;
       END LOOP;
    END;

I can also use a cursor FOR loop to move multiple rows into a collection, populating those rows nonsequentially. In this case, I will switch to using an associative array, so that I can assign rows randomly, that is, using the primary key value of each row in the database as the row number in my collection:

    DECLARE
       TYPE emp_copy_t IS TABLE OF emp%ROWTYPE INDEX BY PLS_INTEGER;
       l_emps emp_copy_t;
    BEGIN
       FOR emp_rec IN (SELECT * FROM emp)
       LOOP
          l_emps (emp_rec.empno) := emp_rec;
       END LOOP;
    END;

Starting with Oracle9i Database Release 2, I can use BULK COLLECT (as described in Chapter 15) to retrieve all the rows of a table in a single assignment step, depositing the data into any of the three types of collections. When using a nested tables or VARRAY, you do not need to explicitly initialize the collection. Here is an example:

    DECLARE
       TYPE emp_copy_nt IS TABLE OF emp%ROWTYPE;
       l_emps emp_copy_nt;
    BEGIN
       SELECT * BULK COLLECT INTO l_emps FROM emp;
    END;

Advantage of nonsequential population of collection

For anyone used to working with traditional arrays, the idea of populating your collection nonsequentially may seem strange. Why would you do such a thing? Consider the following scenario.

In many applications, we find ourselves writing and executing the same queries over and over again. In some cases, the queries are retrieving static data, such as codes and descriptions that rarely (if ever) change. Well, if the data isn’t changing—especially during a user session—then why would I want to keep querying the information from the database? Even if the data is cached in the System Global Area (SGA) , I still need to visit the SGA, confirm that the query has already been parsed, find that information in the data buffers and finally return it to the session program area (the Program Global Area, or PGA).

Here’s an idea: set as a rule that for a given static lookup table, a user will never query a row from the table more than once in a session. After the first time, it will be stored in the session’s PGA and be instantly available for future requests. This is very easy to do with collections. Essentially, you use the collection’s index as an intelligent key.

Let’s take a look at an example. I have a hairstyles table that contains a numeric code (primary key) and a description of the hairstyle (e.g., “Pageboy,” “Shag”). These styles are timeless and rarely change.

Here is the body of a package that uses a collection to cache code-hairstyle pairs and that minimizes trips to the database:

    /* File on web: justonce.sql */
    CREATE OR REPLACE PACKAGE BODY justonce
    IS
       TYPE desc_t IS TABLE OF hairstyles.description%TYPE
          INDEX BY BINARY_INTEGER;
       descriptions   desc_t;
     
       FUNCTION description (code_in IN hairstyles.code%TYPE)
          RETURN hairstyles.description%TYPE
       IS
          return_value   hairstyles.description%TYPE;
     
          FUNCTION desc_from_database RETURN hairstyles.description%TYPE
          IS
             CURSOR desc_cur IS
                SELECT description FROM hairstyles WHERE code = code_in;
             desc_rec   desc_cur%ROWTYPE;
          BEGIN
             OPEN desc_cur;
             FETCH desc_cur INTO desc_rec;
             CLOSE desc_cur;
             RETURN desc_rec.description;
          END;
       BEGIN
          RETURN descriptions (code_in);
       EXCEPTION
          WHEN NO_DATA_FOUND THEN
             descriptions (code_in) := desc_from_database;
             RETURN descriptions (code_in);
       END;
    END justonce;

The table provides a description of the interesting aspects of this program:

Line(s)

Description

3-5

Declare a collection type and the collection to hold my cached descriptions.

7-8

Header of my retrieval function. The interesting thing about the header is that it is not interesting at all. There is no indication that this function is doing anything but the typical query against the database to retrieve the description for the code. The implementation is hidden, which is just the way you want it.

12-22

That very traditional query from the database. But in this case, it is just a private function within my main function, which is fitting because it is not the main attraction.

24

The entire execution section! Simply return the description that is stored in the row indicated by the code number. The first time I run this function for a given code, the row will not be defined. So PL/SQL raises NO_DATA_FOUND (see lines 25-27). For all subsequent requests for this code, however, the row is defined, and the function returns the value immediately.

26-28

So the data hasn’t yet been queried in this session. Fine. Trap the error, look up the description from the database, and deposit it in the collection. Then return that value. Now we are set to divert all subsequent lookup attempts.

So how much of a difference does this caching make? Using the local Oracle9i Database on my laptop, I ran some tests and found that it took just under two seconds to execute 10,000 queries against the hairstyles table. That’s efficient, no doubt about it. Yet it took only 0.1 seconds to retrieve that same information 10,000 times using the above function. That’s more than an order of magnitude improvement—and that’s with a local database. The superiority of the collection caching technique would be even greater in a real-world situation.

Here are some final notes on the collection caching technique:

  • This technique is a classic tradeoff between CPU and memory. Each session has its own copy of the collection (this is program data and is stored in the PGA). If you have 10,000 users, the total memory required for these 10,000 small caches could be considerable.

  • Consider using this technique with any of the following scenarios: small, static tables in a multiuser application; large, static tables in which a given user will access only a small portion of the table; manipulation of large tables in a batch process (just a single connect taking up possibly a lot of memory).

Accessing Data Inside a Collection

There generally isn’t much point to putting information into a collection unless you intend to use or access that data. There are several things you need to keep in mind when accessing data inside a collection:

  • If you try to read a row in a collection that has not been defined, Oracle raises the NO_DATA_FOUND exception. This is one of the reasons you will want to avoid numeric FOR loops when iterating through the contents of a collection. If that collection is not densely filled, Oracle will fail with NO_DATA_FOUND as soon as it hits a gap between the values returned by the FIRST and LAST methods.

  • If you try to read a row that is beyond the limit of EXTENDed rows in a table or VARRAY, Oracle raises the following exception:

        ORA-06533: Subscript beyond count

    When working with nested tables and VARRAYs, you should always make sure that you have extended the collection to encompass the row you want to assign or read.

  • If you try to read a row whose index is beyond the limit of the VARRAY type definition, Oracle raises the following exception:

        ORA-06532: Subscript outside of limit

    Remember: you can always call the LIMIT method to find the maximum number of rows that are allowed in a VARRAY. Because the subscript always starts at 1 in this type of collection, you can then easily determine if you still have room for more data in the data structure.

Beyond these cautionary tales, it is very easy to access individual rows in a collection: simply provide the subscript (or subscripts—see the next section for the syntax needed for collections of collections) after the name of the collection.

Collections of Complex Datatypes

When Oracle first made collections available in PL/SQL 2 (corresponding to Oracle7), you could create associative arrays of scalar values: dates, numbers, strings, and so on. But that was about it, making collections (back then called PL/SQL tables) awkward and of limited use.

But that was long ago. Starting with Oracle9i Database Release 2, you can define collection types of arbitrarily complex structures. All of the following structures are now supported:

Collections of records based on tables with %ROWTYPE

These structures allow you to quickly and easily mimic a relational table within a PL/SQL program.

Collections of user-defined records

The fields of the record can be scalars or complex datatypes in and of themselves. For example, you can define a collection of records where the record TYPE itself contains a field that is itself another collection.

Collections of object types and other complex types

The datatype of the collection can be an object type previously defined with the CREATE TYPE statement. You can also easily define collections of LOBs, XML documents, etc.

Collections of collections (directly and indirectly)

You can define multilevel collections, including collections of collections and collections of datatypes that contain, as an attribute or a field, another collection

Let’s take a look at examples of each of these variations.

Collections of records

You define a collection of records by specifying a record type (through either %ROWTYPE or a programmer-defined record type) in the TABLE OF clause of the collection definition. This technique applies only to collection TYPEs that are declared inside a PL/SQL program. Nested table and VARRAY TYPEs defined in the database cannot reference %ROWTYPE record structures.

Here is an example of a collection of records based on a custom record TYPE:

    CREATE OR REPLACE PACKAGE compensation_pkg
    IS
       TYPE reward_rt IS RECORD (
          nm VARCHAR2(2000),
          sal NUMBER,
          comm NUMBER);
     
       TYPE reward_tt IS TABLE OF reward_rt
          INDEX BY BINARY_INTEGER;
     
    END compensation_pkg;

With these types defined in my package specification, I can now declare collections in other programs like this:

    DECLARE
       holiday_bonuses compensation_pkg.reward_tt;

Collections of records come in especially handy when you want to create in-memory (PGA) collections that have the same structure (and, at least in part, data) as database tables. Why would I want to do this? Suppose that I am running a batch process on Sunday at 3:00 a.m. against tables that are modified only during the week. I need to do some intensive analysis that involves multiple passes against the tables’ data. I could simply query the data repetitively from the database, but that is a relatively slow, intensive process.

Alternately, I can copy the data from the table or tables into a collection and then move much more rapidly (and randomly) through my result set. I am, in essence, emulating bidirectional cursors in my PL/SQL code.

If you decide to copy data into collections and manipulate them within your program, you can choose between two basic approaches for implementing this logic:

  • Embed all of the collection code in your main program

  • Create a separate package to encapsulate access to the data in the collection

I generally choose the second approach for most situations. In other words, I find it useful to create separate, well-defined, and highly reusable APIs (application programmatic interfaces) to complex data structures and logic. Here is the package specification for my bidirectional cursor emulator:

    /* File on web: bidir.pkg */
    CREATE OR REPLACE PACKAGE bidir
    IS
       FUNCTION rowforid (id_in IN employee.employee_id%TYPE)
          RETURN employee%ROWTYPE;

       FUNCTION firstrow RETURN PLS_INTEGER;
       FUNCTION lastrow RETURN PLS_INTEGER;

       FUNCTION rowCount RETURN PLS_INTEGER;

       FUNCTION end_of_data RETURN BOOLEAN;

       PROCEDURE setrow (nth IN PLS_INTEGER);

       FUNCTION currrow RETURN employee%ROWTYPE;

       PROCEDURE nextrow;
       PROCEDURE prevrow;
    END;

So how do you use this API? Here is an example of a program using this API to read through the result set for the employee table, first forward and then backward:

    DECLARE
       l_employee   employee%ROWTYPE;
    BEGIN
       LOOP
          EXIT WHEN bidir.end_of_data;
          l_employee := bidir.currrow;
          DBMS_OUTPUT.put_line (l_employee.last_name);
          bidir.nextrow;
       END LOOP;

       bidir.setrow (bidir.lastrow);

       LOOP
          EXIT WHEN bidir.end_of_data;
          l_employee := bidir.currrow;
          DBMS_OUTPUT.put_line (l_employee.last_name);
          bidir.prevrow;
       END LOOP;
    END;

An astute reader will now be asking: when is the collection loaded up with the data? Or even better: where is the collection? There is no evidence of a collection anywhere in the code I have presented.

Let’s take the second question first. The reason you don’t see the collection is that I have hidden it behind my package specification. A user of the package never touches the collection and doesn’t have to know anything about it. That is the whole point of the API. You just call one or another of the programs that will do all the work of traversing the collection (data set) for you.

Now, when and how is the collection loaded? This may seem a bit magical until you read about packages in Chapter 18. If you look in the package body, you will find that it has an initialization section as follows:

    BEGIN -- Package initialization
       FOR rec IN  (SELECT * FROM employee)
       LOOP
          employees (rec.employee_id) := rec;
       END LOOP;

       g_currrow := firstrow;
    END;

Tip

Note that g_currrow is defined in the package body and therefore was not listed in the specification above.

This means that the very first time I reference any element in the package specification, this code is run automatically, transferring the contents of the employee table to my employees collection. When does that happen in my sample program shown earlier? Inside my loop, when I call the bidir.end_of_data function to see if I am done looking through my data set!

I encourage you to examine the package implementation. The code is very basic and easy to understand; the benefits of this approach can be dramatic.

Collections of objects and other complex types

You can use an object type, LOB, XML document, and virtually any valid PL/SQL type as the datatype of a collection TYPE statement. The syntax for defining these collections is the same, but the way you manipulate the contents of the collections can be complicated, depending on the underlying type.

Here is an example of working with a collection of objects:

    /* File on web: object_collection.sql */
    CREATE TYPE pet_t IS OBJECT (
       tag_no   INTEGER,
       name     VARCHAR2 (60),
       MEMBER FUNCTION set_tag_no (new_tag_no IN INTEGER)
          RETURN pet_t));

    DECLARE
       TYPE pets_t IS TABLE OF pet_t;

       pets   pets_t :=
         pets_t (pet_t (1050, 'Sammy'), pet_t (1075, 'Mercury'));
    BEGIN
       FOR indx IN pets.FIRST .. pets.LAST
       LOOP
          DBMS_OUTPUT.put_line (pets (indx).name);
          pets(indx).set_tag_no (indx);
       END LOOP;
    END;

Once I have my object type defined, I can declare a collection based on that type and then populate it with instances of those object types. You can just as easily declare collections of LOBs, XMLTypes, and so on. All the normal rules that apply to variables of those datatypes also apply to individual rows of a collection of that datatype.

Multilevel collections

Oracle9i Database Release 2 introduced the ability to nest collections within collections, a feature that is also referred to as multilevel collections. Let’s take a look at an example and then discuss how you can use this feature in your applications.

Suppose that I want to build a system to maintain information about my pets. Besides their standard information, such as breed, name, and so on, I would like to keep track of their visits to the veterinarian. So I create a vet visit object type:

    CREATE TYPE vet_visit_t IS OBJECT (
       visit_date  DATE,
       reason      VARCHAR2 (100)
       );

Notice that objects instantiated from this type are not associated with a pet (i.e., a foreign key to a pet table or object). You will soon see why I don’t need to do that. Now I create a nested table of vet visits (we are supposed to go at least once a year):

    CREATE TYPE vet_visits_t IS TABLE OF vet_visit_t

With these data structures defined, I now declare my object type to maintain information about my pets:

    CREATE TYPE pet_t IS OBJECT (
       tag_no   INTEGER,
       name     VARCHAR2 (60),
       petcare vet_visits_t,
       MEMBER FUNCTION set_tag_no (new_tag_no IN INTEGER) RETURN pet_t);

This object type has three attributes and one member method. Any object instantiated from this type will have associated with it a tag number, a name, and a list of visits to the vet. You can also modify the tag number for that pet by calling the set_tag_no program.

So I have now declared an object type that contains as an attribute a nested table. I don’t need a separate database table to keep track of these veterinarian visits; they are a part of my object.

Now let’s take advantage of the multilevel collections features available for Oracle9i Database and Oracle Database 10g. In the following example, lines 1-10 define a multilevel collection; once the collection is populated, lines 11-14 access the collection.

    /* File on web: multilevel_collections.sql */
    DECLARE
       TYPE bunch_of_pets_t IS TABLE OF pet_t INDEX BY BINARY_INTEGER;
       my_pets   bunch_of_pets_t;
    BEGIN
       my_pets (1) :=
             pet_t (100, 'Mercury',
                vet_visits_t (
                   vet_visit_t ('01-Jan-2001', 'Clip wings'),
                   vet_visit_t ('01-Apr-2002', 'Check cholesterol'))
             );
       DBMS_OUTPUT.PUT_LINE (my_pets (1).name);
       DBMS_OUTPUT.PUT_LINE (my_pets (1).petcare (2).reason);
       DBMS_OUTPUT.put_line (my_pets.COUNT);
       DBMS_OUTPUT.put_line (my_pets(1).petcare.LAST);
    END;

The output from running this script is:

    Mercury
    Check cholesterol
    1
    2

The following table explains what’s going on in the code:

Line(s)

Description

2-3

Declare a local associative array TYPE, in which each row contains a single pet object. I then declare a collection to keep track of my “bunch of pets.”

5-10

Assign an object of type pet_t to the first row in this associative array. As you can see, the syntax required when working with nested, complex objects of this sort can be quite intimidating. So let’s parse the various steps required. To instantiate an object of type pet_t, I must provide a tag number, a name, and a list of vet visits, which is a nested table. To provide a nested table of type vet_visits_t, I must call the associated constructor (of the same name). I can either provide a null or empty list, or initialize the nested table with some values. I do this in lines 8 and 9. Each row in the vet_visits_t collection is an object of type vet_visit_t, so again I must use the object constructor and pass in a value for each attribute (date and reason for visit).

11

Display the value of the name attribute of the pet object in row 1 of the my_pets associative array.

12

Display the value of the reason attribute of the vet visit object in row 2 of the nested table, which in turn resides in the first row of the my_pets associative array. That’s a mouthful, and it is a “line-full” of code.

13-14

Demonstrate how you can use the collection methods (in this case, COUNT and LAST) on both outer and nested collections.

In this example we have the good fortune to be working with collections that, at each level, actually have names: the my_pets associative array and the petcare nested table. This is not always the case, as is illustrated in the next example.

Unnamed nested collections: multidimensional arrays

You can use nested, multilevel collections to emulate multidimensional arrays within PL/SQL. Multidimensional collections are declared in stepwise fashion, adding a dimension at each step (quite different from the syntax used to declare an array in a 3GL).

We will start with a simple example and then step through the implementation of a generic three-dimensional array package. Suppose that we want to record temperatures within some three-dimensional space organized using some (X, Y, Z) coordinate system. The following block illustrates the sequential declarations necessary to accomplish this.

    DECLARE
       SUBTYPE temperature IS NUMBER;
       SUBTYPE coordinate_axis IS PLS_INTEGER;
     
       TYPE temperature_x IS TABLE OF temp INDEX BY coordinate_axis;
       TYPE temperature_xy IS TABLE OF tempx INDEX BY coordinate_axis;
       TYPE temperature_xyz IS TABLE OF tempxy INDEX BY coordinate_axis;
     
       temperature_3d temperature_xyz;
    BEGIN
       temperature_3d (1) (2) (3) := 45;
    END;
    /

Here, the subtype and type names are used to provide clarity as to the usage of the contents of the actual collection (temperature_3d): the collection types (temperature_X, temperature_XY, temperature_XYZ) as well as the collection indexes (coordinate_axis).

Note that although our careful naming makes it clear what each of the collection types contains and is used for, we do not have corresponding clarity when it comes to referencing collection elements by subscript; in other words, in what order do we specify the dimensions? It is not obvious from our code whether the temperature 45 degrees is assigned to the point (X:1, Y:2, Z:3) or to (X:3, Y:2, Z:1).

Now let’s move on to a more general treatment of a three-dimensional array structure.

The multdim package allows you to declare your own three-dimensional array, as well as set and retrieve values from individual cells. We create a simple package to encapsulate operations on a three-dimensional associative table storing VARCHAR2 elements indexed in all dimensions by PLS_INTEGER. The following declarations constitute some basic building blocks for the package:

    /* Files on web: multdim.pkg, multdim.tst, multdim2.pkg */
    CREATE OR REPLACE PACKAGE multdim
    IS
       TYPE dimX_t IS TABLE OF VARCHAR2 (32767) INDEX BY PLS_INTEGER;
       TYPE dimY_t IS TABLE OF dimX_t INDEX BY PLS_INTEGER;
       TYPE dimZ_t IS TABLE OF dimY_t INDEX BY PLS_INTEGER;
     
       PROCEDURE setcell (
          array_in   IN OUT   dimZ_t,
          dimX_in             PLS_INTEGER,
          dimY_in             PLS_INTEGER,
          dimZ_in             PLS_INTEGER,
          value_in   IN       VARCHAR2
       );
     
       FUNCTION getcell (
          array_in   IN   dimZ_t,
          dimX_in         PLS_INTEGER,
          dimY_in         PLS_INTEGER,
          dimZ_in         PLS_INTEGER
       )
          RETURN VARCHAR2;
     
       FUNCTION EXISTS (
          array_in   IN   dimZ_t,
          dimX_in         PLS_INTEGER,
          dimY_in         PLS_INTEGER,
          dimZ_in         PLS_INTEGER
       )
          RETURN BOOLEAN;

We have defined the three collection types progressively as before:

Type DimX_t

A one-dimensional associative table of VARCHAR2 elements

Type DimY_t

An associative table of DimX_t elements

Type DimZ_t

An associative table of DimY_t elements

Thus, three-dimensional space is modeled as cells in a collection of planes that are each modeled as a collection of lines. This is consistent with common understanding, which indicates a good model. Of course our collections are sparse and finite, while geometric three-dimensional space is considered to be dense and infinite, so the model has limitations. However, for our purposes, we are concerned only with a finite subset of points in three-dimensional space, and the model is adequate.

We equip our three-dimensional collection type with a basic interface to get and set cell values, as well as the ability to test whether a specific cell value exists in a collection.

Basic operations

Let’s look at the basic interface components. The procedure to set a cell value in a three-dimensional array given its coordinates could not be much simpler:

    PROCEDURE setcell (
       array_in   IN OUT   dimZ_t,
       dimX_in         PLS_INTEGER,
       dimY_in         PLS_INTEGER,
       dimZ_in         PLS_INTEGER,
       value_in   IN   VARCHAR2
    )
    IS
    BEGIN
       array_in(dimZ_in )(dimY_in )(dimX_in) := value_in;
    END;

Despite the simplicity of this code, there is significant added value in encapsulating the assignment statement, as it relieves us of having to remember the order of reference for the dimension indexes. It is not obvious when directly manipulating a DimZ_t collection whether the Z coordinate is the first index or the last. Whatever is not obvious in code will result in bugs sooner or later. The fact that all the collection indexes have the same datatype complicates matters because mixed-up data assignments will not raise exceptions but rather just generate bad results somewhere down the line. If our testing is not thorough, these are the kinds of bugs that make it to production code and wreak havoc on data and our reputations.

Our function to return a cell value is likewise trivial but valuable:

    FUNCTION getcell (
       array_in   IN   dimZ_t,
       dimX_in         PLS_INTEGER,
       dimY_in         PLS_INTEGER,
       dimZ_in         PLS_INTEGER
    )
       RETURN VARCHAR2
    IS
    BEGIN
       RETURN array_in(dimZ_in )(dimY_in )(dimX_in);
    END;

If there is no cell in array_in corresponding to the supplied coordinates, then getcell will raise NO_DATA_FOUND. However, if any of the coordinates supplied are NULL, then the following, less friendly exception is raised:

    ORA-06502: PL/SQL: numeric or value error: NULL index table key value

In a more complete implementation, we should enhance the module to assert a precondition requiring all coordinate parameter values to be NOT NULL. At least Oracle now provides us with an improved error message, informing us that a null index value was responsible for the exception. It would be even better, though, if Oracle did not use the same VALUE_ERROR exception for so many different error conditions.

With the EXISTS function, we get to some code that is a bit more interesting. EXISTS will return TRUE if the cell identified by the coordinates is contained in the collection and FALSE otherwise.

    FUNCTION EXISTS (
       array_in   IN   dimZ_t,
       dimX_in         PLS_INTEGER,
       dimY_in         PLS_INTEGER,
       dimZ_in         PLS_INTEGER
     )
       RETURN BOOLEAN
    IS
       l_value varchar2(32767);
    BEGIN
        l_value := array_in(dimZ_in )(dimY_in )(dimX_in);
        RETURN TRUE;
    EXCEPTION
       WHEN NO_DATA_FOUND
       THEN
          RETURN FALSE;
    END;

This function traps the NO_DATA_FOUND exception raised when the assignment references a nonexistent cell and converts it to the appropriate Boolean. This is a very simple and direct method for obtaining our result, and illustrates a creative reliance on exception handling to handle the “conditional logic” of the function.

Here is a sample script that exercises this package:

    DECLARE
       my_3d_array   multdim.dim3_t;
    BEGIN
       multdim.setcell (my_3d_array, 1, 5, 800, 'def'),
       multdim.setcell (my_3d_array, 1, 15, 800, 'def'),
       multdim.setcell (my_3d_array, 5, 5, 800, 'def'),
       multdim.setcell (my_3d_array, 5, 5, 805, 'def'),

       DBMS_OUTPUT.PUT_LINE (multdim.getcell (my_3d_array, 1, 5, 800));
       DBMS_OUTPUT.PUT_LINE (multdim.EXISTS (my_3d_array, 1, 5, 800));
       DBMS_OUTPUT.PUT_LINE (multdim.EXISTS (my_3d_array, 6000, 5, 800));
       DBMS_OUTPUT.PUT_LINE (multdim.EXISTS (my_3d_array, 6000, 5, 807));

       DBMS_OUTPUT.PUT_LINE (my_3d_array.COUNT);
    END;
    /

The multdim2.pkg file available on the book’s web site contains an enhanced version of the multdim package that implements support for “slicing” of that three-dimensional collection, in which we fix one dimension and isolate the two-dimensional plane determined by the fixed dimension. A slice from a temperature grid would give us, for example, the range of temperatures along a certain latitude or longitude.

Beyond the challenge of writing the code for slicing, an interesting question presents itself: will there be any differences between slicing out an XY plane, an XZ plane, or an YZ plane in this fashion from a symmetric cube of data? If there are significant differences, it could affect how we choose to organize our multidimensional collections.

We encourage you to explore these issues and the implementation of the multdim2.pkg package.

How deeply can I nest collections?

As I played around with two- and three-dimensional arrays, I found myself wondering how deeply I could nest these multilevel collections. So I decided to find out. I built a small code generator that allows me to pass in the number of levels of nesting. It then constructs a procedure that declares N collection TYPEs, each one being a TABLE OF the previous table TYPE. Finally, it assigns a value to the string that is all the way at the heart of the nested collections.

I found that I was able to create a collection of at least 250 nested collections before my computer ran into a memory error! I find it hard to believe that any PL/SQL developer will even come close to that level of complexity. If you would like to run this same experiment in your own system, check out the gen_multcoll.sp file available on the book’s web site.

Using String-Indexed Collections

In Oracle9i Database Release 2, Oracle greatly expanded the datatypes developers can specify as the index type for associative arrays (previously known as index-by-tables. Specifically, we can “index by” strings in addition to integer values (i.e., row numbers). This gives us significant additional flexibility. Let’s look at some examples and explore applications of this feature.

Here is a block of code that demonstrates the basics:

    /* File on web: assoc_array.sql */
    DECLARE
       TYPE population_type IS
          TABLE OF NUMBER INDEX BY VARCHAR2(64);
       l_country_population population_type;
       l_continent_population population_type;
       l_howmany NUMBER;
       l_location VARCHAR2(64);
    BEGIN
       l_country_population('Greenland') := 100000;
       l_country_population('Iceland') := 750000;

       l_howmany := l_country_population('Greenland'),

       l_continent_population('Australia') := 30000000;

       l_continent_population('Antarctica') := 1000; -- New entry
       l_continent_population('Antarctica') := 1001; -- Replaces value

       l_location := l_continent_population.FIRST;
       DBMS_OUTPUT.PUT_LINE (l_location);
       DBMS_OUTPUT.PUT_LINE (l_continent_population(l_location));

       l_location := l_continent_population.LAST;
       DBMS_OUTPUT.PUT_LINE (l_location);
       DBMS_OUTPUT.PUT_LINE (l_continent_population(l_location));
    END;

Here is the output from the script:

    Antarctica
    1001
    Australia
    30000000

Recall that with this type of associative array, the values returned by calls to the FIRST, LAST, PRIOR, and NEXT methods are strings and not numbers. Also note that you cannot use %TYPE to declare the associative array type. You must use a literal, hardcoded declaration.

So why would you want to index by string instead of number? Suppose that you need to do some heavy processing of employee information in your program. You need to go back and forth over the set of selected employees, searching by the employee ID number, last name, and Social Security number (or appropriate national identification number for non-U.S. countries).

    DECLARE
       TYPE name_t IS TABLE OF employee%ROWTYPE INDEX BY VARCHAR2(100);

       TYPE id_t IS TABLE OF employee%ROWTYPE INDEX BY BINARY_INTEGER;

       by_name   name_t;
       by_ssn    name_t;
       by_id     id_t;

       ceo_name employee.last_name%TYPE := 'ELLISON';

       PROCEDURE load_arrays IS
       BEGIN
          FOR rec IN  (SELECT * FROM employee)
          LOOP
             -- Load up all three arrays in single pass to database table.
             by_name (rec.last_name) := rec;
             by_ssn (rec.ssn) := rec;
             by_id (rec.employee_id) := rec;
          END LOOP;
       END;
    BEGIN
       load_arrays;

       -- Now I can retrieve information by name or ID:

       IF by_name (ceo_name).salary > by_id (7645).salary
       THEN
          make_adjustment (by_name);
       END IF;
    END;

As you can see in the preceding example, it doesn’t take a whole lot of code to build multiple, highly efficient entry points into cached data transferred from a relational table. Still, to make it even easier for you to implement these techniques in your application, I have built a utility, which you will find in the genaa.sp file on the book’s web site, that will actually generate a package to implement caching for the specified relational table. It populates a collection based on the integer primary key and another collection for each unique index defined on the table (indexed by BINARY_INTEGER or VARCHAR2, depending on the type(s) of the column(s) in the index).

Finally, the file, summer_reading.pkg, available on the book’s web site, offers another example of the use of VARCHAR2-indexed associative arrays to manipulate lists of information within a PL/SQL program.

Working with Collections in SQL

I’ve been working with Oracle’s SQL for more than 18 years and PL/SQL for more than 14, but my brain has rarely turned as many cartwheels over SQL’s semantics as it did when I first contemplated the collection pseudo-functions introduced in Oracle8 Database. These pseudo-functions exist to coerce database tables into acting like collections, and vice versa. Because there are some manipulations that work best when data is in one form versus the other, these functions give application programmers access to a rich and interesting set of structures and operations.

Tip

The collection pseudo-functions are not available in PL/SQL proper, only in SQL. You can, however, employ these operators in SQL statements that appear in your PL/ SQL code, and it is extremely useful to understand how and when to do so. We’ll see examples in the following sections.

The three collection pseudo-functions are as follows:

CAST

Maps a collection of one type to a collection of another type. This can encompass mapping a VARRAY to a nested table.

MULTISET

Maps a database table to a collection. With MULTISET and CAST, you can actually retrieve rows from a database table as a collection-typed column.

TABLE

Maps a collection to a database table. This is the inverse of MULTISET.

Oracle introduced these pseudo-functions to manipulate collections that live in the database. They are important to our PL/SQL programs for several reasons, not the least of which is that they provide an incredibly efficient way to move data between the database and the application.

Yes, these pseudo-functions can be puzzling. But if you’re the kind of person who gets truly excited by arcane code, these SQL extensions will make you jumping-up-and-down silly.

The CAST pseudo-function

The CAST operator can be used in a SQL statement to convert from one built-in datatype or collection type to another built-in datatype or collection type. In other words, within SQL you can use CAST in place of TO_CHAR to convert from number to string.

Another very handy use of CAST is to convert between types of collections. Here is an example of casting a named collection. Suppose that we have created the color_ models table based on a VARRAY type as follows:

    CREATE TYPE Color_nt AS TABLE OF VARCHAR2(30);
    /
     
    CREATE TYPE Color_vat AS VARRAY(16) OF VARCHAR2(30);

    CREATE TABLE color_models (
       model_type VARCHAR2(12),
       colors Color_vat);

We can CAST the VARRAY colors column as a nested table and apply the pseudo-function TABLE (explained below) to the result:

    SELECT COLUMN_VALUE FROM
       TABLE (SELECT CAST(colors AS Color_nt)
             FROM color_models
            WHERE model_type = 'FGB'),

CAST performs an on-the-fly conversion of the Color_array_t collection type to the Color_tab_t collection type.

It is also possible to cast a “bunch of records”—such as the result of a subquery—as a particular collection type. Doing so requires the MULTISET function, covered in the next section.

The MULTISET pseudo-function

The MULTISET function exists only for use within CASTs. MULTISET allows you to retrieve a set of data and convert it on the fly to a collection type. (Note that the SQL MULTISET function is distinct from the PL/SQL MULTISET operators for nested tables, discussed in the later section, "Nested Table Multiset Operations.”)

The simplest form of MULTISET is this:

    SELECT CAST (MULTISET (SELECTfield FROM table) AS collection-type)
       FROM DUAL;

So if we happen to have a relational table of colors:

    CREATE TABLE some_colors (
       color_name VARCHAR2(30),
       color_classification VARCHAR2(30));

and we want to CAST to a collection so we can fetch a set of them at once, we can do this:

    DECLARE
       some_hot_colors Color_tab_t;
    BEGIN
       SELECT CAST(MULTISET(SELECT color_name
                              FROM some_colors
                             WHERE color_classification = 'HOT')
                AS Color_tab_t)
         INTO some_hot_colors
         FROM DUAL;
    END;

Another way to use MULTISET involves a correlated subquery in the select list:

    SELECT outerfield,
      CAST(MULTISET(SELECT field FROM whateverTable
                     WHERE correlationCriteria)
        AS collectionTypeName)
      FROM outerTable;

This technique is useful for making joins look as if they include a collection. For example, suppose that we had a detail table that listed, for each bird in our table, the countries where that species lives:

    CREATE TABLE birds (
       genus VARCHAR2(128),
       species VARCHAR2(128),
       colors Color_array_t,
       PRIMARY KEY (genus, species)
    );

    CREATE TABLE bird_habitats (
       genus VARCHAR2(128),
       species VARCHAR2(128),
       country VARCHAR2(60),
       FOREIGN KEY (genus, species) REFERENCES birds (genus, species)
    );

    CREATE TYPE Country_tab_t AS TABLE OF VARCHAR2(60);

We should then be able to smush the master and detail tables together in a single SELECT that converts the detail records into a collection type. This feature has enormous significance for client/server programs because the number of roundtrips can be cut down without incurring the overhead of duplicating the master records with each and every detail record:

    DECLARE
       CURSOR bird_curs IS
          SELECT b.genus, b.species,
             CAST(MULTISET(SELECT bh.country FROM bird_habitats bh
                            WHERE bh.genus = b.genus
                              AND bh.species = b.species)
               AS country_tab_t)
            FROM birds b;
       bird_row bird_curs%ROWTYPE;
    BEGIN
       OPEN bird_curs;
       FETCH bird_curs into bird_row;
       CLOSE bird_curs;
    END;

As with the CAST pseudo-function, MULTISET cannot serve as the target of an INSERT, UPDATE, or DELETE statement.

The TABLE pseudo-function

The TABLE operator casts or converts a collection-valued column into something you can SELECT from. It sounds complicated, but this section presents an example that’s not too hard to follow.

Looking at it another way, let’s say that you have a database table with a column of a collection type. How can you figure out which rows in the table contain a collection that meets certain criteria? That is, how can you select from the database table, putting a WHERE clause on the collection’s contents? Wouldn’t it be nice if you could just say:

    SELECT *
      FROM table_name
     WHERE collection_column
           HAS CONTENTS 'whatever';   -- invalid; imaginary syntax!

Logically, that’s exactly what you can do with the TABLE function. Going back to our color_models database table, how could we get a listing of all color models that contain the color RED? Here’s the real way to do it:

    SELECT *
      FROM color_models c
     WHERE 'RED' IN
           (SELECT * FROM TABLE(c.colors));

which, in SQL*Plus, returns:

    MODEL_TYPE   COLORS
    ------------ ------------------------------------------------------
    RGB          COLOR_TAB_T('RED', 'GREEN', 'BLUE')

The query means “go through the color_models table and return all rows whose list of colors contains at least one RED element.” Had there been more rows with a RED element in their colors column, these rows too would have appeared in our SQL*Plus result set.

As illustrated previously, TABLE accepts an alias-qualified collection column as its argument:

    TABLE(alias_name.collection_name)

TABLE returns the contents of this collection coerced into a virtual database table. Hence, you can SELECT from it. In our example, it is used in a subquery.

To repeat an earlier admonition, none of the collection pseudo-functions is available from within PL/SQL, but PL/SQL programmers will certainly want to know how to use these gizmos in their SQL statements!

You will also find the pseudo-functions, particularly TABLE, very handy when you are taking advantage of the table function capability introduced in Oracle9i Database Release 1. A table function is a function that returns a collection, and it can be used in the FROM clause of a query. This functionality is explored in Chapter 17.

Personally, I find these new features fascinating, and I enjoy the mental calisthenics required to understand and use them. Maybe mine isn’t a universal sentiment, but at least you must admit that Oracle hasn’t let its language technology get tired!

Sorting contents of collections

One of the wonderful aspects of pseudo-functions is that you can apply SQL operations against the contents of PL/SQL data structures (nested tables and VARRAYs, at least). You can, for example, use ORDER BY to select information from the nested table in the order you desire. Here, I populate a database table with some of my favorite authors:

    CREATE TYPE names_t AS TABLE OF VARCHAR2 (100);

    CREATE TYPE authors_t AS TABLE OF VARCHAR2 (100);

    CREATE TABLE favorite_authors (name varchar2(200))

    BEGIN
       INSERT INTO favorite_authors VALUES ('Robert Harris'),
       INSERT INTO favorite_authors VALUES ('Tom Segev'),
       INSERT INTO favorite_authors VALUES ('Toni Morrison'),
    END;

Now I would like to blend this information with data from my PL/SQL program:

    DECLARE
       scifi_favorites   authors_t
          := authors_t ('Sheri S. Tepper', 'Orson Scott Card', 'Gene Wolfe'),
    BEGIN
       DBMS_OUTPUT.put_line ('I recommend that you read books by:'),

       FOR rec IN  (SELECT column_value favs
                      FROM TABLE (cast (scifi_favorites AS  names_t))
                    UNION
                    SELECT NAME
                      FROM favorite_authors)
       LOOP
          DBMS_OUTPUT.put_line (rec.favs);
       END LOOP;
    END;

Notice that I can use UNION to combine data from my database table and collection. I can also apply this technique only to PL/SQL data to sort the contents being retrieved:

    DECLARE
       scifi_favorites   authors_t
          := authors_t ('Sheri S. Tepper', 'Orson Scott Card', 'Gene Wolfe'),
    BEGIN
       DBMS_OUTPUT.put_line ('I recommend that you read books by:'),

       FOR rec IN  (SELECT COLUMN_VALUE Favs
                      FROM TABLE (cast (scifi_favorites AS  names_t))
                     ORDER BY column_value)
       LOOP
          DBMS_OUTPUT.put_line (rec.favs);
       END LOOP;
    END;

Tip

COLUMN_VALUE in the above query is the system-defined name of the column created with the TABLE operator.

Nested Table Multiset Operations

The essential advance made in collections in Oracle Database 10g is that Oracle is now treating nested tables more like the multisets that they actually are. Oracle now provides high-level set operations that can be applied to nested tables (and only, for the time being, to nested tables. Here is a brief summary of these set-level capabilities:

Operation

Return value

Description

=

BOOLEAN

Compares two nested tables, and returns TRUE if they have the same named type and cardinality and if the elements are equal.

<> or !=

BOOLEAN

Compares two nested tables, and returns FALSE if they differ in named type, cardinality, or equality of elements.

[NOT] IN ()

BOOLEAN

Returns TRUE [FALSE] if the nested table to the left of IN exists in the list of nested tables in the parentheses.

x MULTISET EXCEPT [DISTINCT] y

NESTED TABLE

Performs a MINUS set operation on nested tables x and y, returning a nested table whose elements are in x, but not in y. x, y, and the returned nested table must all be of the same type. The DISTINCT keyword forces the elimination of duplicates from the returned nested table.

x MULTISET INTERSECT [DISTINCT] y

NESTED TABLE

Performs an INTERSECT set operation on nested tables x and y, returning a nested table whose elements are in both x and y. x, y, and the returned nested table must all be of the same type. The DISTINCT keyword forces the elimination of duplicates from the returned nested table.

x MULTISET UNION [DISTINCT] y

NESTED TABLE

Performs a UNION set operation on nested tables x and y, returning a nested table whose elements include all those in x as well as those in y. x, y, and the returned nested table must all be of the same type. The DISTINCT keyword forces the elimination of duplicates from the returned nested table.

SET(x)

NESTED TABLE

Returns nested table x without duplicate elements.

x IS [NOT] A SET

BOOLEAN

Returns TRUE [FALSE] if the nested table x is composed of unique elements.

x IS [NOT] EMPTY

BOOLEAN

Returns TRUE [FALSE] if the nested table x is empty.

e [NOT] MEMBER [OF] x

BOOLEAN

Returns TRUE [FALSE] if the expression e is a member of the nested table x .

y [NOT] SUBMULTISET [OF] x

BOOLEAN

Returns TRUE [FALSE] if the nested table y contains only elements that are also in nested table x.

In the following sections, we will take a closer look at many of these features. As we do so, we will make frequent references to this nested table type:

    /* File on web: 10g_strings_nt.sql */
    CREATE OR REPLACE TYPE strings_nt IS TABLE OF VARCHAR2(100);
    /

We will, in addition, make repeated use of the following package:

    /* File on web: 10g_authors.pkg */
    CREATE OR REPLACE PACKAGE authors_pkg
    IS
       steven_authors    strings_nt
          := strings_nt ('ROBIN HOBB'
                       , 'ROBERT HARRIS'
                       , 'DAVID BRIN'
                       , 'SHERI S. TEPPER'
                       , 'CHRISTOPHER ALEXANDER'
                        );
       veva_authors   strings_nt
          := strings_nt ('ROBIN HOBB'
                       , 'SHERI S. TEPPER'
                       , 'ANNE MCCAFFREY'
                       );
     
       eli_authors   strings_nt
          := strings_nt ( 'SHERI S. TEPPER'
                        , 'DAVID BRIN'
                        );
     
       PROCEDURE show_authors (
          title_in   IN   VARCHAR2
        , authors_in    IN   strings_nt
       );
    END;
    /
     
    CREATE OR REPLACE PACKAGE BODY authors_pkg
    IS
       PROCEDURE show_authors (
          title_in   IN   VARCHAR2
        , authors_in    IN   strings_nt
       )
       IS
       BEGIN
          DBMS_OUTPUT.put_line (title_in);
     
          FOR indx IN authors_in.FIRST .. authors_in.LAST
          LOOP
             DBMS_OUTPUT.put_line (indx || ' = ' || authors_in (indx));
          END LOOP;
     
          DBMS_OUTPUT.put_line ('_'),
       END show_authors;
    END;
    /

Testing Equality and Membership of Nested Tables

Prior to Oracle Database 10g Release 1, the only way to tell if two collections were identical (i.e., had the same contents) was to compare the values of each row for equality (and if the collection contained records, you would have to compare each field of each record); see the example in 10g_coll_compare_old.sql for an example of this code. Now, in Oracle Database 10g and with nested tables, you only need to use the standard = and != operators as shown in the following example:

    /* File on web: 10g_coll_compare.sql */
    DECLARE
       TYPE clientele IS TABLE OF VARCHAR2 (64);
     
       group1   clientele := clientele ('Customer 1', 'Customer 2'),
       group2   clientele := clientele ('Customer 1', 'Customer 3'),
       group3   clientele := clientele ('Customer 3', 'Customer 1'),
    BEGIN
       IF group1 = group2
       THEN
          DBMS_OUTPUT.put_line ('Group 1 = Group 2'),
       ELSE
          DBMS_OUTPUT.put_line ('Group 1 != Group 2'),
       END IF;
     
       IF group2 != group3
       THEN
          DBMS_OUTPUT.put_line ('Group 2 != Group 3'),
       ELSE
          DBMS_OUTPUT.put_line ('Group 2 = Group 3'),
       END IF;
    END;

Note that the equality check implemented for nested tables treats NULLs consistently with other operators. It considers NULL to be “unknowable.” Thus, one NULL is never equal to another NULL. As a consequence, if both of the nested tables you are comparing contain a NULL value at the same row, they will not be considered equal.

Checking for Membership of an Element in a Nested Table

In a variation on that theme, you can use the MEMBER operator to determine if a particular element is in a nested table. Use SUBMULTISET to determine if an entire nested table is contained in another nested table. Here is an example:

    /* File on web: 10g_submultiset.sql */
    DECLARE
       PROCEDURE bpl (val IN BOOLEAN, str IN VARCHAR2)
       IS
       BEGIN
          IF val
          THEN
             DBMS_OUTPUT.put_line (str || '-TRUE'),
          ELSIF NOT val
          THEN
             DBMS_OUTPUT.put_line (str || '-FALSE'),
          ELSE
             DBMS_OUTPUT.put_line (str || '-NULL'),
          END IF;
       END;
    BEGIN
       bpl (authors_pkg.steven_authors
                SUBMULTISET OF authors_pkg.eli_authors
          , 'Father follows son?'),
       bpl (authors_pkg.eli_authors
                SUBMULTISET OF authors_pkg.steven_authors
          , 'Son follows father?'),
     
       bpl (authors_pkg.steven_authors
                NOT SUBMULTISET OF authors_pkg.eli_authors
          , 'Father doesn''t follow son?'),
       bpl (authors_pkg.eli_authors
                NOT SUBMULTISET OF authors_pkg.steven_authors
          , 'Son doesn''t follow father?'),
    END;
    /

Here are the results of running this code:

    SQL>@10g_submultiset
    Father follows son? - FALSE
    Son follows father? - TRUE
    Father doesn't follow son? - TRUE
    Son doesn't follow father? - FALSE

Performing High-Level Set Operations

Set operations like UNION, INTERSECT, and MINUS are extremely powerful and helpful, precisely because they are such simple, high-level concepts. You can write a very small amount of code to achieve great effects. Consider the following code, which shows a variety of set operators at work:

    /* File on web: 10g_union.sql */
     1  DECLARE
     2     our_authors strings_nt := strings_nt();
     3  BEGIN
     4      our_authors := authors_pkg.steven_authors
     5                     MULTISET UNION authors_pkg.veva_authors;
     6
     7      authors_pkg.show_authors ('MINE then VEVA', our_authors);
     8
     9      our_authors := authors_pkg.veva_authors
    10                     MULTISET UNION authors_pkg.steven_authors;
    11
    12      authors_pkg.show_authors ('VEVA then MINE', our_authors);
    13
    14      our_authors := authors_pkg.steven_authors
    15                     MULTISET UNION DISTINCT authors_pkg.veva_authors;
    16
    17      authors_pkg.show_authors ('MINE then VEVA with DISTINCT', our_authors);
    18
    19      our_authors := authors_pkg.steven_authors
    20                     MULTISET INTERSECT authors_pkg.veva_authors;
    21
    22      authors_pkg.show_authors ('IN COMMON', our_authors);
    23
    24      our_authors := authors_pkg.veva_authors
    25                     MULTISET EXCEPT authors_pkg.steven_authors;
    26
    27      authors_pkg.show_authors (q'[ONLY VEVA'S]', our_authors);
    28* END;

Here is the output from running this script:

    SQL>@10g_union
    MINE then VEVA
    1 = ROBIN HOBB
    2 = ROBERT HARRIS
    3 = DAVID BRIN
    4 = SHERI S. TEPPER
    5 = CHRISTOPHER ALEXANDER
    6 = ROBIN HOBB
    7 = SHERI S. TEPPER
    8 = ANNE MCCAFFREY
    _
    VEVA then MINE
    1 = ROBIN HOBB
    2 = SHERI S. TEPPER
    3 = ANNE MCCAFFREY
    4 = ROBIN HOBB
    5 = ROBERT HARRIS
    6 = DAVID BRIN
    7 = SHERI S. TEPPER
    8 = CHRISTOPHER ALEXANDER
    _
    MINE then VEVA with DISTINCT
    1 = ROBIN HOBB
    2 = ROBERT HARRIS
    3 = DAVID BRIN
    4 = SHERI S. TEPPER
    5 = CHRISTOPHER ALEXANDER
    6 = ANNE MCCAFFREY
    _
    IN COMMON
    1 = ROBIN HOBB
    2 = SHERI S. TEPPER
    _
    ONLY VEVA'S
    1 = ANNE MCCAFFREY

Note that MULTISET UNION does not act precisely the same as the SQL UNION. It does not reorder the data, and it does not remove duplicate values. Duplicates are perfectly acceptable and, indeed, are significant in a multiset.

Handling Duplicates in a Nested Table

So, a nested table can have duplicates (the same value stored more than once)—and those duplicates will persist even beyond a MULTISET UNION operation. Sometimes this is what you want; sometimes, you would much rather have a distinct set of values with which to work. Oracle provides the following operators:

SET operator

Helps you transform a nondistinct set of elements in a nested table into a distinct set. You can think of it as a “SELECT DISTINCT” for nested tables.

IS A SET and IS [NOT] A SET operators

Helps you answers questions like “Does this nested table contain any duplicate entries?”

The following script exercises these features of Oracle Database 10g:

    /* File on web: 10g_set.sql */
    DECLARE
       distinct_authors strings_nt := strings_nt ();

       PROCEDURE bpl (val IN BOOLEAN, str IN VARCHAR2)
       IS
       BEGIN
          IF val
          THEN
             DBMS_OUTPUT.put_line (str || '-TRUE'),
          ELSIF NOT val
          THEN
             DBMS_OUTPUT.put_line (str || '-FALSE'),
          ELSE
             DBMS_OUTPUT.put_line (str || '-NULL'),
          END IF;
       END;
    BEGIN
       -- Add a duplicate author to Steven's list
       authors_pkg.steven_authors.EXTEND;
       authors_pkg.steven_authors(authors_pkg.steven_authors.LAST) := 'ROBERT HARRIS';
     
       distinct_authors :=
          SET (authors_pkg.steven_authors);

       authors_pkg.show_authors (
          'FULL SET', authors_pkg.steven_authors);
     
       bpl (authors_pkg.steven_authors IS A SET, 'My authors distinct?'),
       bpl (authors_pkg.steven_authors IS NOT A SET, 'My authors NOT distinct?'),
       DBMS_OUTPUT.PUT_LINE (''),

       authors_pkg.show_authors (
          'DISTINCT SET', distinct_authors);

       bpl (distinct_authors IS A SET, 'SET of authors distinct?'),
       bpl (distinct_authors IS NOT A SET, 'SET of authors NOT distinct?'),
       DBMS_OUTPUT.PUT_LINE (''),

    END;
    /

And here are the results of this script:

    SQL>@10g_set
    FULL SET
    1 = ROBIN HOBB
    2 = ROBERT HARRIS
    3 = DAVID BRIN
    4 = SHERI S. TEPPER
    5 = CHRISTOPHER ALEXANDER
    6 = ROBERT HARRIS
    _
    My authors distinct? - FALSE
    My authors NOT distinct? - TRUE
     
    DISTINCT SET
    1 = ROBIN HOBB
    2 = ROBERT HARRIS
    3 = DAVID BRIN
    4 = SHERI S. TEPPER
    5 = CHRISTOPHER ALEXANDER
    _
    SET of authors distinct? - TRUE
    SET of authors NOT distinct? - FALSE

Maintaining Schema-Level Collections

Here are some not-so-obvious bits of information that will assist you in using nested tables and VARRAYS. This kind of housekeeping is not necessary or relevant when working with associative arrays.

Necessary Privileges

When they live in the database, collection datatypes can be shared by more than one Oracle user (schema). As you can imagine, privileges are involved. Fortunately, it’s not complicated; only one Oracle privilege—EXECUTE—applies to collection types.

If you are Scott, and you want to grant Joe permission to use Color_tab_t in his programs, all you need to do is grant the EXECUTE privilege to him:

    GRANT EXECUTE on Color_tab_t TO JOE;

Joe can then refer to the type using schema.type notation. For example:

    CREATE TABLE my_stuff_to_paint (
       which_stuff VARCHAR2(512),
       paint_mixture SCOTT.Color_tab_t
    )
    NESTED TABLE paint_mixture STORE AS paint_mixture_st;

EXECUTE privileges are also required by users who need to run PL/SQL anonymous blocks that use the object type. That’s one of several reasons that named PL/SQL modules—packages, procedures, functions—are generally preferred. Granting EXECUTE on the module confers the grantor’s privileges to the grantee while executing the module.

For tables that include collection columns, the traditional SELECT, INSERT, UDPATE, and DELETE privileges still have meaning, as long as there is no requirement to build a collection for any columns. However, if a user is going to INSERT or UPDATE the contents of a collection column, that user must have the EXECUTE privilege on the type because that is the only way to use the default constructor.

Collections and the Data Dictionary

There are a few new entries in the data dictionary that will be very helpful in managing your nested table and VARRAY collection types (see Table 12-4). The shorthand dictionary term for user-defined types is simply TYPE. Collection type definitions are found in the USER_SOURCE view (or DBA_SOURCE, or ALL_SOURCE).

Table 12-4. Data dictionary entries for collection types

To answer the question ...

Use this view

As in

What collection types have I created?

USER_TYPES

SELECT type_name

FROM user_types

WHERE typecode ='COLLECTION’;

What was the original type definition of collection Foo_t?

USER_SOURCE

SELECT text

FROM user_source

WHERE name = ‘FOO_T’

AND type = ‘TYPE’

ORDER BY line;

What columns implement Foo_t?

USER_TAB_ COLUMNS

SELECT table_name,column_name

FROM user_tab_columns

WHERE data_type = ‘FOO_T’;

What database objects are dependent on Foo_t?

USER_DEPENDENCIES

SELECT name, type

FROM user_dependencies

WHERE referenced_name='FOO_T’;

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.216.249