Collection Extensions in 10g
Collection Extensions in 10g
This article covers a range of new set operators, functions and conditions introduced in
10g. Briefly, these extensions enable us to work more efficiently and elegantly with
collections, both in SQL and PL/SQL. Collections, based on user-defined types, have
been available in Oracle since version 8 (note that a collection is a SQL type, not a
PL/SQL table). They offer enormous potential for extending the PL/SQL language and
there are many examples of how to use them available on the web and in forums.
Collection comparisons;
Collection functions such as SET, CARDINALITY and POWERMULTISET;
Collection conditions such as MEMBER and SUBMULTISET; and
Collection operators such as MULTISET EXCEPT and MULTISET UNION.
Note that most of the examples will be in PL/SQL. This is not because the new
operators cannot be used in SQL (they are all primarily SQL extensions in fact) - it is
because it is easier to see the results of the operators with a few DBMS_OUTPUT
statements.
setup
Type created.
comparing collections
Prior to 10g, if we wished to compare the contents of one collection with another, we'd
have to resort to looping through the collections in PL/SQL or using SQL techniques
(such as TABLE() and MINUS for example). Now, however, collections can be
compared for equality as any "regular" datatype (note that to compare collections of
complex types, a MAP method is required. For an example, see the section on complex
collection types at the end of this article). In the following example, we'll initialise three
collections, two of which are equal, and then test them for equality under a range of
scenarios.
SQL> DECLARE
2
3 nt0 varchar2_ntt := varchar2_ntt('A','B','C');
4 nt1 varchar2_ntt := varchar2_ntt('C','D','E');
5 nt2 varchar2_ntt := varchar2_ntt('C','D','E');
6
7 PROCEDURE compare (
8 test_in IN VARCHAR2,
9 ntl_in IN varchar2_ntt,
10 ntr_in IN varchar2_ntt
11 ) IS
12 BEGIN
13 DBMS_OUTPUT.PUT(RPAD('['||test_in||']',30) || '=> ');
14 IF ntl_in = ntr_in THEN
15 DBMS_OUTPUT.PUT_LINE( 'EQUAL' );
16 ELSE
17 DBMS_OUTPUT.PUT_LINE( 'NOT EQUAL' );
18 END IF;
19 END compare;
20
21 BEGIN
22
23 /* Compare populated collections... */
24 compare( '1: nt0->nt1, populated', nt0, nt1 );
25 compare( '2: nt1->nt2, populated', nt1, nt2 );
26
27 /* Compare collections after deletion... */
28 nt0.DELETE;
29 nt1.DELETE;
30 compare( '3: nt0->nt1, deleted', nt0, nt1 );
31
32 /* Compare newly initialised collections... */
33 nt0 := varchar2_ntt();
34 nt1 := varchar2_ntt();
35 compare( '4: nt0->nt1, initialised', nt0, nt1 );
36
37 END;
38 /
[1: nt0->nt1, populated] => NOT EQUAL
[2: nt1->nt2, populated] => EQUAL
[3: nt0->nt1, deleted] => EQUAL
[4: nt0->nt1, initialised] => EQUAL
Equality tests for collections in 10g also include the use of IN (which after all is just a
convenient method of bundling many similar equality-OR tests into a shorter syntax).
We can see this below. Note that the order of the elements is irrelevant. In the following
example, collections nt1 and nt2 are equivalent, which is why the IN test returns TRUE.
SQL> DECLARE
2 nt0 varchar2_ntt := varchar2_ntt('A','B','C');
3 nt1 varchar2_ntt := varchar2_ntt('C','D','E');
4 nt2 varchar2_ntt := varchar2_ntt('E','C','D');
5 BEGIN
6 IF nt2 IN ( nt0, nt1 ) THEN
7 DBMS_OUTPUT.PUT_LINE( 'Collection nt2 is equal to at least one other.' );
8 END IF;
9 END;
10 /
Collection nt2 is equal to at least one other.
In PL/SQL, it has always been trivial to get a count of elements in an array or collection,
using the COUNT pseudo-method. The new CARDINALITY function in 10g now makes
it trivial to count the elements of a collection in SQL. This function is logically equivalent
to the COUNT pseudo-method. In the following examples we'll show simple examples of
the function's usage in both PL/SQL and SQL.
SQL> DECLARE
2 nt varchar2_ntt := varchar2_ntt( 'A','B','C','C' );
3 BEGIN
4 DBMS_OUTPUT.PUT_LINE( 'Count = ' || nt.COUNT );
5 DBMS_OUTPUT.PUT_LINE( 'Cardinality = ' || CARDINALITY(nt) );
6 END;
7 /
Count = 4
Cardinality = 4
This time we can see the CARDINALITY function being used in SQL whereas the
COUNT method cannot.
CARDINALITY(NT)
---------------
4
1 row selected.
SQL> SELECT nt.COUNT
2 FROM (
3 SELECT varchar2_ntt('A','B','C','C') AS nt
4 FROM dual
5 );
SELECT nt.COUNT
*
ERROR at line 1:
ORA-00904: "NT"."COUNT": invalid identifier
The fact that the CARDINALITY function is available to us in SQL means that we can
also use it in filter predicates.
SQL> SELECT *
2 FROM (
3 SELECT varchar2_ntt('A','B') AS nt
4 FROM dual
5 UNION ALL
6 SELECT varchar2_ntt('A','B','C','D') AS nt
7 FROM dual
8 )
9 WHERE CARDINALITY(nt) > 2;
NT
-----------------------------------
VARCHAR2_NTT('A', 'B', 'C', 'D')
1 row selected.
set
The new SET function in 10g returns a distinct range of elements from a collection (i.e.
DISTINCT). For simple collections such as the one we have used throughout this
article, it's very straightforward, as the following example demonstrates.
SQL> SELECT nt
2 , SET(nt) AS nt_set
3 FROM (
4 SELECT varchar2_ntt('A','B','C','C') AS nt
5 FROM dual
6 );
NT NT_SET
----------------------------------- ------------------------------
VARCHAR2_NTT('A', 'B', 'C', 'C') VARCHAR2_NTT('A', 'B', 'C')
1 row selected.
SQL> SELECT nt.column_value AS distinct_element
2 FROM TABLE(SET(varchar2_ntt('A','B','C','C'))) nt;
DISTINCT_ELEMENT
----------------
A
B
C
3 rows selected.
is (not) a set
In addition to turning collections into valid sets, we can also test to see whether a
collection is already a set, as the following example shows. Note that a restriction with
this condition is that the collection must be initialised and not NULL.
SQL> DECLARE
2
3 nt_null varchar2_ntt;
4 nt_initialised varchar2_ntt := varchar2_ntt();
5 nt_set varchar2_ntt := varchar2_ntt('A','B','C');
6 nt_not_set varchar2_ntt := varchar2_ntt('A','B','C','C');
7
8 PROCEDURE test (
9 test_in IN VARCHAR2,
10 result_in IN BOOLEAN
11 ) IS
12 BEGIN
13 DBMS_OUTPUT.PUT( RPAD('[' || test_in || ']',30) || '=> ');
14 DBMS_OUTPUT.PUT_LINE(CASE
15 WHEN result_in
16 THEN 'TRUE'
17 ELSE 'FALSE'
18 END);
19 END test;
20
21 BEGIN
22
23 /* First test the NULL collection... */
24 test( 'nt_null IS A SET', nt_null IS A SET );
25 test( 'nt_null IS NOT A SET', nt_null IS NOT A SET );
26
27 /* Now test the valid collections... */
28 test( 'nt_initialised IS A SET', nt_initialised IS A SET );
29 test( 'nt_set IS A SET', nt_set IS A SET );
30 test( 'nt_not_set IS A SET', nt_not_set IS A SET );
31 test( 'SET(nt_not_set) IS A SET', SET(nt_not_set) IS A SET );
32
33 END;
34 /
[nt_null IS A SET] => FALSE
[nt_null IS NOT A SET] => FALSE
[nt_initialised IS A SET] => TRUE
[nt_set IS A SET] => TRUE
[nt_not_set IS A SET] => FALSE
[SET(nt_not_set) IS A SET] => TRUE
cardinality revisited
We have already seen the new CARDINALITY function above. The term "cardinality"
can sometimes be ambiguous in Oracle. It is sometimes used to describe the count of a
domain of values (for example, when describing bitmap indexes) but most of the time it
is used to describe a straight count (think CBO). If we wish to get the true cardinality of
a collection, we can combine CARDINALITY and SET (for SQL old-timers, this is the
logical equivalent of COUNT(DISTINCT)). We can demonstrate this easily as follows.
SQL> DECLARE
2 nt varchar2_ntt := varchar2_ntt( 'A','B','C','C' );
3 BEGIN
4 DBMS_OUTPUT.PUT_LINE( 'Cardinality = ' || CARDINALITY( nt ) );
5 DBMS_OUTPUT.PUT_LINE( 'True cardinality = ' || CARDINALITY(
SET( nt ) ) );
6 END;
7 /
Cardinality = 4
True cardinality = 3
is (not) empty
As we saw with CARDINALITY, we can now explicitly test for collections with a specific
number of elements. We can also test for empty collections using the IS (NOT) EMPTY
condition. As with the IS (NOT) A SET examples earlier in this article, the restriction on
NULL collections applies. This is demonstrated below (remember that these are
available in SQL).
SQL> DECLARE
2
3 nt_null varchar2_ntt;
4 nt_initialised varchar2_ntt := varchar2_ntt();
5 nt_populated varchar2_ntt := varchar2_ntt('A','B','C');
6
7 PROCEDURE test (
8 test_in IN VARCHAR2,
9 result_in IN BOOLEAN
10 ) IS
11 BEGIN
12 DBMS_OUTPUT.PUT( RPAD('[' || test_in || ']',40) || '=> ');
13 DBMS_OUTPUT.PUT_LINE(CASE
14 WHEN result_in
15 THEN 'TRUE'
16 ELSE 'FALSE'
17 END);
18 END test;
19
20 BEGIN
21
22 /* First test the NULL collection... */
23 test( 'nt_null IS EMPTY', nt_null IS EMPTY );
24 test( 'nt_null IS NOT EMPTY', nt_null IS NOT EMPTY );
25
26
27 /* Test the valid collections... */
28 test( 'nt_initialised IS EMPTY', nt_initialised IS EMPTY );
29 test( 'nt_populated IS NOT EMPTY', nt_populated IS NOT EMPTY );
30
31 /* Delete the populated collection and repeat test for emptiness... */
32 nt_populated.DELETE;
33 test( 'nt_populated (deleted) IS NOT EMPTY', nt_populated IS NOT EMPTY );
34
35 END;
36 /
[nt_null IS EMPTY] => FALSE
[nt_null IS NOT EMPTY] => FALSE
[nt_initialised IS EMPTY] => TRUE
[nt_populated IS NOT EMPTY] => TRUE
[nt_populated (deleted) IS NOT EMPTY] => FALSE
We can see from the example that a NULL collection is neither empty nor not empty.
This is something to consider when you pass potentially-NULL collections around as
parameters for use in IF/CASE tests or SQL predicates.
submultiset conditions
SQL> DECLARE
2
3 nt1 varchar2_ntt := varchar2_ntt( 'C','D','E' );
4 nt2 varchar2_ntt := varchar2_ntt( 'C','D','E','E' );
5 nt3 varchar2_ntt := varchar2_ntt( 'A','B','C' );
6
7 PROCEDURE test (
8 test_in IN VARCHAR2,
9 result_in IN BOOLEAN
10 ) IS
11 BEGIN
12 DBMS_OUTPUT.PUT( RPAD('[' || test_in || ']',30) || '=> ');
13 DBMS_OUTPUT.PUT_LINE(CASE
14 WHEN result_in
15 THEN 'TRUE'
16 ELSE 'FALSE'
17 END);
18 END test;
19
20 BEGIN
21
22 /* SUBMULTISET tests... */
23 test( 'nt1 SUBMULTISET OF nt2', nt1 SUBMULTISET OF nt2 );
24 test( 'nt2 SUBMULTISET OF nt3', nt2 SUBMULTISET OF nt3 );
25 test( 'nt2 NOT SUBMULTISET OF nt3', nt2 NOT SUBMULTISET OF nt3 );
26
27 /* MEMBER OF tests... */
28 test( 'D MEMBER OF nt1', 'D' MEMBER OF nt1 );
29 test( 'F MEMBER OF nt3', 'F' MEMBER OF nt3 );
30
31 END;
32 /
[nt1 SUBMULTISET OF nt2] => TRUE
[nt2 SUBMULTISET OF nt3] => FALSE
[nt2 NOT SUBMULTISET OF nt3] => TRUE
[D MEMBER OF nt1] => TRUE
[F MEMBER OF nt3] => FALSE
multiset operators
Oracle 10g has introduced a number of MULTISET operators. These combine two or
more collections of the same type and return a collection that satisfies the operator. In
high-level terms, these can largely be considered as equivalent to the SQL SET
operators (MINUS, UNION [ALL], INTERSECT), but limited to collections rather than
sets of data resulting from SQL queries. The MULTISET operators are listed below with
their SQL equivalents where applicable.
SQL> DECLARE
2
3 nt0 varchar2_ntt := varchar2_ntt('A','B','C','C');
4 nt1 varchar2_ntt := varchar2_ntt('C','C','D','E','E');
5
6 PROCEDURE display (
7 type_in IN VARCHAR2,
8 nt_in IN varchar2_ntt
9 ) IS
10 i PLS_INTEGER;
11 BEGIN
12 DBMS_OUTPUT.PUT( CHR(10) || RPAD(type_in,50,'.') );
13 i := nt_in.FIRST;
14 WHILE i IS NOT NULL LOOP
15 DBMS_OUTPUT.PUT( nt_in(i) || ' ' );
16 i := nt_in.NEXT(i);
17 END LOOP;
18 DBMS_OUTPUT.NEW_LINE;
19 END display;
20
21 BEGIN
22
23 display('MULTISET UNION',
24 nt0 MULTISET UNION nt1 );
25
26 display('MULTISET UNION ALL (SAME AS MULTISET UNION)',
27 nt0 MULTISET UNION ALL nt1 );
28
29 display('MULTISET UNION DISTINCT',
30 nt0 MULTISET UNION DISTINCT nt1 );
31
32 display('MULTISET EXCEPT (nt0 -> nt1)',
33 nt0 MULTISET EXCEPT nt1 );
34
35 display('MULTISET EXCEPT (nt1 -> nt0)',
36 nt1 MULTISET EXCEPT nt0 );
37
38 display('MULTISET EXCEPT DISTINCT (nt1 -> nt0)',
39 nt1 MULTISET EXCEPT DISTINCT nt0 );
40
41 display('MULTISET INTERSECT',
42 nt0 MULTISET INTERSECT nt1 );
43
44 display('MULTISET INSERSECT DISTINCT',
45 nt0 MULTISET INTERSECT DISTINCT nt1 );
46
47 display('SET OF AN INTERSECT (SAME AS INTERSECT DISTINCT)',
48 SET( nt0 MULTISET INTERSECT nt1 ) );
49
50 END;
51 /
MULTISET UNION....................................A B C C C C D E E
MULTISET INTERSECT................................C C
The following examples simply demonstrate that MULTISETs are available in SQL.
SQL> SELECT *
2 FROM TABLE( varchar2_ntt('A','B','C')
3 MULTISET UNION DISTINCT
4 varchar2_ntt('B','C','D') );
COLUMN_VALUE
------------------------------
A
B
C
D
4 rows selected.
MULTISET_EXCEPT
------------------------------
VARCHAR2_NTT('A')
1 row selected.
powermultiset functions
For the completists amongst us, the POWERMULTISET functions enable us to create a
range of collections from a single input collection. Unlike all other new features in this
article, the POWERMULTISET functions are available in SQL only. There are two
functions in this "family", as follows.
POWERMULTISET
POWERMULTISET_BY_CARDINALITY
Type created.
We now have a collection type that represents multiple instances of our original
collection type. We can now demonstrate POWERMULTISET, as follows. To keep the
output short, we'll return a collection of all multisets available from an input collection of
just three elements.
PWRMLTSET
--------------------------------------------------------------------------------
VARCHAR2_NTTS(VARCHAR2_NTT('A'), VARCHAR2_NTT('B'),
VARCHAR2_NTT('A', 'B'), VARC
HAR2_NTT('C'), VARCHAR2_NTT('A', 'C'), VARCHAR2_NTT('B', 'C'),
VARCHAR2_NTT('A',
'B', 'C'))
1 row selected.
As a collection in SQL, this is probably of little use to most of us. We are more likely to
use POWERMULTISET in a TABLE expression, if at all. For this, the "collection type of
collection type" (VARCHAR2_NTTS in the above example), is not required. The
following example drops the VARCHAR2_NTTS type and demonstrates an alternative
invocation of POWERMULTISET.
Type dropped.
SQL> SELECT *
2 FROM TABLE(POWERMULTISET(varchar2_ntt('A','B','C')));
COLUMN_VALUE
------------------------------
VARCHAR2_NTT('A')
VARCHAR2_NTT('B')
VARCHAR2_NTT('A', 'B')
VARCHAR2_NTT('C')
VARCHAR2_NTT('A', 'C')
VARCHAR2_NTT('B', 'C')
VARCHAR2_NTT('A', 'B', 'C')
7 rows selected.
SQL> SELECT *
2 FROM TABLE(
3 POWERMULTISET_BY_CARDINALITY(
4 varchar2_ntt('A','B','C','D','D'), 3));
COLUMN_VALUE
------------------------------
VARCHAR2_NTT('A', 'B', 'C')
VARCHAR2_NTT('A', 'B', 'D')
VARCHAR2_NTT('A', 'C', 'D')
VARCHAR2_NTT('B', 'C', 'D')
VARCHAR2_NTT('A', 'B', 'D')
VARCHAR2_NTT('A', 'C', 'D')
VARCHAR2_NTT('B', 'C', 'D')
VARCHAR2_NTT('A', 'D', 'D')
VARCHAR2_NTT('B', 'D', 'D')
VARCHAR2_NTT('C', 'D', 'D')
10 rows selected.
So far we've been using a simple collection type for all examples. Furthermore, all
examples work equally in SQL or PL/SQL. The same is not true for complex collection
types (i.e. based on object types with multiple attributes). For complex collections to be
compared to other collections or be used with any of the SUBMULTISET conditions,
MULTISET operators or POWERMULTISET and SET functions, we must enable their
elements to be sorted. We do this by including a single MAP order method in the type
definition. In the following example, we'll create a complex type without a MAP order
method and see how Oracle resolves the sorting when we call the SET function.
Type created.
Type created.
SQL> SELECT *
2 FROM TABLE(
3 SET( complex_ntt(complex_ot('data', 'more data', 1),
4 complex_ot('data', 'some data', 2),
5 complex_ot('data', 'dupe data', 3),
6 complex_ot('data', 'dupe data', 3)) ));
3 rows selected.
Interestingly, we have the correct answer without a MAP method. It appears as though
SQL "understands" our complex type. We can now test the same types in a simple
PL/SQL block.
SQL> DECLARE
2 nt complex_ntt := complex_ntt(complex_ot('data', 'more data', 1),
3 complex_ot('data', 'some data', 2),
4 complex_ot('data', 'dupe data', 3),
5 complex_ot('data', 'dupe data', 3));
6 BEGIN
7 DBMS_OUTPUT.PUT_LINE( 'Complex collection has ' ||
8 TO_CHAR(CARDINALITY(SET(nt))) ||
9 ' distinct elements.' );
10 END;
11 /
TO_CHAR(CARDINALITY(SET(nt))) ||
*
ERROR at line 8:
ORA-06550: line 8, column 46:
PLS-00306: wrong number or types of arguments in call to 'SET'
ORA-06550: line 7, column 4:
PL/SQL: Statement ignored
In PL/SQL, Oracle cannot determine the SET from the complex collection (although the
error message is not particularly helpful in this instance). This time we'll create the
requisite MAP method and try again.
Type created.
Type created.
SQL> DECLARE
2 nt complex_ntt := complex_ntt(complex_ot('data', 'more data', 1),
3 complex_ot('data', 'some data', 2),
4 complex_ot('data', 'dupe data', 3),
5 complex_ot('data', 'dupe data', 3));
6 BEGIN
7 DBMS_OUTPUT.PUT_LINE( 'Complex collection has ' ||
8 TO_CHAR(CARDINALITY(SET(nt))) ||
9 ' distinct elements.' );
10 END;
11 /
Complex collection has 3 distinct elements.