The goal of this article is to show the principal
features about the collections.
We will see how to declare, initialize and handle
collection with SQL and PL/SQL.
All the examples have been runned on a 10.1.0.2.0
database release.
This
is what the documentation says about collections:
“A collection
is an ordered group of elements, all of the same type. It is a general concept that
encompasses lists, arrays, and other familiar datatypes. Each element has a
unique subscript that determines its position in the collection.
PL/SQL offers these
collection types:
·
Index-by
tables, also known as associative
arrays, let you look up elements using arbitrary numbers and strings
for subscript values. (They are similar to hash tables in other
programming languages.)
·
Nested
tables hold an
arbitrary number of elements. They use sequential numbers as subscripts. You
can define equivalent SQL types, allowing nested tables to be stored in
database tables and manipulated through SQL.
·
Varrays (short for variable-size arrays) hold a fixed number
of elements (although you can change the number of elements at runtime). They
use sequential numbers as subscripts. You can define equivalent SQL types,
allowing varrays to be stored in database tables. They can be stored and
retrieved through SQL, but with less flexibility than nested tables.
Although collections can
have only one dimension, you can model multi-dimensional arrays by creating
collections whose elements are also collections.
To use collections in an
application, you define one or more PL/SQL types, then define variables of
those types. You can define collection types in a procedure, function, or
package. You can pass collection variables as parameters, to move data between
client-side applications and stored subprograms.
To look up data that is more
complex than single values, you can store PL/SQL records or SQL object types in
collections. Nested tables and varrays can also be attributes of object types.”
Index-by tables cannot be stored in database tables, so they are non-persistent.
You
cannot use them in a SQL statement and are available only in PL/SQL blocks.
Nested
tables and Varrays are persistent. You can use
the CREATE TYPE statement to create them in the database, you can read and
write them from/to a database column.
Nested
tables and Varrays must have been initialized before you can use them.
TYPE type_name
IS TABLE OF element_type
[NOT NULL];
With
nested tables declared within PL/SQL, element_type can
be any PL/SQL datatype except : REF CURSOR
Nested
tables declared in SQL (CREATE TYPE) have additional restrictions. They cannot use the following element types:
q
BINARY_INTEGER
, PLS_INTEGER
q
BOOLEAN
q
LONG
, LONG
RAW
q
NATURAL
, NATURALN
q
POSITIVE
, POSITIVEN
q
REF
CURSOR
q
SIGNTYPE
q
STRING
Declare
TYPE TYP_NT_NUM IS TABLE OF NUMBER ;
SQL
CREATE [OR
REPLACE] TYPE TYP_NT_NUM IS TABLE OF NUMBER ;
3.2 Varrays
TYPE type_name
IS {VARRAY | VARYING ARRAY} (size_limit
)
OFelement_type
[NOT NULL];
size_limit
is a positive integer
literal representing the maximum number of elements in the array.
PL/SQL
Declare
TYPE TYP_V_CHAR IS VARRAY(10) OF VARCHAR2(20) ;
SQL
CREATE [OR REPLACE] TYPE TYP_V_CHAR IS VARRAY(10) OF VARCHAR2(20) ;
TYPE type_name
IS TABLE OF element_type
[NOT NULL]
INDEX BY [BINARY_INTEGER | PLS_INTEGER | VARCHAR2(size_limit
)];
INDEX BY key_type
;
The key_type can be numeric, either BINARY_INTEGER
or PLS_INTEGER(9i)
.
It can also be VARCHAR2
or
one of its subtypes VARCHAR
, STRING
, or LONG
. You must specify the length of a VARCHAR2
-based key, except for LONG
which is equivalent to declaring a key type of VARCHAR2(32760)
.
The types RAW
, LONG RAW
, ROWID
, CHAR
, and CHARACTER
are not allowed as keys for
an associative array.
Declare
TYPE
TYP_TAB_VAR IS TABLE OF VARCHAR2(50) INDEX BY BINARY_INTEGER ;
Only Nested tables and varrays need
initialization.
To initialize a collection, you use
the “constructor” of the collection which name is the same as the collection.
4.1 Nested
tables
Declare
TYPE
TYP_NT_NUM IS TABLE OF NUMBER ;
Nt_tab
TYP_NT_NUM ;
Begin
Nt_tab :=
TYP_NT_NUM( 5, 10, 15, 20 ) ;
End ;
Declare
TYPE
TYP_V_DAY IS VARRAY(7) OF VARCHAR2(15) ;
v_tab
TYP_V_DAY ;
Begin
v_tab :=
TYP_NT_NUM(
‘Sunday’,’Monday’,’Tuesday’,’Wedneday’,’Thursday’,’Friday’,’Saturday’ ) ;
End ;
It is not required to initialize all the elements of a collection. You
can either initialize no element. In this case, use an empty constructor.
v_tab := TYP_NT_NUM() ;
This collection is empty, which is different than a NULL collection (not initialized).
Declare
TYPE TYP_TAB
IS TABLE OF NUMBER INDEX BY PLS_INTEGER ;
my_tab TYP_TAB ;
Begin
my_tab(1) :=
5 ;
my_tab(2) :=
10 ;
my_tab(3) :=
15 ;
End ;
While the collection is not initialized (Nested tables and Varrays), it
is not possible to manipulate it.
You can test if a collection is initialized:
Declare
TYPE TYP_VAR_TAB is VARRAY(30) of varchar2(100) ;
tab1 TYP_VAR_TAB ; -- declared but not initialized
Begin
If Tab1 IS NULL Then
-- NULL collection, have to initialize it --
Tab1 := TYP_VAR_TAB('','','','','','','','','','');
End if ;
-- Now, we can handle the collection --
End ;
To access an element of a collection, we need to use a subscript value that indicates the unique element of the collection.
The subscript is of type integer or
varchar2.
Declare
Type TYPE_TAB_EMP IS
TABLE OF Varchar2(60) INDEX BY BINARY_INTEGER ;
emp_tab TYPE_TAB_EMP ;
i pls_integer ;
Begin
For i in 0..10 Loop
emp_tab( i+1 ) := 'Emp ' || ltrim( to_char( i ) ) ;
End loop ;
End ;
Declare
Type TYPE_TAB_DAYS IS
TABLE OF PLS_INTEGER INDEX BY VARCHAR2(20) ;
day_tab TYPE_TAB_DAYS ;
Begin
day_tab( 'Monday' ) :=
10 ;
day_tab( 'Tuesday' ) :=
20 ;
day_tab( 'Wednesday' ) := 30 ;
End ;
It is possible to assign values of a collection to another collection if
they are of the same type.
Declare
Type TYPE_TAB_EMP IS TABLE OF EMP%ROWTYPE INDEX BY BINARY_INTEGER ;
Type TYPE_TAB_EMP2 IS TABLE OF EMP%ROWTYPE INDEX BY BINARY_INTEGER ;
tab1 TYPE_TAB_EMP := TYPE_TAB_EMP( ... );
tab2 TYPE_TAB_EMP := TYPE_TAB_EMP( ... );
tab3 TYPE_TAB_EMP2 := TYPE_TAB_EMP2( ... );
Begin
tab2 := tab1 ; -- OK
tab3 := tab1 ; -- Error : types not similar
...
End ;
Until the 10g release, collections cannot be directly compared for
equality or inequality.
The 10g release allows to do some comparaisons between collections:
You can compare collections of same type to verify if they ar equals or
not equals
DECLARE
TYPE Colors IS TABLE OF VARCHAR2(64);
primaries Colors := Colors('Blue','Green','Red');
rgb Colors := Colors('Red','Green','Blue');
traffic_light Colors := Colors('Red','Green','Amber');
BEGIN
-- We can use = or !=, but not < or >.
-- 2 collections are equal even if the membersare not in the same order.
IF primaries = rgb THEN
dbms_output.put_line('OK, PRIMARIES & RGB have same members.');
END IF;
IF rgb != traffic_light THEN
dbms_output.put_line('RGB & TRAFFIC_LIGHT have different members');
END IF;
END;
You can also apply some operators on the collections:
DECLARE
TYPE nested_typ IS TABLE OF NUMBER;
nt1 nested_typ := nested_typ(1,2,3);
nt2 nested_typ := nested_typ(3,2,1);
nt3 nested_typ := nested_typ(2,3,1,3);
nt4 nested_typ := nested_typ(1,2,4);
reponse BOOLEAN;
combien NUMBER;
PROCEDURE verif(test BOOLEAN DEFAULT NULL, label IN VARCHAR2
DEFAULT NULL, quantity NUMBER DEFAULT NULL) IS
BEGIN
IF test IS NOT NULL THEN
dbms_output.put_line(label || ' -> ' || CASE test WHEN
TRUE THEN 'True' WHEN FALSE THEN 'False' END);
END IF;
IF quantity IS NOT NULL THEN
dbms_output.put_line(quantity);
END IF;
END;
BEGIN
reponse := nt1 IN (nt2,nt3,nt4); --
true, nt1 correspond to nt2
verif(test => reponse, label => 'nt1 IN (nt2,nt3,nt4)');
reponse := nt1 SUBMULTISET OF nt3; --
true, all elements correpond
verif(test => reponse, label => 'nt1 SUBMULTISET OF nt3');
reponse := nt1 NOT SUBMULTISET OF nt4; -- true
verif(test => reponse, label => 'nt1 NOT SUBMULTISET OF
nt4');
combien := CARDINALITY(nt3); --
number of elements of nt3
verif(quantity => combien);
combien := CARDINALITY(SET(nt3)); -- number of distinct elements
verif(quantity => combien);
reponse := 4 MEMBER OF nt1; -- false, no corresponding element
verif(test => reponse, label => '4 MEMBER OF nt1');
reponse := nt3 IS A SET; -- false,
nt3 have duplicated elements
verif(test => reponse, label => 'nt3 IS A SET' );
reponse := nt3 IS NOT A SET; --
true, nt3 have diplicated elements
verif(test => reponse, label => 'nt3 IS NOT A SET' );
reponse := nt1 IS EMPTY; -- false,
nt1 have elements
verif(test => reponse, label => 'nt1 IS EMPTY' );
END;
nt1 IN (nt2,nt3,nt4) -> True
nt1 SUBMULTISET OF nt3 -> True
nt1 NOT SUBMULTISET OF nt4 -> True
4
3
4 MEMBER OF nt1 -> False
nt3 IS A SET -> False
nt3 IS NOT A SET -> True
nt1 IS EMPTY -> False
We can use the following methods on a collection:
·
EXISTS
·
COUNT
·
LIMIT
·
FIRST and LAST
·
PRIOR and NEXT
·
EXTEND
·
TRIM
·
DELETE
A collection method
is a built-in function or procedure that operates on collections and is called
using dot notation.
collection_name.method_name[(parameters)]
Collection methods cannot be called
from SQL statements.
Only the EXISTS method can be used on
a NULL collection.
all other methods applied on a null collection raise the
COLLECTION_IS_NULL error.
6.1 EXISTS(index)
Returns TRUE if the index element exists in the collection, else it
returns FALSE.
Use this method to be sure you are doing a valid operation on the collection.
This method does not raise the SUBSCRIPT_OUTSIDE_LIMIT exception if used on an
element that does not exists in the collection.
If my_collection.EXISTS(10)
Then
My_collection.DELETE(10) ;
End if ;
6.2
COUNT
Returns the number of elements in a collection.
SQL> Declare
2 TYPE
TYP_TAB IS TABLE OF NUMBER;
3 my_tab
TYP_TAB := TYP_TAB( 1, 2, 3, 4, 5 );
4 Begin
5 Dbms_output.Put_line( 'COUNT = ' ||
To_Char( my_tab.COUNT ) ) ;
6 my_tab.DELETE(2) ;
7 Dbms_output.Put_line( 'COUNT = ' ||
To_Char( my_tab.COUNT ) ) ;
8 End ;
9 /
COUNT = 5
COUNT = 4
PL/SQL procedure successfully completed.
6.3
LIMIT
Returns the maximum number of elements that a varray can contain.
Return NULL for Nested tables and Index-by tables
SQL> Declare
2 TYPE TYP_ARRAY IS ARRAY(30) OF NUMBER ;
3 my_array TYP_ARRAY :=
TYP_ARRAY( 1, 2, 3 ) ;
4 Begin
5 dbms_output.put_line( 'Max array size is '
|| my_array.LIMIT ) ;
6 End;
7 /
Max array size is 30
6.4
FIRST and LAST
Returns the first or last subscript of a collection.
If the collection is empty, FIRST
and LAST
return NULL
SQL> Declare
2 TYPE
TYP_TAB IS TABLE OF NUMBER;
3 my_tab
TYP_TAB := TYP_TAB( 1, 2, 3, 4, 5 );
4 Begin
5 For i IN my_tab.FIRST .. my_tab.LAST
Loop
6
Dbms_output.Put_line( 'my_tab(' || Ltrim(To_Char(i)) || ') = ' ||
To_Char( my_tab(i) ) ) ;
7 End loop ;
8 End ;
9
10 /
my_tab(1) = 1
my_tab(2) = 2
my_tab(3) = 3
my_tab(4) = 4
my_tab(5) = 5
PL/SQL procedure successfully completed.
SQL> Declare
2 TYPE
TYP_TAB IS TABLE OF PLS_INTEGER INDEX BY VARCHAR2(1);
3 my_tab
TYP_TAB;
4 Begin
5 For i in 65 .. 69 Loop
6 my_tab( Chr(i) ) := i ;
7 End loop ;
8 Dbms_Output.Put_Line( 'First= ' ||
my_tab.FIRST || ' Last= ' ||
my_tab.LAST ) ;
9 End ;
10 /
First= A Last= E
PL/SQL procedure successfully completed.
6.5
PRIOR(index) and NEXT(index)
Returns the previous or next subscript of the index element.
If the index element has no predecessor, PRIOR(index)
returns NULL
. Likewise, if index
has no successor, NEXT(index)
returns NULL
.
SQL> Declare
2 TYPE
TYP_TAB IS TABLE OF PLS_INTEGER INDEX BY VARCHAR2(1) ;
3 my_tab
TYP_TAB ;
4 c
Varchar2(1) ;
5 Begin
6 For i in 65 .. 69 Loop
7 my_tab( Chr(i) ) := i ;
8 End loop ;
9 c := my_tab.FIRST ; -- first element
10 Loop
11 Dbms_Output.Put_Line( 'my_tab(' || c || ') = ' || my_tab(c) ) ;
12 c := my_tab.NEXT(c) ; -- get the successor element
13 Exit When c IS NULL ; -- end of collection
14 End loop ;
15 End ;
16 /
my_tab(A) = 65
my_tab(B) = 66
my_tab(C) = 67
my_tab(D) = 68
my_tab(E) = 69
PL/SQL procedure successfully completed.
Use the PRIOR() or NEXT() method to be sure that you do not access an
invalid element:
SQL> Declare
2 TYPE
TYP_TAB IS TABLE OF PLS_INTEGER ;
3 my_tab
TYP_TAB := TYP_TAB( 1, 2, 3, 4, 5 );
4 Begin
5 my_tab.DELETE(2) ; -- delete an element of the collection
6 For i in my_tab.FIRST .. my_tab.LAST Loop
7 Dbms_Output.Put_Line( 'my_tab(' ||
Ltrim(To_char(i)) || ') = ' || my_tab(i) ) ;
8 End loop ;
9 End ;
10 /
my_tab(1) = 1
Declare
*
ERROR at line 1:
ORA-01403: no data found
ORA-06512: at line 7
In this example, we get an error because one element of the collection
was deleted.
One solution is to use the PRIOR()/NEXT() method:
SQL> Declare
2 TYPE
TYP_TAB IS TABLE OF PLS_INTEGER ;
3 my_tab
TYP_TAB := TYP_TAB( 1, 2, 3, 4, 5 );
4 v Pls_Integer ;
5 Begin
6
my_tab.DELETE(2) ;
7 v := my_tab.first ;
8 Loop
9 Dbms_Output.Put_Line( 'my_tab(' ||
Ltrim(To_char(v)) || ') = ' || my_tab(v) ) ;
10 v := my_tab.NEXT(v) ; -- get the
next valid subscript
11 Exit When v IS NULL ;
12 End loop ;
13 End ;
14 /
my_tab(1) = 1
my_tab(3) = 3
my_tab(4) = 4
my_tab(5) = 5
PL/SQL procedure successfully completed.
Another solution is to test if the index exists
before use it:
SQL> Declare
2 TYPE
TYP_TAB IS TABLE OF PLS_INTEGER ;
3 my_tab
TYP_TAB := TYP_TAB( 1, 2, 3, 4, 5 );
4 Begin
5 my_tab.DELETE(2) ;
6 For i IN my_tab.FIRST .. my_tab.LAST Loop
7 If my_tab.EXISTS(i) Then
8 Dbms_Output.Put_Line( 'my_tab(' ||
Ltrim(To_char(i)) || ') = ' || my_tab(i) ) ;
9 End if ;
10 End loop ;
11 End ;
12 /
my_tab(1) = 1
my_tab(3) = 3
my_tab(4) = 4
my_tab(5) = 5
PL/SQL procedure successfully completed.
Used to extend a collection (add new elements)
·
EXTEND
appends one null element to a collection.
·
EXTEND(n)
appends n
null elements to a collection.
·
EXTEND(n,i)
appends n
copies of the i
th element to a collection.
SQL> Declare
2 TYPE TYP_NES_TAB is table of Varchar2(20) ;
3 tab1 TYP_NES_TAB ;
4 i
Pls_Integer ;
5 Procedure Print( i in Pls_Integer ) IS
6 BEGIN Dbms_Output.Put_Line( 'tab1(' ||
ltrim(to_char(i)) ||') = ' || tab1(i) ) ; END ;
7 Procedure PrintAll IS
8 Begin
9 Dbms_Output.Put_Line( '* Print all
collection *' ) ;
10 For i IN tab1.FIRST..tab1.LAST Loop
11 If tab1.EXISTS(i) Then
12 Dbms_Output.Put_Line( 'tab1(' ||
ltrim(to_char(i)) ||') = ' || tab1(i) ) ;
13 End if ;
14 End loop ;
15 End ;
16 Begin
17 tab1 := TYP_NES_TAB('One') ;
18 i := tab1.COUNT ;
19 Dbms_Output.Put_Line( 'tab1.COUNT = ' ||
i ) ;
20 Print(i) ;
21 -- the
following line raise an error because the second index does not exists in the
collection --
22 -- tab1(2) :=
'Two' ;
23 -- Add one
empty element --
24 tab1.EXTEND ;
25 i := tab1.COUNT ;
26 tab1(i) := 'Two' ; Printall ;
27 -- Add two
empty elements --
28 tab1.EXTEND(2) ;
29 i := i + 1 ;
30 tab1(i) := 'Three' ;
31 i := i + 1 ;
32 tab1(i) := 'Four' ; Printall ;
33 -- Add three
elements with the same value as element 4 --
34 tab1.EXTEND(3,1) ;
35 i := i + 3 ; Printall ;
36 End;
/
tab1.COUNT = 1
tab1(1) = One
* Print all collection *
tab1(1) = One
tab1(2) = Two
* Print all collection *
tab1(1) = One
tab1(2) = Two
tab1(3) = Three
tab1(4) = Four
* Print all collection *
tab1(1) = One
tab1(2) = Two
tab1(3) = Three
tab1(4) = Four
tab1(5) = One
tab1(6) = One
tab1(7) = One
PL/SQL procedure successfully completed.
6.7 TRIM[(n)]
Used to decrease the size of a collection
·
TRIM
removes one element from the end of a collection.
·
TRIM(n)
removes n
elements from the end of a collection.
SQL> Declare
2 TYPE TYP_TAB is table of varchar2(100) ;
3 tab
TYP_TAB ;
4 Begin
5 tab := TYP_TAB( 'One','Two','Three' ) ;
6 For i in tab.first..tab.last Loop
7 dbms_output.put_line( 'tab(' || ltrim(
to_char( i ) ) || ') = ' || tab(i) ) ;
8 End loop ;
9 -- add 3 element with second element value --
10 dbms_output.put_line( '* add 3 elements
*' ) ;
11 tab.EXTEND(3,2) ;
12 For i in tab.first..tab.last Loop
13 dbms_output.put_line( 'tab(' || ltrim(
to_char( i ) ) || ') = ' || tab(i) ) ;
14 End loop ;
15 -- suppress the
last element --
16 dbms_output.put_line( '* suppress the last
element *' ) ;
17 tab.TRIM ;
18 For i in tab.first..tab.last Loop
19 dbms_output.put_line( 'tab(' || ltrim(
to_char( i ) ) || ') = ' || tab(i) ) ;
20 End loop ;
21 End;
22 /
tab(1) = One
tab(2) = Two
tab(3) = Three
* add 3 elements *
tab(1) = One
tab(2) = Two
tab(3) = Three
tab(4) = Two
tab(5) = Two
tab(6) = Two
* suppress the last element *
tab(1) = One
tab(2) = Two
tab(3) = Three
tab(4) = Two
tab(5) = Two
PL/SQL procedure successfully completed.
If you try to suppress more elements than the
collection contents, you get a SUBSCRIPT_BEYOND_COUNT exception.
·
DELETE
removes all elements from a collection.
·
DELETE(n)
removes the n
th element from an associative array with a numeric key or a nested
table. If the associative array has a string key, the element corresponding to
the key value is deleted. If n
is null, DELETE(n)
does nothing.
·
DELETE(n,m)
removes all elements in the range m..n
from an associative array or nested table. If m
is larger than n
or if m
or n
is null, DELETE(n,m)
does nothing
Caution :
LAST returns the greatest subscript of a collection and COUNT
returns the number of elements of a collection.
If you delete some elements, LAST !=
COUNT.
Suppression of all the elements
SQL> Declare
2 TYPE TYP_TAB is table of varchar2(100) ;
3 tab
TYP_TAB ;
4 Begin
5 tab := TYP_TAB( 'One','Two','Three' ) ;
6 dbms_output.put_line( 'Suppression of all
elements' ) ;
7 tab.DELETE ;
8 dbms_output.put_line( 'tab.COUNT = ' ||
tab.COUNT) ;
9 End;
10 /
Suppression of all elements
tab.COUNT = 0
PL/SQL procedure successfully completed.
Suppression of the second element
SQL> Declare
2 TYPE TYP_TAB is table of varchar2(100) ;
3 tab
TYP_TAB ;
4 Begin
5 tab := TYP_TAB( 'One','Two','Three' ) ;
6 dbms_output.put_line( 'Suppression of the
2nd element' ) ;
7 tab.DELETE(2) ;
8 dbms_output.put_line( 'tab.COUNT = ' ||
tab.COUNT) ;
9 dbms_output.put_line( 'tab.LAST = ' || tab.LAST) ;
10 For i IN tab.FIRST .. tab.LAST Loop
11 If tab.EXISTS(i) Then
12 dbms_output.put_line( tab(i) ) ;
13 End if ;
14 End loop ;
15 End;
16 /
Suppression of the 2nd element
tab.COUNT = 2
tab.LAST = 3
One
Three
PL/SQL procedure successfully completed.
Caution:
For Varrays, you can suppress only the last element.
If the element does not exists, no exception is
raised.
DECLARE
TYPE NumList
IS TABLE OF NUMBER;
nums NumList; -- atomically null
BEGIN
/* Assume
execution continues despite the raised exceptions. */
nums(1) :=
1; -- raises
COLLECTION_IS_NULL (1)
nums :=
NumList(1,2); -- initialize table
nums(NULL)
:= 3 -- raises VALUE_ERROR (2)
nums(0) := 3; -- raises SUBSCRIPT_OUTSIDE_LIMIT (3)
nums(3) :=
3; -- raises
SUBSCRIPT_BEYOND_COUNT (4)
nums.DELETE(1); --
delete element 1
IF nums(1) =
1 THEN ... -- raises NO_DATA_FOUND (5)
A collection is a one-dimension table.
You can have multi-dimension tables by creating collection of
collection.
SQL> Declare
2 TYPE TYP_TAB is table of NUMBER index by
PLS_INTEGER ;
3 TYPE TYP_TAB_TAB is table of TYP_TAB
index by PLS_INTEGER ;
4 tab1 TYP_TAB_TAB ;
5 Begin
6 For i IN 1 .. 3 Loop
7 For j IN 1 .. 2 Loop
8
tab1(i)(j) := i + j ;
9 dbms_output.put_line( 'tab1(' || ltrim(to_char(i))
10 || ')(' || ltrim(to_char(j))
11 || ') = ' ||
tab1(i)(j) ) ;
12 End loop ;
13 End loop ;
14 End;
15 /
tab1(1)(1) = 2
tab1(1)(2) = 3
tab1(2)(1) = 3
tab1(2)(2) = 4
tab1(3)(1) = 4
tab1(3)(2) = 5
PL/SQL procedure successfully completed.
SQL> Declare
2 TYPE TYP_TAB is table of DEPT%ROWTYPE
index by PLS_INTEGER ;
3 tb_dept TYP_TAB ;
4 rec
DEPT%ROWTYPE ;
5 Cursor
CDEPT IS Select * From DEPT ;
6 Begin
7 Open CDEPT ;
8 Loop
9 Fetch
CDEPT Into rec ;
10 Exit
When CDEPT%NOTFOUND ;
11 tb_dept(CDEPT%ROWCOUNT) := rec ;
12 End loop ;
13 For i IN tb_dept.FIRST .. tb_dept.LAST
Loop
14 dbms_output.put_line( tb_dept(i).DNAME
|| ' - ' ||tb_dept(i).LOC ) ;
15 End loop ;
16 End;
17 /
ACCOUNTING - NEW YORK
RESEARCH - DALLAS
SALES - CHICAGO
OPERATIONS - BOSTON
PL/SQL procedure successfully completed.
Nested tables and Varrays can be stored in a database column of
relational or object table.
To manipulate collection from SQL, you have to create the types in the
database with the CREATE TYPE statement.
CREATE [OR REPLACE] TYPE [schema. .] type_name
{ IS | AS } TABLE OF datatype;
CREATE [OR REPLACE] TYPE [schema. .] type_name
{ IS | AS } { VARRAY | VARYING ARRAY } ( limit ) OF datatype;
One or several collections can be stored in a database column.
Let’s see an example with a relational table.
You want to make a table that store the invoices and the currents
invoice lines of the company.
You need to define the invoice line type as following:
-- type of invoice line --
CREATE TYPE TYP_LIG_ENV AS
OBJECT (
lig_num Integer,
lig_code
Varchar2(20),
lig_Pht
Number(6,2),
lig_Tva Number(3,1),
ligQty Integer
);
-- nested table of invoice lines --
CREATE TYPE TYP_TAB_LIG_ENV AS TABLE OF TYP_LIG_ENV ;
Then create the invoice table as following:
-- table of invoices --
CREATE TABLE INVOICE (
inv_num Number(9),
inv_numcli Number(6),
inv_date Date,
inv_line
TYP_TAB_LIG_ENV ) –- lines collection
NESTED TABLE
inv_line STORE AS inv_line_table ;
You can query the USER_TYPES view to get information on the types
created in the database.
-- show all types --
SQL> select type_name, typecode, attributes from
user_types
2 /
TYPE_NAME TYPECODE ATTRIBUTES
------------------------------
------------------------------ ----------
TYP_LIG_ENV OBJECT
5
TYP_TAB_LIG_ENV COLLECTION 0
SQL>
You can query the USER_COLL_TYPES view to get information on the
collections created in the database.
-- show collections --
SQL> select type_name, coll_type,
elem_type_owner, elem_type_name from user_coll_types
2 /
TYPE_NAME COLL_TYPE ELEM_TYPE_OWNER ELEM_TYPE_NAME
------------------------- ----------------------
------------------------- -------
TYP_TAB_LIG_ENV TABLE
TEST
TYP_LIG_ENV
You can query the USER_TYPE_ATTRS view to get information on the
collection attributes.
-- show collection attributes --
SQL> select type_name, attr_name,
attr_type_name, length, precision, scale, attr_no
2 from user_type_attrs
3 /
TYPE_NAME
ATTR_NAME ATTR_TYPE_ LENGTH
PRECISION SCALE ATTR_NO
--------------- --------------- ---------- ---------- ----------
---------- ----------
TYP_LIG_ENV
LIG_NUM INTEGER
1
TYP_LIG_ENV
LIG_CODE VARCHAR2 20 2
TYP_LIG_ENV
LIG_PHT NUMBER 6 2 3
TYP_LIG_ENV
LIG_TVA NUMBER 3 1 4
TYP_LIG_ENV
LIGQTY INTEGER
5
You can enforce constraints on each attribute of a collection
-- constraints on collection attributes
--
alter table inv_line_table
add constraint
lignum_notnull CHECK( lig_num IS NOT NULL ) ;
alter table inv_line_table
add constraint ligcode_unique UNIQUE( lig_code ) ;
alter table inv_line_table
add constraint ligtva_check CHECK( lig_tva IN(
5.0,19.6 ) ) ;
Constraints on the whole collection
-- constraints on the whole collection --
alter table invoice
add constraint invoice_notnull CHECK( inv_line IS
NOT NULL )
SQL> select constraint_name, constraint_type,
table_name
2 from
user_constraints
3 where table_name IN ('INVOICE','INV_LINE_TABLE')
4 order
by table_name
5 /
CONSTRAINT_NAME C TABLE_NAME
------------------------------ -
------------------------------
LIGNUM_NOTNULL C INV_LINE_TABLE
LIGCODE_UNIQUE U INV_LINE_TABLE
LIGTVA_CHECK C INV_LINE_TABLE
SYS_C0011658 U INVOICE
INVOICE_NOTNULL C INVOICE
SQL> select constraint_name, column_name,
table_name
2 from
user_cons_columns
3 where table_name IN ('INVOICE','INV_LINE_TABLE')
4 order
by table_name
5 /
CONSTRAINT_NAME COLUMN_NAME TABLE_NAME
------------------------------ --------------------
------------------
LIGNUM_NOTNULL LIG_NUM INV_LINE_TABLE
LIGCODE_UNIQUE LIG_CODE INV_LINE_TABLE
LIGTVA_CHECK
LIG_TVA INV_LINE_TABLE
SYS_C0011658 SYS_NC0000400005$ INVOICE
INVOICE_NOTNULL SYS_NC0000400005$ INVOICE
INVOICE_NOTNULL INV_LINE INVOICE
6 rows selected.
Use the INSERT statement with all the constructors needed for the
collection
SQL> INSERT INTO INVOICE
2 VALUES
3 (
4 1
5 ,1000
6 ,SYSDATE
7 , TYP_TAB_LIG_ENV -- Table of
objects constructor
8 (
9 TYP_LIG_ENV( 1 ,'COD_01', 1000, 5.0,
1 ) -– object constructor
10 )
11 )
12 /
1 row created.
Use the INSERT INTO TABLE statement
INSERT INTO TABLE
( SELECT the_collection
FROM the_table WHERE ... )
The sub query must return a single collection row.
SQL> INSERT INTO TABLE (SELECT inv_line FROM
INVOICE WHERE inv_num = 1)
2 VALUES( TYP_LIG_ENV( 2 ,'COD_02', 50, 5.0,
10 ) )
3 /
1 row created.
You can add more than one element in a collection by using the SELECT
statement instead of the VALUES keyword.
INSERT INTO TABLE (SELECT inv_line FROM INVOICE WHERE
inv_num = 1)
SELECT nt.* FROM TABLE (SELECT inv_line FROM INVOICE
WHERE inv_num = 1) nt
/
8.2.1 Nested table
Use the UPDATE TABLE statement
UPDATE TABLE
( SELECT the_collection
FROM the_table WHERE ... ) alias
SET
Alias.col_name = ...
WHERE ...
The sub query must return a single collection row.
Update a single row of the collection
SQL> UPDATE TABLE (SELECT inv_line FROM INVOICE
WHERE inv_num = 1) nt
2 SET
nt.ligqty = 10
3 WHERE
nt.lig_num = 1
4 /
1 row updated.
Update all the rows of the collection
SQL> UPDATE TABLE (SELECT inv_line FROM INVOICE
WHERE inv_num = 1) nt
2 SET
nt.lig_pht = nt.lig_pht * .1
3 /
2 rows updated.
It is not possible to update one element of a VARRAY
collection with SQL.
You cannot use the TABLE keyword for this purpose
(because Varrays are not stored in particular table like Nested tables).
So, a single VARRAY element of a collection must be
updated within a PL/SQL block:
-- varray of invoice lines --
CREATE TYPE TYP_VAR_LIG_ENV AS VARRAY(5) OF
TYP_LIG_ENV ;
-- table of invoices with varray --
CREATE TABLE
INVOICE_V (
inv_num
Number(9),
inv_numcli
Number(6),
inv_date
Date,
inv_line TYP_VAR_LIG_ENV ) ;
-- insert
a row --
Insert into INVOICE_V
Values
(
1, 1000, SYSDATE,
TYP_VAR_LIG_ENV
(
TYP_LIG_ENV( 1, 'COD_01', 1000, 5, 1 ),
TYP_LIG_ENV( 2, 'COD_02', 500, 5, 10 ),
TYP_LIG_ENV( 3, 'COD_03', 10, 5, 100 )
)
) ;
SQL> -- Query the varray
collection --
SQL> Declare
2 v_table
TYP_VAR_LIG_ENV ;
3 LC$Head
Varchar2(200) ;
4 LC$Lig Varchar2(200) ;
5 Begin
6 LC$Head
:= 'Num Code Pht Tva Qty' ;
7 Select inv_line Into v_table From
INVOICE_V Where inv_num = 1 For Update of inv_line ;
8 dbms_output.put_line ( LC$Head ) ;
9 For i IN v_table.FIRST .. v_table.LAST
Loop
10 LC$Lig := Rpad(To_char(
v_table(i).lig_num ),3) || ' '
11 || Rpad(v_table(i).lig_code, 10) ||
' '
12 || Rpad(v_table(i).lig_pht,10) || '
'
13 || Rpad(v_table(i).lig_tva,10) || '
'
14 || v_table(i).ligqty ;
15 dbms_output.put_line( LC$Lig ) ;
16 End loop ;
17 End ;
18 /
Num Code
Pht Tva Qty
1 COD_01 1000 5 1
2 COD_02 500 5 10
3 COD_03 10 5 100
PL/SQL procedure successfully completed..
Update the second line of the varray
to change the quantity
SQL> Declare
2 v_table
TYP_VAR_LIG_ENV ;
3 Begin
4 Select inv_line
5 Into
v_table
6 From
INVOICE_V
7 Where
inv_num = 1
8 For Update of inv_line ;
9 v_table(2).ligqty := 2 ; -- update the second element
10 Update INVOICE_V Set inv_line =
v_table Where inv_num = 1 ;
11 End ;
12 /
PL/SQL procedure successfully completed.
Display the new varray:
SQL> -- Query the varray
collection --
SQL> Declare
2 v_table
TYP_VAR_LIG_ENV ;
3 LC$Head
Varchar2(200) ;
4 LC$Lig Varchar2(200) ;
5 Begin
6 LC$Head
:= 'Num Code Pht Tva Qty' ;
7 Select inv_line Into v_table From
INVOICE_V Where inv_num = 1 For Update of inv_line ;
8 dbms_output.put_line ( LC$Head ) ;
9 For i IN v_table.FIRST .. v_table.LAST
Loop
10 LC$Lig := Rpad(To_char(
v_table(i).lig_num ),3) || ' '
11 || Rpad(v_table(i).lig_code, 10) ||
' '
12 || Rpad(v_table(i).lig_pht,10) || '
'
13 || Rpad(v_table(i).lig_tva,10) || '
'
14 || v_table(i).ligqty ;
15 dbms_output.put_line( LC$Lig ) ;
16 End loop ;
17 End ;
18 /
Num Code
Pht Tva Qty
1 COD_01 1000 5 1
2 COD_02 500 5 2
3 COD_03 10 5 100
PL/SQL procedure successfully completed.
8.3.1 Nested table
Use the DELETE FROM TABLE statement
Delete a single collection row
DELETE FROM TABLE
( SELECT the_collection
FROM the_table WHERE ... ) alias
WHERE alias.col_name = ...
SQL> DELETE FROM TABLE (SELECT inv_line FROM
INVOICE WHERE inv_num = 1) nt
2 WHERE nt.lig_num = 2
3 /
1 row deleted.
Delete all the collection rows
SQL> DELETE FROM TABLE (SELECT inv_line FROM
INVOICE WHERE inv_num = 1) nt
2 /
1 row deleted.
SQL> Declare
2 TYPE TYP_REC IS RECORD
3 (
4 inv_num INVOICE.inv_num%Type,
5 inv_numcli INVOICE.inv_numcli%Type,
6 inv_date INVOICE.inv_date%Type,
7 inv_line INVOICE.inv_line%Type –- collection line
8 );
9
rec_inv TYP_REC ;
10 Cursor C_INV IS Select *
From INVOICE ;
11 Begin
12 Open C_INV ;
13 Loop
14 Fetch C_INV into rec_inv ;
15 Exit when C_INV%NOTFOUND ;
16 For i IN 1 .. rec_inv.inv_line.LAST Loop –- loop through
the collection lines
17 dbms_output.put_line( 'Numcli/Date '
|| rec_inv.inv_numcli || '/' || rec_inv.inv_date
18 || ' Line ' ||
rec_inv.inv_line(i).lig_num
19 || ' code ' ||
rec_inv.inv_line(i).lig_code || ' Qty '
20 ||
To_char(rec_inv.inv_line(i).ligqty) ) ;
21 End loop ;
22 End loop ;
23 End ;
24 /
Numcli/Date 1000/11/11/05 Line 1 code COD_01 Qty 1
Numcli/Date 1000/11/11/05 Line 2 code COD_02 Qty 10
PL/SQL procedure successfully completed.
8.3.2 Varray
Varrays are more complicated to handle.
It is not possible to delete a single element in a Varray collection.
To do the job, you need a PL/SQL block and a temporary Varray that keep
only the lines that are not deleted.
SQL> Declare
2 v_table
TYP_VAR_LIG_ENV ;
3 v_tmp v_table%Type := TYP_VAR_LIG_ENV() ;
4 ind
pls_integer := 1 ;
5 Begin
6 -- select the
collection --
7 Select inv_line
8 Into
v_table
9 From
INVOICE_V
10 Where
inv_num = 1
11 For Update of inv_line ;
12 -- Extend the
temporary varray --
13 v_tmp.EXTEND(v_table.LIMIT) ;
14 For i IN v_table.FIRST .. v_table.LAST
Loop
15 If v_table(i).lig_num <> 2 Then
16 v_tmp(ind) := v_table(i) ; ind
:= ind + 1 ;
17 End if ;
18 End loop ;
19
20 Update INVOICE_V Set inv_line = v_tmp
Where inv_num = 1 ;
21 End ;
22 /
PL/SQL procedure successfully completed.
Display the new collection:
SQL> Declare
2 v_table
TYP_VAR_LIG_ENV ;
3 LC$Head
Varchar2(200) ;
4 LC$Lig Varchar2(200) ;
5 Begin
6 LC$Head
:= 'Num Code Pht Tva Qty' ;
7 Select inv_line Into v_table From
INVOICE_V Where inv_num = 1 For Update of inv_line ;
8 dbms_output.put_line ( LC$Head ) ;
9 For i IN v_table.FIRST .. v_table.LAST
Loop
10 LC$Lig := Rpad(To_char(
v_table(i).lig_num ),3) || ' '
11 || Rpad(v_table(i).lig_code, 10) || ' '
12 || Rpad(v_table(i).lig_pht,10) || '
'
13 || Rpad(v_table(i).lig_tva,10) || '
'
14 || v_table(i).ligqty ;
15 dbms_output.put_line( LC$Lig ) ;
16 End loop ;
17 End ;
18 /
Num Code
Pht Tva Qty
1 COD_01 1000 5 1
3 COD_03 10 5 100
PL/SQL procedure successfully completed.
The second line of the Varray has been deleted.
Here is a Procedure that do the job with any Varray collection
CREATE OR REPLACE
PROCEDURE DEL_ELEM_VARRAY
(
PC$Table in Varchar2, -- Main table name
PC$Pk
in Varchar2, -- PK to identify the main
table row
PC$Type
in Varchar2, -- Varray TYPE
PC$Coll
in Varchar2, -- Varray column name
PC$Index in Varchar2, -- value of PK
PC$Col
in Varchar2, -- Varray column
PC$Value in Varchar2 -- Varray
column value to delete
)
IS
LC$Req Varchar2(2000);
Begin
LC$Req := 'Declare'
|| ' v_table ' || PC$Type || ';'
|| ' v_tmp v_table%Type := ' || PC$Type || '()
;'
|| ' ind
pls_integer := 1 ;'
|| 'Begin'
|| ' Select ' || PC$Coll
|| ' Into
v_table'
|| ' From
' || PC$Table
|| ' Where ' || PC$Pk || '=''' || PC$Index ||
''''
|| ' For Update of ' || PC$Coll || ';'
|| ' v_tmp.EXTEND(v_table.LIMIT) ;'
|| ' For i IN v_table.FIRST .. v_table.LAST
Loop'
|| ' If v_table(i).' || PC$Col|| '<>'''
|| PC$Value || ''' Then'
|| '
v_tmp(ind) := v_table(i) ; ind := ind + 1 ;'
|| ' End if ;'
|| ' End loop ;'
|| ' Update ' || PC$Table || ' Set ' || PC$Coll
|| ' = v_tmp Where ' || PC$Pk || '=''' || PC$Index || ''';'
|| ' End;' ;
Execute immediate LC$Req ;
End ;
/
Let’s delete the third element of the Varray:
SQL> Begin
2 DEL_ELEM_VARRAY
3 (
4 'INVOICE_V',
5 'inv_num',
6 'TYP_VAR_LIG_ENV',
7
'inv_line',
8 '1',
9 'lig_num',
10 '3'
11 );
12 End ;
13 /
PL/SQL procedure successfully completed.
SQL> select * from INVOICE
2 /
INV_NUM
INV_NUMCLI INV_DATE
---------- ---------- --------
INV_LINE(LIG_NUM, LIG_CODE, LIG_PHT, LIG_TVA, LIGQTY)
------------------------------------------------------------------------------------------
3 1001 11/11/05
TYP_TAB_LIG_ENV()
2 1002 12/11/05
TYP_TAB_LIG_ENV(TYP_LIG_ENV(1, 'COD_03', 1000, 5, 1))
1 1000 11/11/05
TYP_TAB_LIG_ENV(TYP_LIG_ENV(1, 'COD_01', 1000, 5, 1),
TYP_LIG_ENV(2, 'COD_02', 50, 5, 10))
Let’s try another syntax:
SQL> SELECT t1.inv_num, t1.inv_numcli, t1.inv_date,
t2.* FROM invoice t1, TABLE(t1.inv_line) t2
2 ORDER BY t1.inv_num, t2.lig_num desc
3 /
INV_NUM
INV_NUMCLI INV_DATE LIG_NUM
LIG_CODE LIG_PHT
LIG_TVA LIGQTY
---------- ---------- -------- ----------
-------------------- ---------- ---------- ----------
1 1000 11/11/05 2 COD_02 50
5 10
1 1000 11/11/05 1 COD_01 1000 5 1
2 1002 12/11/05 1 COD_03 1000
5 1
We can see that the collection is treated as a table with the TABLE keyword.
The collection could be sorted on any column.
SQL> SELECT
t1.inv_num, t1.inv_numcli, t1.inv_date, t2.* FROM invoice t1,
TABLE(t1.inv_line) t2
2 WHERE
t1.inv_num = 1
3 ORDER BY t1.inv_num, t2.lig_num desc
4 /
INV_NUM
INV_NUMCLI INV_DATE LIG_NUM
LIG_CODE LIG_PHT LIG_TVA LIGQTY
---------- ---------- -------- ----------
-------------------- ---------- ---------- ----------
1 1000 11/11/05 2 COD_02 50
5 10
1 1000 11/11/05 1 COD_01 1000
5 1
SQL> SELECT
t1.inv_num, t1.inv_numcli, t1.inv_date, t2.* FROM invoice t1,
TABLE(t1.inv_line) t2
2 WHERE
t1.inv_num = 1
3 AND
t2.lig_code = 'COD_01'
4 /
INV_NUM INV_NUMCLI INV_DATE LIG_NUM LIG_CODE LIG_PHT
LIG_TVA LIGQTY
---------- ---------- -------- ----------
-------------------- ---------- ---------- ----------
1 1000 11/11/05 1 COD_01 1000
5 1
SQL> select t2.* from invoice t1,TABLE(t1.inv_line)
t2
2 /
LIG_NUM
LIG_CODE LIG_PHT LIG_TVA LIGQTY
---------- -------------------- ---------- ----------
----------
1
COD_03 1000 5 1
1 COD_01 1000
5 1
2
COD_02 50 5 10
Use the SELECT FROM TABLE statement
SELECT FROM TABLE
( SELECT the_collection
FROM the_table WHERE ... )
SQL> select
* from TABLE(SELECT inv_line FROM INVOICE WHERE inv_num = 1)
2 /
LIG_NUM
LIG_CODE LIG_PHT LIG_TVA LIGQTY
---------- -------------------- ---------- ----------
----------
1 COD_01 1000
5 1
2
COD_02 50 5 10
Another syntax:
SQL> Select t2.* from invoice
t1,TABLE(t1.inv_line) t2
2 Where
t1.inv_numcli = 1000
3 /
LIG_NUM
LIG_CODE LIG_PHT LIG_TVA LIGQTY
---------- -------------------- ---------- ----------
----------
1
COD_01 1000 5 1
2
COD_02 50 5 10
SQL> Declare
2 TYPE TYP_REC IS RECORD
3 (
4 num
INV_LINE_TABLE.LIG_NUM%Type,
5 code
INV_LINE_TABLE.LIG_CODE%Type,
6 pht
INV_LINE_TABLE.LIG_PHT%Type,
7 tva
INV_LINE_TABLE.LIG_TVA%Type,
8 qty
INV_LINE_TABLE.LIGQTY%Type
9 );
10 -- Table of records --
11 TYPE TAB_REC IS TABLE OF TYP_REC ;
12 t_rec
TAB_REC ;
13 Begin
14 -- Store the lines into the table of
records --
15 Select *
16 BULK COLLECT
17 Into
t_rec
18 from
TABLE(SELECT inv_line FROM INVOICE WHERE inv_num = 1) nt ;
19 -- Print the record attributes of each
line--
20 For i IN t_rec.FIRST .. t_rec.LAST Loop
21 dbms_output.put_line( '** Line = ' || t_rec(i).num || ' **' ) ;
22 dbms_output.put_line( 'Code = ' || t_rec(i).code ) ;
23 dbms_output.put_line( 'Price = ' || t_rec(i).pht ) ;
24 dbms_output.put_line( 'Tax rate = ' ||
t_rec(i).tva ) ;
25 dbms_output.put_line( 'Quantity = ' ||
t_rec(i).qty ) ;
26 End loop ;
27 End ;
28 /
** Line = 1 **
Code =
COD_01
Price = 1000
Tax rate = 5
Quantity = 1
** Line = 2 **
Code =
COD_02
Price = 50
Tax rate = 5
Quantity = 10
PL/SQL procedure successfully completed.
SQL> SELECT nt.lig_code, nt.lig_pht
2 FROM
TABLE (SELECT inv_line FROM INVOICE WHERE inv_num = 1) nt
3 WHERE
nt.lig_num = 1
4 /
LIG_CODE
LIG_PHT
-------------------- ----------
COD_01 1000
Another syntax:
SQL> Select t2.* from invoice
t1,TABLE(t1.inv_line) t2
2 Where
t1.inv_numcli = 1000
3 And t2.lig_num = 1
4 /
LIG_NUM
LIG_CODE LIG_PHT LIG_TVA LIGQTY
---------- -------------------- ---------- ----------
----------
1
COD_01 1000 5 1
SQL> Declare
2 TYPE t_rec IS RECORD
3 (
4 num
INV_LINE_TABLE.LIG_NUM%Type,
5 code
INV_LINE_TABLE.LIG_CODE%Type,
6 pht
INV_LINE_TABLE.LIG_PHT%Type,
7 tva
INV_LINE_TABLE.LIG_TVA%Type,
8 qty
INV_LINE_TABLE.LIGQTY%Type
9 );
10 rec
t_rec ;
11 Begin
12 -- Store the line into the record --
13 Select *
14 Into
rec
15 from
TABLE(SELECT inv_line FROM INVOICE WHERE inv_num = 1) nt
16 Where
nt.lig_num = 1 ;
17 -- Print the record attributes --
18 dbms_output.put_line( 'Code = ' || rec.code ) ;
19 dbms_output.put_line( 'Price = '
|| rec.pht ) ;
20 dbms_output.put_line( 'Tax rate = ' ||
rec.tva ) ;
21 dbms_output.put_line( 'Quantity = ' ||
rec.qty ) ;
22 End ;
23 /
Code =
COD_01
Price = 1000
Tax rate = 5
Quantity = 1
PL/SQL procedure successfully completed.
All the collection’s rows
SQL> SELECT v.inv_numcli, v.inv_date, nt.lig_code,
nt.lig_pht
2 FROM
INVOICE v,
3 TABLE (SELECT inv_line FROM
INVOICE WHERE inv_num = 1) nt
4 WHERE
v.inv_num = 1
5 /
INV_NUMCLI
INV_DATE LIG_CODE LIG_PHT
----------
-------- -------------------- ----------
1000 11/11/05 COD_01 1000
1000 11/11/05 COD_02 50
A particular collection’s row
SQL> SELECT
v.inv_numcli, v.inv_date, nt.lig_code, nt.lig_pht
2 FROM
INVOICE v,
3 TABLE (SELECT inv_line FROM INVOICE
WHERE inv_num = 1) nt
4 WHERE
v.inv_num = 1
5 AND
nt.lig_num = 1
6 /
INV_NUMCLI
INV_DATE LIG_CODE LIG_PHT
---------- -------- -------------------- ----------
1000
11/11/05 COD_01 1000
SQL> Declare
2 invoice_rec INVOICE%ROWTYPE ;
3 LC$Print
Varchar2(512) ;
4 Begin
5 -- Select the INVOICE line --
6 Select *
7 Into
invoice_rec
8 From
INVOICE
9 Where
inv_numcli = 1000 ;
10 -- Print the parent and collection
attributes--
11 For i IN invoice_rec.inv_line.FIRST ..
invoice_rec.inv_line.LAST Loop
12 LC$Print := invoice_rec.inv_numcli
13 || ' - ' ||
To_Char(invoice_rec.inv_date,'DD/MM/YYYY')
14 || ' - ' || invoice_rec.inv_line(i).lig_num
15 || ' - ' ||
invoice_rec.inv_line(i).lig_code
16 || ' - ' || invoice_rec.inv_line(i).lig_pht
17 || ' - ' || invoice_rec.inv_line(i).lig_tva
18 || ' - ' || invoice_rec.inv_line(i).ligqty
;
19 dbms_output.put_line( LC$Print ) ;
20 End loop ;
21 End ;
22 /
1000 - 11/11/2005 - 1 - COD_01 - 1000 - 5 - 1
1000 - 11/11/2005 - 2 - COD_02 - 50 - 5 - 10
PL/SQL procedure successfully completed.
Let’s insert a row with an empty collection:
SQL> INSERT INTO INVOICE
2 VALUES
3 (
4 3
5 ,1001
6 ,SYSDATE
7
, TYP_TAB_LIG_ENV() -- Empty collection
8 )
9 /
1 row created.
SQL> SELECT v.inv_numcli, v.inv_date, nt.lig_code,
nt.lig_pht
2 FROM
INVOICE v,
3 TABLE (SELECT inv_line FROM INVOICE
WHERE inv_num = 1) nt
4 WHERE
v.inv_num = 1
5 /
INV_NUMCLI
INV_DATE LIG_CODE LIG_PHT
---------- -------- -------------------- ----------
1000
11/11/05 COD_01 1000
1000
11/11/05 COD_02 50
The client 1001 does not appears in
the query
You can use NESTED CURSOR to get information on rows where collection is
NULL or EMPTY
SQL> SELECT
2 v.inv_numcli,
3 v.inv_date,
4 CURSOR( SELECT nt.lig_code, nt.lig_pht
FROM TABLE (inv_line) nt)
5 FROM
INVOICE v
6 /
INV_NUMCLI INV_DATE CURSOR(SELECTNT.LIG_
---------- -------- --------------------
1001
11/11/05 CURSOR STATEMENT : 3
CURSOR STATEMENT : 3
no rows selected
INV_NUMCLI INV_DATE CURSOR(SELECTNT.LIG_
---------- -------- --------------------
1000
11/11/05 CURSOR STATEMENT : 3
CURSOR STATEMENT : 3
LIG_CODE LIG_PHT
-------------------- ----------
COD_01 1000
COD_02 50
1001
11/11/05 CURSOR STATEMENT : 3
CURSOR STATEMENT : 3
no rows selected
SQL> -- count of number
of elements in the collection --
SQL> Select COUNT(*) from TABLE( SELECT inv_line
FROM INVOICE WHERE inv_num = 1 )
2 /
COUNT(*)
----------
2
SQL> -- maximum quantity
of all the collection rows --
SQL> Select MAX(ligqty) from TABLE( SELECT inv_line
FROM INVOICE WHERE inv_num = 1 )
2 /
MAX(LIGQTY)
-----------
10
SQL> -- Number of
collection lines for each invoice --
SQL> Select
i.inv_numcli, COUNT(nt.lig_num)
2 From
invoice i, TABLE( i.inv_line) nt
3 Group by i.inv_numcli
4 /
INV_NUMCLI COUNT(NT.LIG_NUM)
---------- -----------------
1000 2
1002 1
SQL> -- Number of
distinct product code for each invoice --
SQL> Select
i.inv_numcli, COUNT(DISTINCT(nt.lig_code))
2 From
invoice i, TABLE( i.inv_line) nt
3 Group by i.inv_numcli
4 /
INV_NUMCLI COUNT(DISTINCT(NT.LIG_CODE))
---------- ----------------------------
1000 2
1002 1
SQL> -- total price for
each invoice --
SQL> Select
i.inv_numcli, SUM(nt.lig_pht + (( nt.lig_pht * nt.lig_tva ) / 100.0))
2 From
invoice i, TABLE( i.inv_line) nt
3 Group by i.inv_numcli
4 /
INV_NUMCLI
SUM(NT.LIG_PHT+((NT.LIG_PHT*NT.LIG_TVA)/100.0))
----------
-----------------------------------------------
1000 1102,5
1002
1050
SQL> -- lines for
customers 1000 and 10002 --
SQL> Select nt.lig_code, nt.ligqty
2 From
invoice i, TABLE( i.inv_line ) nt
3 Where
i.inv_numcli = 1000
4 UNION
5 Select nt.lig_code, nt.ligqty
6 From
invoice i, TABLE( i.inv_line ) nt
7 Where
i.inv_numcli = 1002
8 /
LIG_CODE LIGQTY
--------------------
----------
COD_01 1
COD_02 10
COD_03 1
9.1 BULK COLLECT
This keyword ask the SQL engine to return all the rows in one or several
collections before returning to the PL/SQL engine.
So, there is one single roundtrip for all the rows between SQL and
PL/SQL engine.
BULK COLLECT cannot be use on the client-side
(Select)(Fetch)(execute immediate) … BULK COLLECT Into collection_name
[,collection_name, …] [LIMIT max_lines] ;
LIMIT is used to limit the number of rows returned
SQL> set serveroutput on
SQL> Declare
2 TYPE
TYP_TAB_EMP IS TABLE OF EMP.EMPNO%Type ;
3 Temp_no TYP_TAB_EMP ; -- collection of
EMP.EMPNO%Type
4 Cursor
C_EMP is Select empno From EMP ;
5 Pass
Pls_integer := 1 ;
6 Begin
7 Open C_EMP ;
8 Loop
9 -- Fetch
the table 3 by 3 --
10 Fetch C_EMP BULK COLLECT into Temp_no LIMIT
3 ;
11 Exit When C_EMP%NOTFOUND ;
12 For i In Temp_no.first..Temp_no.last
Loop
13 dbms_output.put_line( 'Pass ' ||
to_char(Pass) || ' Empno= ' || Temp_no(i) ) ;
14 End loop ;
15 Pass := Pass + 1 ;
16 End Loop ;
17 End ;
18 /
Pass 1 Empno= 9999
Pass 1 Empno= 7369
Pass 1 Empno= 7499
Pass 2 Empno= 7521
Pass 2 Empno= 7566
Pass 2 Empno= 7654
Pass 3 Empno= 7698
Pass 3 Empno= 7782
Pass 3 Empno= 7788
Pass 4 Empno= 7839
Pass 4 Empno= 7844
Pass 4 Empno= 7876
Pass 5 Empno= 7900
Pass 5 Empno= 7902
Pass 5 Empno= 7934
PL/SQL procedure successfully completed.
You can use the LIMIT keyword to preserve your rollback segment:
Declare
TYPE
TYP_TAB_EMP IS TABLE OF EMP.EMPNO%Type ;
Temp_no TYP_TAB_EMP ;
Cursor
C_EMP is Select empno From EMP ;
max_lig Pls_Integer := 3 ;
Begin
Open C_EMP ;
Loop
Fetch C_EMP BULK COLLECT into Temp_no LIMIT max_lig ;
Forall i In Temp_no.first..Temp_no.last
Update EMP set SAL = Round(SAL * 1.1)
Where empno = Temp_no(i) ;
Commit ; --
Commit every 3 rows
Temp_no.DELETE ;
Exit When C_EMP%NOTFOUND ;
End Loop ;
End ;
BULK COLLECT can also be used to retrieve the result of a DML statement
that uses the RETURNING INTO clause:
SQL> Declare
2 TYPE
TYP_TAB_EMPNO IS TABLE OF EMP.EMPNO%Type ;
3 TYPE
TYP_TAB_NOM IS TABLE OF
EMP.ENAME%Type ;
4 Temp_no TYP_TAB_EMPNO ;
5 Tnoms
TYP_TAB_NOM ;
6 Begin
7 -- Delete rows
and return the result into the collection --
8 Delete From EMP where sal > 3000
9 RETURNING empno, ename BULK COLLECT
INTO Temp_no, Tnoms ;
10 For i in Temp_no.first..Temp_no.last Loop
11 dbms_output.put_line( 'Fired employee
: ' || To_char( Temp_no(i) ) || ' ' || Tnoms(i) ) ;
12 End
loop ;
13 End ;
14 /
Fired employee : 7839 KING
PL/SQL procedure successfully completed.
9.2 FORALL
FORALL index IN min_index .. max_index [SAVE EXCEPTION] sql_order
This instruction allows to compute all the rows of a collection in a
single pass.
FORALL cannot be use on the client-side and can proceed one and only one
statement at a time.
SQL> Declare
2 TYPE TYP_TAB_TEST IS
TABLE OF TEST%ROWTYPE ;
3 tabrec TYP_TAB_TEST ;
4 CURSOR
C_test is select A,
B From TEST ;
5 Begin
6 -- Load the collection from the table --
7 Select A, B
BULK COLLECT into tabrec From
TEST ;
8
9 -- Insert into the table from the collection --
10 Forall i in
tabrec.first..tabrec.last
11 Insert into TEST values
tabrec(i) ;
12
13 -- Update the table from the collection --
14 For i in tabrec.first..tabrec.last Loop
15 tabrec(i).B := tabrec(i).B * 2 ;
16 End loop ;
17
18 -- Use of cursor --
19 Open C_test ;
20 Fetch
C_test BULK COLLECT Into tabrec ;
21 Close C_test
;
22
23 End ;
24 /
It is not allowed to use the FORALL statement and an UPDATE order that
use the SET ROW functionality
SQL> Declare
2 TYPE
TAB_EMP is table of EMP%ROWTYPE ;
3 emp_tab TAB_EMP ;
4 Cursor
CEMP is Select * From EMP ;
5 Begin
6 Open
CEMP;
7 Fetch CEMP BULK COLLECT Into emp_tab ;
8 Close CEMP ;
9
10 Forall i in emp_tab.first..emp_tab.last
11 Update EMP set row = emp_tab(i) where EMPNO = emp_tab(i).EMPNO
; -- ILLEGAL
12
13 End ;
14 /
Update EMP
set row = emp_tab(i) where EMPNO =
emp_tab(i).EMPNO ; -- ILLEGAL
*
ERROR at line 11:
ORA-06550: line 11, column 52:
PLS-00436: implementation restriction: cannot
reference fields of BULK In-BIND
table of records
You have to use a standard FOR LOOP statement:
For i in emp_tab.first..emp_tab.last loop
Update EMP set row =
emp_tab(i) where EMPNO = emp_tab(i).EMPNO ;
End loop ;
Or use simple collections:
Declare
TYPE
TAB_EMPNO is table of
EMP.EMPNO%TYPE ;
TYPE
TAB_EMPNAME is table of EMP.ENAME%TYPE ;
no_tab
TAB_EMPNO ;
na_tab TAB_EMPNAME ;
Cursor CEMP is Select EMPNO,
ENAME From EMP ;
Begin
Open
CEMP;
Fetch CEMP BULK COLLECT Into no_tab, na_tab
;
Close CEMP ;
Forall i in no_tab.first..no_tab.last
Update EMP set ENAME = na_tab(i) where
EMPNO = no_tab(i) ;
End ;
If an error is raised by the FORALL statement, all the rows processed
are rolled back.
You can save the rows that raised an error (and do not abort the
process) with the SAVE EXCEPTION keyword.
Every exception
raised during execution is stored in the %BULK_EXCEPTIONS collection.
This is a collection of records composed by two attributes:
·
%BULK_EXCEPTIONS(n).ERROR_INDEX which contains the index number
·
%BULK_EXCEPTIONS(n).ERROR_CODE which contains the error code
The total amount
of errors raised by the FORALL instruction is stored in the SQL%BULK_EXCEPTIONS.COUNT
attribute.
SQL> Declare
2 TYPE TYP_TAB IS TABLE OF Number ;
3 tab TYP_TAB := TYP_TAB( 2, 0, 1, 3, 0, 4, 5 ) ;
4 nb_err Pls_integer ;
5 Begin
6 Forall i in tab.first..tab.last SAVE EXCEPTIONS
7 Delete from EMP where SAL = 5 / tab(i) ;
8 Exception
9 When others then
10 nb_err := SQL%BULK_EXCEPTIONS.COUNT ;
11 dbms_output.put_line( to_char( nb_err ) || ' Errors ' ) ;
12 For i in 1..nb_err Loop
13 dbms_output.put_line( 'Index ' || to_char( SQL%BULK_EXCEPTIONS(i).ERROR_INDEX ) || ' Er
ror : '
14 || to_char( SQL%BULK_EXCEPTIONS(i).ERROR_CODE ) ) ;
15 End loop ;
16 End ;
17 /
2 Errors
Index 2 Error : 1476
Index 5 Error : 1476
PL/SQL procedure successfully completed.
The %BULK_ROWCOUNT attribute.
This is an INDEX-BY table that contains for each SQL order the number of rows processed.
If no row is impacted, SQL%BULK_ROWCOUNT(n) equals 0.
SQL> Declare
2 TYPE
TYP_TAB_TEST IS TABLE OF TEST%ROWTYPE ;
3 TYPE
TYP_TAB_A IS TABLE OF TEST.A%TYPE ;
4 TYPE
TYP_TAB_B IS TABLE OF TEST.B%TYPE ;
5 tabrec TYP_TAB_TEST ;
6
taba TYP_TAB_A ;
7 tabb
TYP_TAB_B ;
8 total
Pls_integer := 0 ;
9 CURSOR C_test is select A, B From TEST ;
10 begin
11 -- Load the
collection from the table --
12 Select A, B BULK COLLECT into tabrec From
TEST ;
13
14 -- Insert rows
--
15 Forall i in tabrec.first..tabrec.last
16 insert into TEST values tabrec(i) ;
17
18 For i in tabrec.first..tabrec.last Loop
19 total := total +
SQL%BULK_ROWCOUNT(i) ;
20 End loop ;
21
22 dbms_output.put_line('Total insert : ' ||
to_char( total) ) ;
23
24 total := 0 ;
25 -- Upadate rows
--
26 For i in tabrec.first..tabrec.last loop
27 update TEST set row = tabrec(i) where A = tabrec(i).A ;
28 End loop ;
29
30 For i in tabrec.first..tabrec.last Loop
31 total := total +
SQL%BULK_ROWCOUNT(i) ;
32 End loop ;
33
34 dbms_output.put_line('Total upfdate : '
|| to_char( total) ) ;
35
36 End ;
37 /
Total insert : 20
Total upfdate : 20
PL/SQL procedure successfully completed.
10. Oracle Forms and collections
Oracle Forms, in its actual version (10.1.2) does not handle collections
internally.
However, we can handle this kind of object with a few lines of code.
NESTED_TABLE.fmb
This is a MASTER/DETAIL module.
The first block (Invoice) is based on the INVOICE table
The second block (Nested table) is based on a FROM clause
At initialization, the dummy FROM clause is specified as:
Select 1,2,3,4,5 from Dual.
In the When-New-Record-Instance of the first block, we change dynamically this property:
Declare
LC$Req Varchar2(256) ;
Begin
If :INVOICE.INV_NUM Is not null Then
-- Dynamic query on nested table block --
LC$Req := '(SELECT nt.lig_num, nt.lig_code, nt.lig_pht, nt.lig_tva, nt.ligqty FROM TABLE ( SELECT inv_line FROM INVOICE WHERE inv_num = ' || :INVOICE.INV_NUM || ') nt)' ;
Go_Block('NT' );
Clear_Block ;
Set_Block_Property( 'NT', QUERY_DATA_SOURCE_NAME, LC$Req ) ;
:System.message_level := 25 ;
Execute_Query ;
:System.message_level := 0 ;
Go_Block('INVOICE') ;
Else
Go_Block('NT' );
Clear_Block ;
Go_Block('INVOICE') ;
End if ;
End ;
All we have to do is to overload the standard Forms process for Insert,
Update and Delete line of the collection.
This job is done in the ON-xxx triggers of the detail block.
Trigger ON-INSERT:
-- Insert a line into the collection --
INSERT INTO TABLE
(
SELECT
inv_line
FROM
INVOICE
WHERE
inv_num =
:INVOICE.inv_num
)
Values
(
TYP_LIG_ENV(
:NT.lig_num, :NT.lig_code, :NT.lig_pht, :NT.lig_tva, :NT.ligqty )
);
-- Update the line in collection --
UPDATE TABLE
(
SELECT
inv_line
FROM
INVOICE
WHERE
inv_num =
:INVOICE.inv_num
) nt
SET
VALUE(nt) =
TYP_LIG_ENV( :NT.lig_num, :NT.lig_code, :NT.lig_pht, :NT.lig_tva, :NT.ligqty )
WHERE
nt.lig_num =
:NT.lig_num
;
-- Delete the line from the collection --
DELETE FROM TABLE
(
SELECT
inv_line
FROM
INVOICE
WHERE
inv_num =
:INVOICE.inv_num
) nt
WHERE
nt.lig_num =
:NT.lig_num
;
You
can download the collection.zip
Unzip the collection.zip file
Create the database objects with the /scripts/install.sql script
Open the NESTED_TABLE.fmb module ( Oracle Forms 10.1.2 )
Compile the module and run.