Collections
and Bulk Binds
Introduction to
Object Types and Records
Collections
When to use What
Using Collection Methods
(Count, First, Last, etc)
Handle Collections (Check if
null, Assign Elements, Compare Collections, Operations with
Collections)
Collections and DB Tables
Moving
from Cursor-Loops to Collections
Bulk Binding
Handling
and Reporting Exceptions
Multi-Dimensional
Arrays
Returning Result
Sets
Returning s
Single Row
Returning
Cursor variables using REF CURSOR
Returning
Collections
Returning
Collections with Dynamic SQL
Return using Table
Functions (pipelined)
Cursor
Attributes
Improvements
to Bulk Bind and Collections in 10g
Improvements
in Oracle 11g
Improvements
in Oracle 12c
Introduction to Object
Types and Records
Very quick Summary
Index-by tables or Associative
array
Syntax
TYPE type_name IS TABLE OF element_type [NOT NULL] INDEX BY
subscript_type;
table_name type_name;
Example
TYPE l_tab_type IS TABLE OF NUMBER INDEX BY VARCHAR2(10);
l_tab l_tab_type;
Variable-size array or Varray
TYPE Foursome IS VARRAY(4) OF VARCHAR2(15);
Nested table
Syntax
TYPE type_name IS TABLE OF element_type [NOT NULL];
table_name type_name;
Example
TYPE l_tab_type IS TABLE OF NUMBER;
l_tab l_tab_type;
TYPE l_rec_type is RECORD (
ID NUMBER,
NAME VARCHAR2(200)
);
TYPE l_tab_type IS TABLE OF l_rec_type;
l_tab l_tab_type;
A database object type is very
similar to a CREATE TABLE statement, but it does not create a
"container" for data. Rather it is a "template" for data.
Example:
CREATE OR
REPLACE TYPE "TYPE_VARCHAR2_ARRAY" as TABLE OF VARCHAR2(300);
/
CREATE OR REPLACE TYPE "TYPE_NUMBER_ARRAY" as TABLE OF NUMBER;
/
CREATE TYPE type_food AS OBJECT (
name
VARCHAR2(100),
food_group VARCHAR2
(100),
grown_in
VARCHAR2 (100)
);
/
DECLARE
-- Create a new object with
a constructor
my_favorite_vegetable_rec
type_food
:=
type_food('Brussel Sprouts', 'VEGETABLE', 'Farm,Greenhouse,Backyard');
BEGIN
--Read an attribute value
DBMS_OUTPUT.put_line
(my_favorite_vegetable_rec.name);
--Modify an attribute value
my_favorite_vegetable_rec.food_group := 'SATISFACTION';
END;
/
A PL/SQL RECORD is a composite datatype, is composed of multiple pieces
of information called fields. Records can be declared using
relational tables or explicit cursors as "templates" with the
%ROWTYPE declaration attribute. You can also declare records
based on TYPES that you define
yourself. The easiest way to define a record is by using the
%ROWTYPE syntax in your declaration. For example, the
statement: bestseller books%ROWTYPE; creates
a record that has a structure corresponding to the books table;
for every column in the table, there is a field in the record
with the same name and datatype as
the column. The %ROWTYPE keyword is especially valuable because
the declaration is guaranteed to match the corresponding
schema-level template and is immune to schema-level changes in
definition of the shape of the table. If we change the structure
of the books table, all we have to do is recompile the above
code and bestseller will take on the new structure of that
table.
A second way to declare a record is to define your own RECORD
TYPE:
DECLARE
TYPE EmpTabTyp IS TABLE OF
emp%ROWTYPE INDEX BY BINARY_INTEGER;
emp_tab EmpTabTyp;
TYPE extra_book_info_t
IS RECORD (
title
books.title%TYPE,
is_bestseller
BOOLEAN,
reviewed_by
names_list );
first_book
extra_book_info_t;
Notice that
the user-defined record datatype
above includes a field (“title”) that is based on the column
definition of a database table, a field (“is_bestseller”)
based on a scalar data type (PL/SQL Boolean flag), and a
collection (list of names of people who reviewed the book).
Next, we can declare a record based on this type (you do
not use %ROWTYPE in this case, because you are already
referencing a type to perform the declaration). Once you have
declared a record, you can then manipulate the data in these
fields (or the record as a whole) as you can see below:
DECLARE
bestseller
books%ROWTYPE; --Based on a DB Table
required_reading books%ROWTYPE;
BEGIN
-- Modify a field value
bestseller.title := 'ORACLE PL/SQL
PROGRAMMING';
-- Copy one record to another
required_reading :=
bestseller;
END;
Note that in the above code we have
used the structure of the books table to define our PL/SQL
records, but the assignment to the title field did not in any
way affect data inside that table.
You can also pass records as arguments to
procedures and functions. This technique allows you to
shrink down the size of a parameter list (pass a single record
instead of a lengthy and cumbersome list of individual values).
Here is an example of a function with a record in the parameter
list:
CREATE
OR
REPLACE PROCEDURE calculate_royalties
( book_in IN books%ROWTYPE,
quarter_end_in IN DATE )
IS ...
Another Full Example:
DECLARE
-- Declare a basic Table Type Array
TYPE a_char_data IS TABLE OF VARCHAR2(10) INDEX BY BINARY_INTEGER;
-- Declare a complex record type with an embedded index by table. So now we have a ragged record type. A single record with a dimensional name column.
TYPE r_data IS RECORD (
ssn VARCHAR2(9) NOT NULL := -1,
name a_char_data, -- Notice the table_type used here
dob DATE );
-- Declare an "Associative Array (or index-by table)" using the complex record type. This creates an array of ragged records.
TYPE a_multi IS TABLE OF r_data INDEX BY BINARY_INTEGER;
-- Declare a variable using the complex array
v_data a_multi;
BEGIN
-- Populate the ssn and dob columns of the first record of the v_data variable.
v_data(1).ssn := '123456789';
v_data(1).dob := '01-JAN-1900';
-- Populate the first and second rows of the name table in the first row of the v_data variable.
v_data(1).name(1) := 'Lewis';
v_data(1).name(2) := 'Joe';
--Finally, display the ssn of the first row of the v_data variable and then looped through the name table of the first row of the v_data variable.
dbms_output.put_line(v_data(1).ssn);
-- Loop through the v_data(1).name table
FOR i IN v_data(1).name.FIRST..v_data(1).name.LAST LOOP
dbms_output.put_line(v_data(1).name(i));
END LOOP;
END;
/
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:
- Associative Arrays (Index-by
Tables), 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.
Nested tables and Varrays must have been initialized before you can
use them.
The following scenarios generally indicate a need for collections:
- Repeated access to the same, static database information. If,
during execution of your program (or during a session, since
your collection can be declared as package data and thereby
persist with all its rows for the entire session), you need to
read the same data more than once, load it into a collection.
Multiple scannings of the collection will be much more efficient
than multiple executions of a SQL query.
- Management of program-only lists. You may build and manipulate
lists of data that exist only within your program, never
touching a database table. In this case, collections-and,
specifically, associative arrays-will be the way to go.
IMPORTANT
NOTES:
Memory for collections comes out of the PGA or Process Global Area,
One per session, so a program using collections can consume a large
amount of memory.
Use the NOCOPY hint to reduce overhead of passing collections in and
out of program units.
Encapsulate or hide details of collection management.
Don't always fill collections sequentially. Think about how you need
to manipulate the contents.
Try to read a row that doesn't exist, and Oracle raises
NO_DATA_FOUND.
1-Associative Arrays (ALSO
INDEX-BY_TABLES)
Associative arrays are sets of key-value pairs, where each
key is unique and is used to locate a corresponding value in the
array. The key can be an integer or a string.
Associative arrays help you represent data sets of arbitrary size,
with fast lookup for an individual element without knowing its
position within the array and without having to loop through all the
array elements.
It is like a simple version of a SQL table where you can retrieve
values based on the primary key.
Because associative arrays are intended for temporary data rather
than storing persistent data, you cannot use them with SQL
statements such as INSERT and SELECT INTO. You can make them
persistent for the life of a database session by declaring the type
in a package and assigning the values in a package body. You don't
need to initialize the Associative Array.
Declaration:
TYPE
type_name IS TABLE OF
element_type [NOT NULL] INDEX BY [BINARY_INTEGER |
PLS_INTEGER | VARCHAR2(size_limit)];
Probably the most familiar collection type is the associative
arrays. The code block below is a typical use
of an associative array:
DECLARE
TYPE num_array IS TABLE OF NUMBER INDEX BY BINARY_INTEGER;
my_num_array num_array;
TYPE emp_array IS TABLE OF emp%ROWTYPE INDEX BY PLS_INTEGER;
my_emp_array emp_array;
my_emp_array2 emp_array;
TYPE char_array
IS TABLE OF VARCHAR2(50)
INDEX BY BINARY_INTEGER;
my_chars_array char_array;
BEGIN
FOR i IN 1..100 LOOP
my_num_array(i) := power(2, i);
END LOOP;
my_chars_array(1) := 'Diego';
my_chars_array(2) := 'Diego2';
--In the example below,
you store a single record in the index-by table, and its subscript
is 7468 rather than 1
SELECT * INTO my_emp_array2(7468)
FROM emp WHERE empno = 7468;
FOR rec IN (select * from emp)
LOOP
my_emp_array(rec.empno) := rec;
END LOOP;
--I can also use BULK
to Grab all the data without a Cursor
SELECT * BULK COLLECT
INTO my_emp_array
FROM
emp;
DBMS_OUTPUT.PUT_LINE ('Fetched ' || TO_CHAR ( my_emp_array.COUNT
) ||' records from
EMP TABLE.' );
END;
/
This first loop creates an array of unlimited size (up to your OS
and DB version limitations) of NUMBER which is indexed by a
BINARY_INTEGER datatype. The index is just the subscript and
BINARY_INTEGER is just a numeric data type. An associative arrays
does NOT have to be
initialized and it can be sparse (non-consecutive numbers).
Let’s now look at a specific scenario in which a
VARCHAR2 indexed array would be ideal. The requirement to look up
a value via a unique non-numeric key is a generic computational
problem. Suppose we have a set of English-French vocabulary pairs
stored persistently in the most obvious way in a schema level
table:
SELECT * FROM translations;
ENGLISH
FRENCH
------------- ----------
computer
ordinateur
tree
arbre
book
livre
cabbage
chou
country
pays
Our task is to allow lookup from French to English. What’s the
most efficient way to implement the lookup procedure? We certainly
have a wide set of choices, including:
• Pure SQL approach:
Simply query the English word for the French each time it’s
needed. This will be performed with a simple select using on the
where clause the english word.
• Full collection scan, a.k.a. “linear search”: Use the
“traditional” INDEX BY BINARY_INTEGER collection to cache all the
French-English pairs. Search the entire collection for a match
each time a lookup is needed.
• Hash-based indexing:
Build our own VARCHAR2- based index using Oracle’s hashing
algorithm.
• VARCHAR2-indexed associative array: Cache all French-English
pairs using the French word as the key, allowing direct lookup of
the English word, all within PL/SQL.
But by far the most optimized way would be to use Associative
Array with the INDEX BY VARCHAR2 option.
Another
Example
Showing a Sparse Collection
DECLARE
TYPE
list_of_names_t IS TABLE OF VARCHAR2 (32767) INDEX BY
PLS_INTEGER;
happyfamily list_of_names_t;
l_row
PLS_INTEGER;
BEGIN
happyfamily (2
** 31 - 1) := 'Eli';
happyfamily
(-15070) := 'Steven';
happyfamily
(-90900) := 'Chris';
happyfamily
(88) := 'Veva';
--
l_row :=
happyfamily.FIRST;
WHILE (l_row IS
NOT NULL)
LOOP
DBMS_OUTPUT.put_line
( 'Value at index ' || l_row || ' = '|| happyfamily
(l_row));
l_row
:= happyfamily.NEXT (l_row);
END LOOP;
/*
FOR l_row IN
happyfamily.FIRST .. happyfamily.LAST
LOOP
DBMS_OUTPUT.put_line
(happyfamily (l_row));
END LOOP;
*/
END;
/
Another
Example
showing string-indexed collections
DECLARE
TYPE
phone_no_tab
IS TABLE OF VARCHAR2(20) INDEX BY VARCHAR2(30);
phone_nos phone_no_tab;
BEGIN
phone_nos('office')
:=
'+44 (0) 117 942 2508';
DBMS_output.put_line('phone_no(office)
is '||phone_nos('office'));
END;
/
phone_no(office)
is
+44
(0) 117 942 2508
DECLARE
SUBTYPE
location_name_t IS VARCHAR2 (2000);
TYPE
population_type IS TABLE OF PLS_INTEGER INDEX BY
location_name_t;
country_population population_type;
continent_population population_type;
--
howmany
PLS_INTEGER;
l_limit
location_name_t;
BEGIN
country_population ('Greenland') := 100000;
country_population ('Iceland') := 750000;
continent_population ('Australia') := 30000000;
continent_population ('Antarctica') := 1000;
continent_population ('antarctica') := 1001;
--
howmany :=
country_population.COUNT;
DBMS_OUTPUT.put_line ('COUNT in country_population = ' || howmany);
l_limit :=
continent_population.FIRST;
DBMS_OUTPUT.put_line ('FIRST row in continent_population = ' || l_limit);
DBMS_OUTPUT.put_line ('FIRST value continent_population = ' ||
continent_population (l_limit));
l_limit :=
continent_population.LAST;
DBMS_OUTPUT.put_line ('LAST row in continent_population = ' || l_limit);
DBMS_OUTPUT.put_line ('LAST value in continent_population = ' ||
continent_population (l_limit));
/* THIS WILL NOT WORK
FOR
indx IN continent_population.FIRST .. continent_population.LAST LOOP
NULL;
END
LOOP;
*/
END;
/
2-Nested
Tables
A nested table is similar to an associative array in that there is
no maximum size to the array; however prior to assign a new element
to a nested table a PL/SQL program needs to explicitly extend the
size before adding new elements. A
nested table is an object type and therefore needs to first be
initialized with a constructor before being used.
Nested tables hold an arbitrary number of elements. They use
sequential numbers as subscripts.
The size of a nested table can increase dynamically, i.e., nested
tables are unbounded. Elements in a nested table initially have
consecutive subscripts, but as elements are deleted, they can have
non-consecutive subscripts. The range of values for nested table
subscripts is 1..2147483647. To extend a nested table, the built-in
procedure EXTEND must be used. To delete elements, the built-in
procedure DELETE must be used.
An uninitialized nested table is atomically null, so the IS NULL
comparison operator can be used to see if a nested table is null.
Declaration:
TYPE
type_name IS TABLE OF
element_type [NOT
NULL];
In
PL/SQL
Declare
TYPE TYP_NT_NUM IS TABLE OF NUMBER ;
In SQL
CREATE [OR REPLACE] TYPE TYP_NT_NUM IS TABLE OF NUMBER ;
With nested tables declared within PL/SQL, element_type can be any
PL/SQL datatype except : REF CURSOR
DECLARE
TYPE nest_tab_t IS TABLE OF NUMBER;
my_nt nest_tab_t := nest_tab_t(); --We need to initialize this type with a constructor
TYPE emp_ntt IS TABLE OF emp%ROWTYPE;
my_emp emp_ntt := emp_ntt();
--We need to initialize this type with a constructor
TYPE TYP_NT_NUM IS TABLE OF NUMBER ;
Nt_tab TYP_NT_NUM ;
TYPE CourseList IS TABLE OF VARCHAR2(16);
my_courses CourseList;
TYPE CourseList2 IS TABLE OF VARCHAR2(16);
my_courses CourseList := CourseList('Art 1111', 'Hist
3100', 'Engl 2005'); --Here we initialize the Nested Table
Defining its elements
BEGIN
Nt_tab := TYP_NT_NUM( 5, 10, 15, 20 ) ; --Here we initialize the Nested
Table Defining its elements inside the BEGIN
my_courses :=
CourseList('Econ 2010', 'Acct 3401', 'Mgmt 3100'); --Here we initialize the Nested
Table Defining its elements inside the BEGIN
FOR i IN 1..100 LOOP
my_nt.EXTEND;
my_nt(i) := i;
END LOOP;
FOR rec IN (select * from emp)
LOOP
my_emp.EXTEND;
my_emp(my.emp.LAST) := rec;
END LOOP;
END;
Note that the variable was initialized to an empty nested table
using the constructor for its type. Also,
the example shows how the nested table EXTEND method is used to
allocate a new element to the array so that it can be assigned to in
the next statement.
Another Example using a DB
Table containing a Nested DB Object
The following example illustrates how a simple nested table is
created.
a) First, define a DB Object type as follows:
CREATE TYPE ELEMENTS AS OBJECT
(
ELEM_ID NUMBER(6),
PRICE NUMBER(7,2));
/
b) Next, create a table type ELEMENTS_TAB which stores ELEMENTS
objects:
CREATE TYPE ELEMENTS_TAB AS
TABLE OF ELEMENTS;
/
c) Finally, create a database table STORAGE having type ELEMENTS_TAB
as one of its columns:
CREATE TABLE STORAGE (
SALESMAN NUMBER(4),
ELEM_ID NUMBER(6),
ORDERED DATE,
ITEMS ELEMENTS_TAB)
NESTED TABLE ITEMS STORE AS ITEMS_TAB;
This example demonstrates how to populate the STORAGE table with a
single row:
INSERT INTO STORAGE VALUES
(100, 123456, SYSDATE,
ELEMENTS_TAB(ELEMENTS(175692,120.12),
ELEMENTS(167295,130.45),
ELEMENTS(127569,99.99)));
The following example demonstrates how to use the operator THE which
is used in a SELECT statement to identify a nested table:
INSERT INTO
THE
(SELECT ITEMS FROM
STORAGE WHERE ELEM_ID = 123456)
VALUES (125762,
101.99);
The following example shows how to update the STORAGE table row
where salesman column has value 100:
UPDATE STORAGE SET ITEMS =
ELEMENTS_TAB(ELEMENTS(192512, 199.99)) WHERE SALESMAN = 100;
The following example shows how to retrieve data from a table to a
nested Table:
DECLARE
my_elements
ELEMENTS;
BEGIN
SELECT ITEMS INTO
my_elements FROM STORAGE
WHERE SALESMAN = 100;
END;
/
Within PL/SQL, you can manipulate the nested table by looping
through its elements, using methods such as TRIM or EXTEND, and
updating some or all of the elements. Afterwards, you can store the
updated table in the database again.
New functions
(Multi-set operations) on 10g for Nested Tables
3-Varrays
Varrays are ordered groups of items
of type VARRAY.
With Varrays the number of elements in the array is variable
up to the declared size. Arguably then, variable-sized arrays
aren't that variable in size. 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.
The maximum size of a varray needs to be specified in its type
definition. The range of values for the index of a varray is
from 1 to the maximum specified in its type definition. If no
elements are in the array, then the array is atomically null,
so the IS NULL comparison operator can be used to see if a
varray is null. Varrays cannot be compared for equality or
inequality.
The main use of a varray is to group small or uniform-sized
collections of objects.
A varray can be assigned to another varray, provided the
datatypes are the exact same type. For example, suppose you
declared two PL/SQL types:
TYPE My_Varray1 IS VARRAY(10) OF My_Type;
TYPE My_Varray2 IS VARRAY(10) OF My_Type;
An object of type My_Varray1 can be assigned to another object
of type My_Varray1 because they are the exact same type.
However, an object of type My_Varray2 cannot be assigned to an
object of type My_Varray1 because they are not the exact same
type, even though they have the same element type.
VARRAYs find their optimum application when data
set, which has to be stored in order and is relatively small.
Declaration:
TYPE
type_name IS VARRAY
(size_limit) OF element_type [NOT
NULL];
size_limit is a
positive integer literal representing the maximum number of
elements in the array.
Like nested
tables, varrays can be both
PL/SQL types and SQL types and therefore can take advantage of
the many of the features listed above. The
main differences with varrays in PL/SQL is that their maximum
size must be specified when the type is declared. PL/SQL nested table or varray variables
can be used to atomically insert values into tables that use
them. Apart from this capability, varrays are of less interest than
nested tables to the PL/SQL developer because they have the
restriction of an upper bound and most anything one can do in
code with a varray, one can do
with a nested table.
Examples:
PL/SQL
declare
type v is varray(50) of number;
type Calendar is varray(366) OF DATE;
SQL
CREATE [OR REPLACE] TYPE TYP_V_CHAR IS VARRAY(10) OF VARCHAR2(20)
Initialization
Declare
TYPE TYP_V_DAY IS VARRAY(7) OF VARCHAR2(15) ;
v_tab TYP_V_DAY ;
TYPE Clientele IS VARRAY(100) OF Customer;
vips Clientele := Clientele(); -- initialize empty varray
Begin
v_tab := TYP_NT_NUM( ‘Sun’,’Mon’,’Tue’,’Wed’,’Thu’,’Fri’,’Sat’ ) ; --We can also initialize inside the code
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).
Another Example using a DB Table containing a Nested DB Object
The following example illustrates how a simple nested table is created.
a) First, define a object type ELEMENTS as follows:
CREATE or REPLACE TYPE MEDICINES AS OBJECT (
MED_ID NUMBER(6),
MED_NAME VARCHAR2(14),
MANF_DATE DATE);
/
b) Next, define a VARRAY type MEDICINE_ARR which stores MEDICINES objects:
CREATE TYPE MEDICINE_ARR AS VARRAY(40) OF MEDICINES;
/
Finally, create a relational table MED_STORE which has MEDICINE_ARR as a column type:
CREATE TABLE MED_STORE (
LOCATION VARCHAR2(15),
STORE_SIZE NUMBER(7),
EMPLOYEES NUMBER(6),
MED_ITEMS MEDICINE_ARR);
Each item in column MED_ITEMS is a varray that will several medicines for a given location.
The following example shows how to insert two rows into the MED_STORE table:
INSERT INTO MED_STORE VALUES ('BELMONT',1000,10, MEDICINE_ARR(MEDICINES(11111,'STOPACHE',SYSDATE)));
INSERT INTO MED_STORE VALUES ('REDWOOD CITY',700,5, MEDICINE_ARR(MEDICINES(12345,'STRESS_BUST',SYSDATE)));
The following example shows how to delete the second row we have inserted in example 6 above:
DELETE FROM MED_STORE WHERE LOCATION = 'REDWOOD CITY';
The following example shows how to update the MED_STORE table and add more medicines to the Belmont store:
UPDATE MED_STORE SET MED_ITEMS = MEDICINE_ARR (
MEDICINES(12346,'BUGKILL',SYSDATE),
MEDICINES(12347,'INHALER',SYSDATE),
MEDICINES(12348,'PAINKILL',SYSDATE));
Another Example:
Below SQL uses TABLE function to display the ORDERS table data in relational format.
To manipulate the individual elements of a collection with SQL, use the TABLE operator. The TABLE operator uses a subquery to extract the varray or nested table,
so that the INSERT, UPDATE, or DELETE statement applies to the nested table rather than the top-level table.
SELECT T1.LOCATION, T1.STORE_SIZE, T1.EMPLOYEES, T2.*
FROM MED_STORE T1, TABLE(T1.MANF_DATE) T2;
-- In the following example, you retrieve the title and cost of the Maintenance Department's fourth project from the varray column projects:
DECLARE
my_cost NUMBER(7,2);
my_title VARCHAR2(35);
BEGIN
SELECT cost, title INTO my_cost, my_title
FROM TABLE(SELECT projects FROM department
WHERE dept_id = 50)
WHERE project_no = 4;
...
END;
/
Example: Performing INSERT, UPDATE, and DELETE Operations on a Varray with SQL
Currently, you cannot reference the individual elements of a varray in an INSERT, UPDATE, or DELETE statement.
You must retrieve the entire varray, use PL/SQL procedural statements to add, delete, or update its elements, and then store the changed varray back in the database table.
In the following example, stored procedure ADD_PROJECT inserts a new project into a department's project list at a given position:
CREATE PROCEDURE add_project (
dept_no IN NUMBER,
new_project IN Project,
position IN NUMBER) AS
my_projects ProjectList;
BEGIN
SELECT projects INTO my_projects FROM department
WHERE dept_no = dept_id FOR UPDATE OF projects;
my_projects.EXTEND; -- make room for new project
/* Move varray elements forward. */
FOR i IN REVERSE position..my_projects.LAST - 1 LOOP
my_projects(i + 1) := my_projects(i);
END LOOP;
my_projects(position) := new_project; -- add new project
UPDATE department SET projects = my_projects
WHERE dept_no = dept_id;
END add_project;
/
The following stored procedure updates a given project:
CREATE PROCEDURE update_project (
dept_no IN NUMBER,
proj_no IN NUMBER,
new_title IN VARCHAR2 DEFAULT NULL,
new_cost IN NUMBER DEFAULT NULL) AS
my_projects ProjectList;
BEGIN
SELECT projects INTO my_projects FROM department
WHERE dept_no = dept_id FOR UPDATE OF projects;
/* Find project, update it, then exit loop immediately. */
FOR i IN my_projects.FIRST..my_projects.LAST LOOP
IF my_projects(i).project_no = proj_no THEN
IF new_title IS NOT NULL THEN
my_projects(i).title := new_title;
END IF;
IF new_cost IS NOT NULL THEN
my_projects(i).cost := new_cost;
END IF;
EXIT;
END IF;
END LOOP;
UPDATE department SET projects = my_projects
WHERE dept_no = dept_id;
END update_project;
/
Examples
for
nested tables and varrays
set
serveroutput
on
declare
type nestab is table
of number;
someNumbers_nt nestab;
type varr is
varray(50) of varchar2(30);
someNames_varr
varr;
i
binary_integer;
begin
--Load some elements
someNumbers_nt := nestab(10, 4, 6, 9, 2, 5);
someNames_varr := varr('Fred','Joe','Caesar');
i:=3;
--Ask if Item on
position 3 is a 6
if someNumbers_nt(i) = 6 then
dbms_output.put_line ('someNumbers_nt(' || i || ') = 6');
else
dbms_output.put_line ('someNumbers_nt(' || i || ') <> 6');
end if;
someNumbers_nt.delete(1); --delete element 1
someNumbers_nt.delete(4); --delete element 4
--More Ways to
delete -- If an element doesn't exist no
exception rais
-- someNumbers_nt.delete(20,30); --delete elements 20 through
30
-- someNumbers_nt.delete;
--delete entire PL/SQL Table
--Step on 1st
Position
i := someNumbers_nt.first();
while i is not null
loop
dbms_output.put_line ('Position = ' || i || ': ' || someNumbers_nt(i));
i := someNumbers_nt.next(i);
end loop;
end;
/
Output:
someNumbers_nt(3) = 6
someNumbers_nt(3) <> 6
2: 4
3: 7
5: 2
6: 5
Differences between Nested Tables
and Varrays
- Nested tables are unbounded, whereas varrays have a maximum
size.
- Individual elements can be deleted from a nested table, but
not from a varray (on varrays you can delete just elements from
the end). Therefore, nested tables can be sparse, whereas
varrays are always dense.
- Varrays are stored by Oracle in-line (in the same tablespace),
whereas nested table data is stored out-of-line in a store
table, which is a system-generated database table associated
with the nested table.
- When stored in the database, nested tables do not retain their
ordering and subscripts, whereas varrays do.
- Nested tables support indexes while varrays do not.
Example:
Declaring
Nested
Tables,
Varrays,
and Associative Arrays
DECLARE
TYPE
nested_type IS TABLE OF VARCHAR2(20);
TYPE
varray_type IS VARRAY(5) OF INTEGER;
TYPE
assoc_array_num_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE
assoc_array_str_type IS TABLE OF VARCHAR2(32) INDEX BY
PLS_INTEGER;
TYPE
assoc_array_str_type2 IS TABLE OF VARCHAR2(32) INDEX BY
VARCHAR2(64);
v1 nested_type;
v2 varray_type;
v3
assoc_array_num_type;
v4
assoc_array_str_type;
v5
assoc_array_str_type2;
BEGIN
v1 :=
nested_type('Arbitrary','number','of','strings');
v2 :=
varray_type(10, 20, 40, 80, 160); -- Up to 5 integers
v3(99) := 10;
-- Just start assigning to elements.
v3(7) := 100;
-- Subscripts can be any integer values.
v4(42) :=
'Cat'; -- Just start assigning to elements.
v4(54) :=
'Hat'; -- Subscripts can be any integer values.
v5('Canada') :=
'North America'; -- Just start assigning to elements.
v5('Greece') :=
'Europe'; --
Subscripts can be string values.
END;
/
Table
Functions
To do this, the PL/SQL code executes a SQL statement passing the
local nested table variable to the server. There
are two special functions necessary to achieve this functionality. The TABLE function tells the server to
bind over the values of the nested table,
perform the requested SQL operation and return the results back as
if the variable was a SQL table in the database.
The CAST function is an explicit directive to the server to
map the variable to the SQL type that was defined globally in the
previous step. With this capability, many new
operations become possible.. For
example, one can take a nested table of objects that have been
created in code and send them to the server for ordering or
aggregation. Almost any SQL operation
is possible. For example a nested table can be joined with other SQL
tables in the database. The next
example shows a simple ordering of an array by the second field.
DECLARE
eml_dmo_nt email_demo_nt_t := email_demo_nt_t();
BEGIN
-- Some logic that
populates the nested table …
eml_dmo_nt.EXTEND(3);
eml_dmo_nt(1) := email_demo_obj_t(45, 3, '23');
eml_dmo_nt(2) := email_demo_obj_t(22, 3, '41');
eml_dmo_nt(3) := email_demo_obj_t(18, 7, 'over_100k');
-- Process the data in
assending order of email id.
FOR r IN (SELECT * FROM
TABLE(CAST(eml_dmo_nt AS email_demo_nt_t))
ORDER BY 1)
LOOP
dbms_output.put_line(r.email_id || ' ' || r.demo_id);
END LOOP;
END;
When to use what
If you're new to PL/SQL collections, you may have a fair
understanding of their mechanics by this point, but are
uncertain when to use a particular type.The following table
summarizes each collection's capabilities.
Has Ability To
|
Varray
|
Nested
|
Associative Array
|
be indexed by non-integer
|
No
|
No
|
Yes
|
preserve element order
|
Yes
|
No
|
No
|
be stored in database
|
Yes
|
Yes
|
No
|
have elements selected
indidually in database
|
Yes
|
Yes
|
--
|
have elements updated
indidually in database
|
Yes
|
Yes
|
--
|
The following guidelines will help you
choose an associative array, nested table, or VARRAY:
- If you need a sparsely indexed list
(for "data-smart" storage, for example), your only practical
option is an associative array. True, you could allocate and
then delete elements of a nested table variable, but it is
inefficient to do so for anything but the smallest
collections.
- If your PL/SQL application requires
negative subscripts, you 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 on 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 Database 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 data set
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.
The information in Table 1 will also
help you make your choice.
Table 1: Associative
array, nested table, or VARRAY
| Characteristic |
Associative Array |
Nested Table |
VARRAY |
| Dimensionality |
Single |
Single |
Single |
| Use inside the FROM clause of query
with TABLE operator |
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 EXTEND
element at any time |
Yes |
No; may need to use EXTEND first |
No; may need to use past upper
bound first and cannot use EXTEND |
| Means of extending |
Assign value to element with a new
subscript |
Use built-in EXTEND procedure (or
TRIM to condense), with no predefined maximum |
Use 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 |
In addition, the following bullet points can be
referred to when deciding what collection best suits a
particular solution.
Varray
- Use to preserve ordered list
- Use when working with a fixed set, with a
known number of entries
- Use when you need to store in the database and
operate on the Collection as a whole
- Access the collection inside SQL (table
functions, columns in tables).
Nested Table
- Use when working with an unbounded list that
needs to increase dynamically
- Use when you need to store in the database and
operate on elements individually
- Access the collection inside SQL (table
functions, columns in tables)
- Want to perform set operations
Associative
Array
- Use when there is no need to store the
Collection in the database. Its speed and indexing
flexibility make it ideal for internal application use.
- Work within PL/SQL code only
- Sparsely fill and manipulate the collection
- Take advantage of negative index values
Choosing
Between
Nested Tables and Associative Arrays
Both nested tables and associative arrays (formerly known as
index-by tables) use similar subscript notation, but they have
different characteristics when it comes to persistence and ease
of parameter passing.
Nested tables can be stored in a database column, but
associative arrays cannot. Nested tables are appropriate for
important data relationships that must be stored persistently.
Associative arrays are appropriate for relatively small lookup
tables where the collection can be constructed in memory each
time a procedure is called or a package is initialized. They are
good for collecting information whose volume is unknown
beforehand, because there is no fixed limit on their size. Their
index values are more flexible, because associative array
subscripts can be negative, can be nonsequential, and can use
string values instead of numbers when appropriate.
PL/SQL automatically converts between host arrays and
associative arrays that use numeric key values. The most
efficient way to pass collections to and from the database
server is to use anonymous PL/SQL blocks to bulk-bind input and
output host arrays to associative arrays.
Choosing
Between
Nested Tables and Varrays
Varrays are a good choice when the number of elements is known
in advance, and when the elements are usually all accessed in
sequence. When stored in the database, varrays retain their
ordering and subscripts.
Each varray is stored as a single object, either inside the
table of which it is a column (if the varray is less than 4KB)
or outside the table but still in the same tablespace (if the
varray is greater than 4KB). You must update or retrieve all
elements of the varray at the same time, which is most
appropriate when performing some operation on all the elements
at once. But you might find it impractical to store and retrieve
large numbers of elements this way.
Nested tables can be sparse: you can delete arbitrary elements,
rather than just removing an item from the end. Nested table
data is stored out-of-line in astore table, a system-generated
database table associated with the nested table. This makes
nested tables suitable for queries and updates that only affect
some elements of the collection. You cannot rely on the order
and subscripts of a nested table remaining stable as the table
is stored and retrieved, because the order and subscripts are
not preserved when a nested table is stored in the database.
Using Collection
Methods
The following collection methods help generalize code, make
collections easier to use, and make your applications easier to
maintain:
- 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. The
syntax follows: collection_name.method_name[(parameters)]
Collection methods cannot be called from SQL statements.
EXTEND and TRIM cannot be used with associative arrays.
Only EXISTS can be used on a null collection. If you apply another
method to such collections, PL/SQL raises COLLECTION_IS_NULL.
Some Examples:
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.
If my_collection.EXISTS(10)
Then
My_collection.DELETE(10) ;
End if ;
COUNT
Returns the number of elements in a collection.
Declare
TYPE TYP_TAB IS TABLE OF NUMBER;
my_tab
TYP_TAB := TYP_TAB( 1, 2, 3, 4, 5 );
Begin
Dbms_output.Put_line( 'COUNT = ' || To_Char( my_tab.COUNT ) ) ;
my_tab.DELETE(2)
;
Dbms_output.Put_line( 'COUNT = ' || To_Char( my_tab.COUNT ) ) ;
End ;
/
COUNT = 5
COUNT = 4
LIMIT
For nested tables and associative arrays, which have no maximum
size, LIMIT returns NULL. For varrays, LIMIT returns the maximum
number of elements that a varray can contain (which you must specify
in its type definition, and can change later with the TRIM and
EXTEND methods).
Declare
TYPE TYP_ARRAY IS
ARRAY(30) OF NUMBER ;
my_array
TYP_ARRAY := TYP_ARRAY( 1, 2, 3 ) ;
Begin
dbms_output.put_line(
'Max array size is ' || my_array.LIMIT ) ;
End;
/
Max array size is 30
FIRST
and LAST
FIRST and LAST return the first and last (smallest and largest)
index numbers in a collection. For an associative array with
VARCHAR2 key values, the lowest and highest key values are returned;
ordering is based on the binary values of the characters in the
string, unless the NLS_COMP initialization parameter is set to ANSI,
in which case the ordering is based on the locale-specific sort
order specified by the NLS_SORT initialization parameter.
If the collection is empty, FIRST and LAST return NULL.
For varrays, FIRST always returns 1 and LAST always equals COUNT.
For nested tables, FIRST normally returns 1. But, if you delete
elements from the beginning of a nested table, FIRST returns a
number larger than 1. Also for nested tables, LAST normally equals
COUNT. But, if you delete elements from the middle of a nested
table, LAST becomes larger than COUNT.
When scanning elements, FIRST and LAST ignore deleted elements.
Declare
TYPE TYP_TAB IS TABLE OF PLS_INTEGER INDEX BY
VARCHAR2(1);
my_tab
TYP_TAB;
TYPE TYP_TAB IS TABLE OF NUMBER;
my_tab TYP_TAB := TYP_TAB( 1, 2, 3, 4, 5 );
Begin
For i in 65 .. 69
Loop
my_tab( Chr(i) ) := i ;
End loop ;
Dbms_Output.Put_Line( 'First= ' || my_tab.FIRST || ' Last= '
|| my_tab.LAST ) ;
For i IN my_tab.FIRST .. my_tab.LAST Loop
Dbms_output.Put_line( 'my_tab(' || Ltrim(To_Char(i)) || ') = ' ||
To_Char( my_tab(i) ) ) ;
End loop ;
End ;
/
First= A Last= E
my_tab(1) = 1
my_tab(2) = 2
my_tab(3) = 3
my_tab(4) = 4
my_tab(5) = 5
PRIOR(index)
and NEXT(index)
Returns the previous or next index number that precedes index n. If
n has no predecessor,PRIOR(n) returns NULL. If n has no successor,
NEXT(n) returns NULL.
For associative arrays with VARCHAR2 keys, these methods return the
appropriate key value; ordering is based on the binary values of the
characters in the string, unless the NLS_COMP initialization
parameter is set to ANSI, in which case the ordering is based on the
locale-specific sort order specified by the NLS_SORT initialization
parameter.
These methods are more reliable than looping through a fixed set of
subscript values, because elements might be inserted or deleted from
the collection during the loop. This is especially true for
associative arrays, where the subscripts might not be in consecutive
order and so the sequence of subscripts might be (1,2,4,8,16) or
('A','E','I','O','U').
PRIOR and NEXT do not wrap from one end of a collection to the
other. For example, the following statement assigns NULL to n
because the first element in a collection has no predecessor:
n := courses.PRIOR(courses.FIRST); -- assigns NULL to n
PRIOR is the inverse of NEXT. For instance, if element i exists, the
following statement assigns element i to itself:
projects(i) := projects.PRIOR(projects.NEXT(i));
You can use PRIOR or NEXT to traverse collections indexed by any
series of subscripts. In the following example, you use NEXT to
traverse a nested table from which some elements have been deleted:
i := courses.FIRST; --
get subscript of first element
WHILE i IS NOT NULL LOOP
-- do something
with courses(i)
i :=
courses.NEXT(i); -- get subscript of next element
END LOOP;
When traversing elements, PRIOR and NEXT ignore deleted elements.
Declare
TYPE TYP_TAB IS TABLE OF PLS_INTEGER INDEX BY
VARCHAR2(1) ;
my_tab
TYP_TAB ;
c Varchar2(1) ;
Begin
For i in 65 .. 69
Loop
my_tab( Chr(i) ) := i ;
End loop ;
c := my_tab.FIRST
; -- first element
Loop
Dbms_Output.Put_Line( 'my_tab(' || c || ') = ' || my_tab(c) ) ;
c := my_tab.NEXT(c) ; -- get the successor element
Exit When c IS NULL ; -- end of collection
End loop ;
End ;
/
my_tab(A) = 65
my_tab(B) = 66
my_tab(C) = 67
my_tab(D) = 68
my_tab(E) = 69
Use the PRIOR() or NEXT() method to be sure that you do not access
an invalid element:
Declare
TYPE TYP_TAB IS TABLE OF PLS_INTEGER ;
my_tab
TYP_TAB := TYP_TAB( 1, 2, 3, 4, 5 );
Begin
my_tab.DELETE(2)
; -- delete an element of the collection
For i in
my_tab.FIRST .. my_tab.LAST Loop
Dbms_Output.Put_Line( 'my_tab(' || Ltrim(To_char(i)) || ') = ' ||
my_tab(i) ) ;
End loop ;
End ;
/
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:
Declare
TYPE TYP_TAB IS TABLE OF PLS_INTEGER ;
my_tab
TYP_TAB := TYP_TAB( 1, 2, 3, 4, 5 );
v Pls_Integer ;
Begin
my_tab.DELETE(2)
;
v := my_tab.first
;
Loop
Dbms_Output.Put_Line( 'my_tab(' || Ltrim(To_char(v)) || ') = ' ||
my_tab(v) ) ;
v := my_tab.NEXT(v) ; -- get the next valid subscript
Exit When v IS NULL ;
End loop ;
End ;
/
my_tab(1) = 1
my_tab(3) = 3
my_tab(4) = 4
my_tab(5) = 5
Another solution is to test if the index exists before use it:
Declare
TYPE TYP_TAB IS TABLE OF PLS_INTEGER ;
my_tab
TYP_TAB := TYP_TAB( 1, 2, 3, 4, 5 );
Begin
my_tab.DELETE(2)
;
For i IN
my_tab.FIRST .. my_tab.LAST Loop
If my_tab.EXISTS(i) Then
Dbms_Output.Put_Line(
'my_tab('
||
Ltrim(To_char(i))
|| ') = ' || my_tab(i) ) ;
End if ;
End loop ;
End ;
/
my_tab(1) = 1
my_tab(3) = 3
my_tab(4) = 4
my_tab(5) = 5
EXTEND[(n[,i])]
Used to extend a collection (add new elements) of a nested table or
varray. You cannot use EXTEND with Associative Arrays. The procedure
has 3 forms:
- EXTEND appends one null element to a collection.
- EXTEND(x) appends 'x' null elements to a collection.
- EXTEND(n,i) appends n copies of
the ith element to a collection.
You cannot use EXTEND to initialize an atomically null collection.
Also, if you impose the NOT NULL constraint on a TABLE or VARRAY
type, you cannot apply the first two forms of EXTEND to collections
of that type.
EXTEND operates on the internal size of a collection, which includes
any deleted elements. So, if EXTEND encounters deleted elements, it
includes them in its tally. PL/SQL keeps placeholders for deleted
elements so that you can replace them if you wish.
Consider the following example:
DECLARE
TYPE CourseList
IS TABLE OF VARCHAR2(10);
courses
CourseList;
BEGIN
courses :=
CourseList('Biol 4412', 'Psyc 3112', 'Anth 3001');
courses.DELETE(3); -- delete element 3
/* PL/SQL keeps a
placeholder for element 3. So, the next statement appends element 4,
not element 3. */
courses.EXTEND; -- append one null element
/* Now element 4
exists, so the next statement does not raise
SUBSCRIPT_BEYOND_COUNT. */
courses(4) :=
'Engl 2005';
When it includes deleted elements, the internal size of a nested
table differs from the values returned by COUNT and LAST. For
instance, if you initialize a nested table with five elements, then
delete elements 2 and 5, the internal size is 5, COUNT returns 3,
and LAST returns 4. All deleted elements (whether leading, in the
middle, or trailing) are treated alike
Declare
TYPE TYP_NES_TAB
is table of Varchar2(20) ;
tab1 TYP_NES_TAB
;
i Pls_Integer ;
Procedure Print( i in Pls_Integer ) IS
BEGIN
Dbms_Output.Put_Line( 'tab1(' ||
ltrim(to_char(i)) ||') = ' || tab1(i) ) ;
END ;
Procedure PrintAll IS
Begin
Dbms_Output.Put_Line( '* Print all collection *' ) ;
For i IN
tab1.FIRST..tab1.LAST Loop
If tab1.EXISTS(i) Then
Dbms_Output.Put_Line( 'tab1(' ||
ltrim(to_char(i)) ||') = ' || tab1(i) ) ;
End if ;
End loop ;
End ;
Begin
tab1 :=
TYP_NES_TAB('One') ;
i := tab1.COUNT ;
Dbms_Output.Put_Line( 'tab1.COUNT = ' || i ) ;
Print(i) ;
-- the following
line raise an error because the second index does not exists in
the collection --
-- tab1(2) :=
'Two' ;
-- Add one empty
element --
tab1.EXTEND ;
i := tab1.COUNT ;
tab1(i) := 'Two'
;
Printall ;
-- Add two empty elements --
tab1.EXTEND(2) ;
i := i + 1 ;
tab1(i) :=
'Three' ;
i := i + 1 ;
tab1(i) := 'Four'
;
Printall ;
-- Add three elements with the same value as element
4 --
tab1.EXTEND(3,1)
;
i := i + 3 ;
Printall ;
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
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.
Declare
TYPE TYP_TAB is
table of varchar2(100) ;
tab TYP_TAB
;
Begin
tab := TYP_TAB(
'One','Two','Three' ) ;
For i in
tab.first..tab.last Loop
dbms_output.put_line( 'tab(' || ltrim( to_char( i ) ) || ') = ' ||
tab(i) ) ;
End loop ;
-- add 3 element
with second element value --
dbms_output.put_line( '* add 3 elements *' ) ;
tab.EXTEND(3,2) ;
For i in
tab.first..tab.last Loop
dbms_output.put_line( 'tab(' || ltrim( to_char( i ) ) || ') = ' ||
tab(i) ) ;
End loop ;
-- suppress the
last element --
dbms_output.put_line( '* suppress the last element *' ) ;
tab.TRIM ;
For i in
tab.first..tab.last Loop
dbms_output.put_line( 'tab(' || ltrim( to_char( i ) ) || ') = ' ||
tab(i) ) ;
End loop ;
End;
/
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
If you try to suppress more elements than the collection
contents, you get a SUBSCRIPT_BEYOND_COUNT exception.
DELETE[(n[,m])]
· DELETE removes all elements from a collection.
· DELETE(n) removes the nth 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 the second element
Declare
TYPE TYP_TAB is table
of varchar2(100) ;
tab TYP_TAB ;
Begin
tab := TYP_TAB(
'One','Two','Three' ) ;
dbms_output.put_line(
'Suppression of the 2nd element' ) ;
tab.DELETE(2) ;
dbms_output.put_line(
'tab.COUNT = ' || tab.COUNT) ;
dbms_output.put_line(
'tab.LAST = ' || tab.LAST) ;
For i IN tab.FIRST ..
tab.LAST Loop
If
tab.EXISTS(i) Then
dbms_output.put_line( tab(i) ) ;
End if ;
End loop ;
End;
/
Suppression of the 2nd element
tab.COUNT = 2
tab.LAST = 3
One
Three
Caution:
For Varrays, you can suppress only the last element. If the element
does not exists, no exception is raised.
Handle Collections
Checking
if
a Collection Is Null
Nested tables and varrays can be atomically null, so they can be
tested for nullity:
DECLARE
TYPE Staff IS
TABLE OF Employee;
members Staff;
BEGIN
-- Condition yields
TRUE because we haven't used a constructor.
IF members IS NULL THEN ...
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 ;
Accesing
Specific
Element
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 ;
Assign
values between collections
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 ;
Comparing
collections
Until the 10g release, collections cannot be directly compared for
equality or inequality. The 10g release allows doing some
comparisons between collections:
You can compare collections of same type to verify if they are
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 and RGB have same members.');
END IF;
IF rgb !=
traffic_light THEN
dbms_output.put_line('RGB and TRAFFIC_LIGHT have different
members');
END IF;
END;
/
OK, PRIMARIES and RGB have
same members.
RGB and TRAFFIC_LIGHT have
different members
Another Example:
DECLARE
TYPE clientele IS
TABLE OF VARCHAR2 (64);
client_list_12 clientele := clientele ('Customer
1', 'Customer 2');
client_list_13 clientele := clientele ('Customer
1', 'Customer 3');
client_list_31 clientele := clientele ('Customer
3', 'Customer 1');
--
client_list_133 clientele := clientele
('Customer 1', 'Customer 3', 'Customer 3');
--
client_list_13n clientele := clientele ('Customer 1',
'Customer 3', NULL);
client_list_3n1 clientele := clientele ('Customer 3',
NULL, 'Customer 1');
PROCEDURE
compare_clients (title_in IN VARCHAR2, clients1_in IN clientele,
clients2_in IN clientele)
IS
BEGIN
DBMS_OUTPUT.put_line (title_in);
IF clients1_in = clients2_in
THEN
DBMS_OUTPUT.put_line
('
Client
list 1 = Client list 2');
ELSIF clients1_in != clients2_in
THEN
DBMS_OUTPUT.put_line
('
Client
list 1 != Client list 2');
ELSIF (clients1_in = clients2_in) IS NULL
THEN
DBMS_OUTPUT.put_line
('
NULL
result');
END IF;
END
compare_clients;
BEGIN
compare_clients
('1,2 compared to 1,3', client_list_12, client_list_13);
--
compare_clients
('1,3 compared to 3,1', client_list_13, client_list_31);
--
compare_clients
('1,3,3 compared to 3,1', client_list_133, client_list_31);
--
compare_clients
('1,3,NULL compared to 3,NULL,1', client_list_13n,
client_list_3n1);
compare_clients
('1,3,NULL compared to 1,3,NULL', client_list_13n,
client_list_13n);
END;
/
Operations
with
Collections
You can also apply some operators on the collections:
|
Operator
|
Description
|
|
submultiset
|
Tests whether a given
nested table is a subset of another nested table.
|
|
multiset union
|
Returns a nested table
that contains the values of two inputted nested tables.
The all or distinct option may be used, as required, to
allow for or eliminate duplicates, including NULL values.
|
|
multiset intersect
|
Returns a nested table
that contains values that are common to the two nested
tables passed in through the input operators. The all or
distinct option may be used, as required, to allow for or
eliminate duplicates, including NULL values.
|
|
multiset except
|
This operator takes the
names of two nested tables as parameters. When executed it
will return a nested table which contains elements listed
in the first parameter of the function, that do not exist
in the parameter. The all or distinct option may be used,
as required, to allow for or eliminate duplicates,
including NULL values.
|
|
cardinality
|
Returns the number of
elements contained within a given nested table.
|
|
member of
|
Makes it possible to test
a given value and determine whether it is a member of a
nested table. The result is a Boolean value.
|
|
set
|
Used to convert a nested
table into a set of elements that are distinct. This set
is returned in the form of a nested table.
|
|
is a set
|
Used to determine whether
a nested table is made up of distinct elements. If a
duplicate value is contained in the nested table, then the
operator returns FALSE; otherwise, TRUE is returned.
|
|
is empty
|
Used to determine whether
a given nested table is empty.
|
|
collect
|
Creates a nested table
from a set of elements.
|
|
powermultiset
|
Used on nested tables to
generate multiple sets of nested table entries from a
given nested table.
|
|
powermultiset_by_cardinality
|
Used on nested tables to
generate multiple sets of nested table entries based on a
specified cardinality. This restricts the depth of the
resulting nested table output.
|
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 correspond
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 element, operation performed by the SET
statement, is like a distinct
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
Collections
and DB Tables
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.
Nested tables
CREATE [OR REPLACE] TYPE [schema. .] type_name { IS | AS }
TABLE OF datatype;
Varrays
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.
select type_name, typecode, attributes from
user_types;
TYPE_NAME
TYPECODE
ATTRIBUTES
------------------------------
------------------------------
----------
TYP_LIG_ENV
OBJECT
5
TYP_TAB_LIG_ENV
COLLECTION
0
You can query the USER_COLL_TYPES view to get information on
the collections created in the database.
select type_name, coll_type, elem_type_owner,
elem_type_name from user_coll_types;
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.
select type_name, attr_name, attr_type_name,
length, precision, scale, attr_no from user_type_attrs;
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
1
Insertion
Add a line in the INVOICE table
Use the INSERT statement with all the constructors needed for
the collection
INSERT INTO INVOICE VALUES
(1 ,1000 ,SYSDATE
, TYP_TAB_LIG_ENV -- Table of objects constructor
( TYP_LIG_ENV( 1 ,'COD_01', 1000, 5.0, 1 )
-– object constructor
)
);
Add a line to the collection
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.
INSERT INTO TABLE (SELECT inv_line FROM INVOICE
WHERE inv_num = 1) VALUES( TYP_LIG_ENV( 2 ,'COD_02', 50,
5.0, 10 ) );
Multiple inserts
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;
2
Update
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
UPDATE TABLE (SELECT inv_line FROM INVOICE WHERE
inv_num = 1) nt
SET nt.ligqty = 10
WHERE
nt.lig_num = 1;
Update all the rows of the collection
UPDATE TABLE (SELECT inv_line FROM INVOICE WHERE
inv_num = 1) nt
SET nt.lig_pht =
nt.lig_pht * .1;
2.2 Varray
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
)
)
) ;
-- Query the varray collection --
Declare
v_table TYP_VAR_LIG_ENV ;
LC$Head Varchar2(200) ;
LC$Lig Varchar2(200) ;
Begin
LC$Head :=
'Num Code
Pht
Tva Qty' ;
Select
inv_line Into v_table
From INVOICE_V
Where inv_num = 1
For Update of inv_line ;
dbms_output.put_line ( LC$Head ) ;
For i IN
v_table.FIRST .. v_table.LAST Loop
LC$Lig := Rpad(To_char( v_table(i).lig_num ),3) || ' '
||
Rpad(v_table(i).lig_code,
10)
||
' '
||
Rpad(v_table(i).lig_pht,10)
||
'
'
||
Rpad(v_table(i).lig_tva,10)
||
'
'
|| v_table(i).ligqty
;
dbms_output.put_line( LC$Lig )
;
End loop ;
End ;
/
Num
Code
Pht
Tva Qty
1
COD_01
1000
5 1
2
COD_02
500
5 10
3
COD_03
10
5 100
Update the second line of the varray to change the quantity
Declare
v_table TYP_VAR_LIG_ENV ;
Begin
Select
inv_line Into
v_table
From INVOICE_V
Where inv_num = 1
For Update of inv_line ;
v_table(2).ligqty := 2 ; -- update the second element
Update
INVOICE_V Set inv_line = v_table Where inv_num = 1 ;
End ;
/
Display the new varray:
-- Query the varray collection --
Declare
v_table TYP_VAR_LIG_ENV ;
LC$Head Varchar2(200) ;
LC$Lig Varchar2(200) ;
Begin
LC$Head :=
'Num Code
Pht
Tva Qty' ;
Select
inv_line Into v_table
From INVOICE_V
Where inv_num = 1
For Update of inv_line ;
dbms_output.put_line ( LC$Head ) ;
For i IN
v_table.FIRST .. v_table.LAST Loop
LC$Lig := Rpad(To_char( v_table(i).lig_num ),3) || ' '
||
Rpad(v_table(i).lig_code,
10)
||
' '
||
Rpad(v_table(i).lig_pht,10)
||
'
'
||
Rpad(v_table(i).lig_tva,10)
||
'
'
|| v_table(i).ligqty
;
dbms_output.put_line( LC$Lig )
;
End loop ;
End ;
/
Num Code
Pht
Tva Qty
1
COD_01
1000
5 1
2
COD_02
500
5 2
3
COD_03
10
5 100
3 Delete
3.1 Nested table
Use the DELETE FROM TABLE statement.
DELETE FROM TABLE
( SELECT
the_collection FROM the_table WHERE ... ) alias
WHERE alias.col_name =
...
Delete a single collection row
DELETE FROM TABLE (SELECT
inv_line FROM INVOICE WHERE inv_num = 1) nt
WHERE nt.lig_num
= 2;
Delete all the collection rows
DELETE FROM TABLE (SELECT inv_line FROM INVOICE
WHERE inv_num = 1) nt;
Use of a PL/SQL record to handle the whole structure
Declare
TYPE TYP_REC IS
RECORD
(
inv_num INVOICE.inv_num%Type,
inv_numcli INVOICE.inv_numcli%Type,
inv_date INVOICE.inv_date%Type,
inv_line INVOICE.inv_line%Type
–- collection line
);
rec_inv
TYP_REC ;
Cursor C_INV IS
Select * From INVOICE ;
Begin
Open C_INV
;
Loop
Fetch C_INV into rec_inv ;
Exit when C_INV%NOTFOUND ;
For i IN 1 .. rec_inv.inv_line.LAST Loop –- loop
through the collection lines
dbms_output.put_line(
'Numcli/Date
'
||
rec_inv.inv_numcli || '/' || rec_inv.inv_date
||
'
Line
'
|| rec_inv.inv_line(i).lig_num
||
'
code
'
|| rec_inv.inv_line(i).lig_code || ' Qty '
|| To_char(rec_inv.inv_line(i).ligqty) ) ;
End loop ;
End loop ;
End ;
/
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
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.
Declare
v_table TYP_VAR_LIG_ENV ;
v_tmp v_table%Type :=
TYP_VAR_LIG_ENV() ;
ind pls_integer :=
1 ;
Begin
-- select
the collection --
Select
inv_line
Into v_table
From INVOICE_V
Where
inv_num = 1
For Update
of inv_line ;
-- Extend
the temporary varray --
v_tmp.EXTEND(v_table.LIMIT) ;
For i IN
v_table.FIRST .. v_table.LAST Loop
If v_table(i).lig_num <> 2 Then
v_tmp(ind)
:=
v_table(i)
;
ind := ind + 1 ;
End
if ;
End loop ;
Update
INVOICE_V Set inv_line = v_tmp Where inv_num = 1 ;
End ;
/
Display the new collection:
Declare
v_table TYP_VAR_LIG_ENV ;
LC$Head Varchar2(200) ;
LC$Lig Varchar2(200) ;
Begin
LC$Head :=
'Num Code
Pht
Tva Qty' ;
Select
inv_line Into v_table From INVOICE_V Where inv_num = 1 For
Update of inv_line ;
dbms_output.put_line ( LC$Head ) ;
For i IN
v_table.FIRST .. v_table.LAST Loop
LC$Lig := Rpad(To_char( v_table(i).lig_num ),3) || ' '
||
Rpad(v_table(i).lig_code,
10)
||
' '
||
Rpad(v_table(i).lig_pht,10)
||
'
'
||
Rpad(v_table(i).lig_tva,10)
||
'
'
|| v_table(i).ligqty
;
dbms_output.put_line( LC$Lig )
;
End loop ;
End ;
/
Num
Code
Pht
Tva Qty
1
COD_01
1000
5 1
3
COD_03
10
5 100
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:
Begin
DEL_ELEM_VARRAY
('INVOICE_V', 'inv_num', 'TYP_VAR_LIG_ENV', 'inv_line', '1',
'lig_num', '3');
End ;
/
4
Query
Query the whole table
select * from INVOICE;
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))
Not easy to read ! Let’s try another syntax:
SELECT t1.inv_num, t1.inv_numcli, t1.inv_date,
t2.* FROM invoice t1, TABLE(t1.inv_line) t2
ORDER BY
t1.inv_num, t2.lig_num desc;
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.
Query one main table row with a particular collection row
SELECT t1.inv_num, t1.inv_numcli,
t1.inv_date, t2.* FROM invoice t1, TABLE(t1.inv_line) t2
WHERE t1.inv_num = 1
AND t2.lig_code = 'COD_01';
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
Query only the collection lines
select t2.* from invoice t1,TABLE(t1.inv_line)
t2;
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
Query the collection for a particular parent row
Use the SELECT FROM TABLE statement
SQL Mode
SELECT FROM TABLE
( SELECT
the_collection FROM the_table WHERE ... )
select * from TABLE(SELECT inv_line FROM INVOICE
WHERE inv_num = 1);
LIG_NUM
LIG_CODE
LIG_PHT LIG_TVA
LIGQTY
----------
-------------------- ---------- ---------- ----------
1
COD_01
1000
5 1
2
COD_02
50
5 10
Another syntax:
Select t2.* from invoice t1,TABLE(t1.inv_line)
t2
Where
t1.inv_numcli = 1000;
LIG_NUM
LIG_CODE
LIG_PHT LIG_TVA
LIGQTY
----------
-------------------- ---------- ---------- ----------
1
COD_01
1000
5 1
2
COD_02
50
5 10
PL/SQL Mode
Declare
TYPE TYP_REC IS
RECORD
(
num INV_LINE_TABLE.LIG_NUM%Type,
code INV_LINE_TABLE.LIG_CODE%Type,
pht INV_LINE_TABLE.LIG_PHT%Type,
tva INV_LINE_TABLE.LIG_TVA%Type,
qty INV_LINE_TABLE.LIGQTY%Type
);
-- Table of
records --
TYPE TAB_REC IS
TABLE OF TYP_REC ;
t_rec
TAB_REC ;
Begin
-- Store the
lines into the table of records --
Select * BULK COLLECT INTO t_rec
from TABLE(SELECT inv_line FROM INVOICE WHERE
inv_num = 1) nt ;
-- Print the
record attributes of each line--
For i IN
t_rec.FIRST .. t_rec.LAST Loop
dbms_output.put_line( '** Line = ' || t_rec(i).num ||
' **' ) ;
dbms_output.put_line( 'Code = ' ||
t_rec(i).code ) ;
dbms_output.put_line( 'Price = ' ||
t_rec(i).pht ) ;
dbms_output.put_line( 'Tax rate = ' || t_rec(i).tva ) ;
dbms_output.put_line( 'Quantity = ' || t_rec(i).qty ) ;
End loop ;
End ;
/
** Line = 1 **
Code
= COD_01
Price
= 1000
Tax rate = 5
Quantity = 1
** Line = 2 **
Code
= COD_02
Price
= 50
Tax rate = 5
Quantity = 10
Query a particular column of the collection
SQL Mode
SELECT nt.lig_code, nt.lig_pht
FROM
TABLE (SELECT inv_line FROM INVOICE WHERE inv_num = 1) nt
WHERE
nt.lig_num = 1;
LIG_CODE
LIG_PHT
--------------------
----------
COD_01
1000
Another syntax:
Select t2.* from invoice t1,TABLE(t1.inv_line)
t2
Where
t1.inv_numcli = 1000
And t2.lig_num = 1;
LIG_NUM
LIG_CODE
LIG_PHT LIG_TVA
LIGQTY
----------
-------------------- ---------- ---------- ----------
1
COD_01
1000
5 1
PL/SQL Mode
Declare
TYPE t_rec IS
RECORD
(
num INV_LINE_TABLE.LIG_NUM%Type,
code INV_LINE_TABLE.LIG_CODE%Type,
pht INV_LINE_TABLE.LIG_PHT%Type,
tva INV_LINE_TABLE.LIG_TVA%Type,
qty INV_LINE_TABLE.LIGQTY%Type
);
rec t_rec ;
Begin
-- Store the line
into the record --
Select *
Into
rec
from
TABLE(SELECT inv_line FROM INVOICE WHERE inv_num = 1) nt
Where
nt.lig_num = 1 ;
-- Print the
record attributes --
dbms_output.put_line( 'Code = ' ||
rec.code ) ;
dbms_output.put_line( 'Price = ' ||
rec.pht ) ;
dbms_output.put_line( 'Tax rate = ' || rec.tva ) ;
dbms_output.put_line( 'Quantity = ' || rec.qty )
;
End ;
/
Code
= COD_01
Price
= 1000
Tax rate = 5
Quantity = 1
Query both table and collection, All the collection’s rows
SQL Mode
SELECT v.inv_numcli, v.inv_date, nt.lig_code,
nt.lig_pht
FROM
INVOICE v,
TABLE
(SELECT
inv_line
FROM
INVOICE WHERE inv_num = 1) nt
WHERE
v.inv_num = 1;
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
SELECT v.inv_numcli, v.inv_date, nt.lig_code,
nt.lig_pht
FROM
INVOICE v,
TABLE
(SELECT
inv_line
FROM
INVOICE WHERE inv_num = 1) nt
WHERE
v.inv_num = 1
AND nt.lig_num = 1;
INV_NUMCLI INV_DATE
LIG_CODE
LIG_PHT
---------- --------
-------------------- ----------
1000
11/11/05
COD_01
1000
PL/SQL Mode
Declare
invoice_rec
INVOICE%ROWTYPE ;
LC$Print
Varchar2(512) ;
Begin
-- Select the
INVOICE line --
Select *
Into
invoice_rec
From
INVOICE
Where
inv_numcli = 1000 ;
-- Print the
parent and collection attributes--
For i IN
invoice_rec.inv_line.FIRST .. invoice_rec.inv_line.LAST Loop
LC$Print := invoice_rec.inv_numcli
|| ' - ' ||
To_Char(invoice_rec.inv_date,'DD/MM/YYYY')
|| ' - ' ||
invoice_rec.inv_line(i).lig_num
|| ' - ' ||
invoice_rec.inv_line(i).lig_code
|| ' - ' ||
invoice_rec.inv_line(i).lig_pht
|| ' - ' ||
invoice_rec.inv_line(i).lig_tva
|| ' - ' ||
invoice_rec.inv_line(i).ligqty ;
dbms_output.put_line( LC$Print ) ;
End loop ;
End ;
/
1000 - 11/11/2005 - 1 -
COD_01 - 1000 - 5 - 1
1000 - 11/11/2005 - 2 -
COD_02 - 50 - 5 - 10
What happens when
the collection is empty ?
Let’s insert a row with an empty collection:
INSERT INTO INVOICE VALUES
(3,1001 ,SYSDATE
,
TYP_TAB_LIG_ENV() -- Empty collection
) ;
SELECT v.inv_numcli, v.inv_date, nt.lig_code,
nt.lig_pht
FROM
INVOICE v,
TABLE
(SELECT
inv_line
FROM
INVOICE WHERE inv_num = 1) nt
WHERE
v.inv_num = 1;
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
SELECT v.inv_numcli, v.inv_date,
CURSOR(
SELECT nt.lig_code, nt.lig_pht FROM TABLE (inv_line) nt)
FROM INVOICE v;
INV_NUMCLI INV_DATE
CURSOR(SELECTNT.LIG_
---------- --------
--------------------
1001
11/11/05 CURSOR STATEMENT : 3
CURSOR STATEMENT : 3
Full example
moving from Cursor-Loops to Collections and Bulks
Let's say that we want to load one table into another one:
DECLARE
BEGIN
FOR x IN (SELECT * FROM all_objects)
LOOP
INSERT INTO t1
(owner, object_name, subobject_name, object_id,
data_object_id,
object_type, created, last_ddl_time,
timestamp, status,
temporary, generated, secondary)
VALUES (x.owner,
x.object_name, x.subobject_name, x.object_id,
x.data_object_id,
x.object_type, x.created,
x.last_ddl_time,
x.timestamp, x.status, x.temporary, x.generated, x.secondary);
END LOOP;
COMMIT;
END test_proc;
Elapsed: 00:00:20.02
This procedure does three things:
1. Declares a cursor that points to the resultset from SELECT *
FROM ALL_OBJECTS
2. Starts at record one, and inserts into the t1 table the columns
from the first row in the cursor (here is the BIG problem a lot of
calls between PL/SQL and SQL)
3. Then, it loops back and gets the next row of data, until all
rows from the cursor have been retrieved.
The data is then committed, and the procedure ends.
The following solution uses a nested
table
to hold the data from the ALL_OBJECTS table, and performs
BULK COLLECT to load all of the source tables' data into the
nested table.
truncate table t1;
CREATE
OR REPLACE PROCEDURE fast_proc (p_array_size IN PLS_INTEGER
DEFAULT 100)
IS
TYPE My_ARRAY
IS TABLE OF all_objects%ROWTYPE;
l_data My_ARRAY;
CURSOR c IS
SELECT *
FROM all_objects;
BEGIN
OPEN c;
LOOP
FETCH
c BULK COLLECT INTO l_data LIMIT p_array_size;
FORALL
i IN 1..l_data.COUNT
INSERT INTO t1 VALUES l_data(i);
EXIT WHEN
c%NOTFOUND;
END LOOP;
CLOSE c;
END fast_proc;
/
Elapsed: 00:00:09.06
The next example is a variation on this, that does much the same
thing with slightly more compact code, I just removed the cursor.
truncate table t1;
create
or replace procedure fast_proc2
is
TYPE My_ARRAY IS TABLE OF
all_objects%ROWTYPE;
l_data My_ARRAY;
begin
--Here I put all the rows in memory on this
collection
select
* BULK COLLECT INTO l_data
from
ALL_OBJECTS;
-- Now I work with that collection
FORALL
x in l_data.First..l_data.Last
INSERT
INTO t1 VALUES l_data(x) ;
end;
/
Elapsed: 00:00:09.27
Bulk Binding
Bulk binding improves performance by reducing the
context switches between the PL/SQL and SQL engines for execution
of SQL statements. Bulk Collect causes the SQL engine to bulk-bind
the entire output collection before sending it to the PL/SQL
engine. An ‘in-bind’ is when we pass a value from
a program to the SQL engine, often either to constraint on a column or to
specify a value for a DML statement
| Commonly, in-binds are only of interest because they are
essential for SQL statements to be sharable.
When DBA’s talk of the importance of applications
using ‘bind variables’ it is in the context of in-binds
since, in applications that use dynamic SQL, using literals
instead of bind variables causes each SQL statement to be
parsed. This
is a critical consideration for overall database performance |
An ‘out-bind’
occurs when values are passed from the SQL engine back to the
host language.
When processing a cursor, application developers can choose to
either fetch back values one-at-a-time or returned in a batch
operation which will bind back many rows to the host application
in a single operation.
Before Oracle 8i values being bound out into PL/SQL
host variables had to be fetched one at a time.
The following CURSOR FOR-LOOP construct is a familiar one.
--Archive historical data
DECLARE
CURSOR sales_cur (p_customer_id NUMBER) IS
SELECT * FROM sales
WHERE customer_id =
p_customer_id;
v_customer_id NUMBER := 1234;
BEGIN
FOR rec IN sales_cur(v_customer_id) LOOP
INSERT INTO sales_hist(customer_id,
detail_id, process_date)
VALUES
(v_customer_id, rec.sales_id, sysdate);
END LOOP;
END;
--Elapsed:
00:00:44.02
for 360,000 records
--The
insert was executed 36,0352 times
| In a CURSOR FOR-LOOP, a record variable is implicitly
declared that matches the column list of the cursor. On
each iteration of the loop, the execution context is
switched from the PL/SQL engine to the SQL engine, performing
an out-bind of the column values into the record variable
once for each loop iteration. Likewise,
an
in-bind for the insert statement will occur once on each
iteration. Although stored PL/SQL
code has the advantage over other host languages of keeping
this interaction within the same process, the context
switching between the SQL engine and the PL/SQL engine is
relatively expensive making the above code very inefficient.In addition, the cursor is defined as
SELECT * instead of just selecting from the columns to be
utilized which is also inefficient. Whether
the code references a column or not, Oracle will have to
fetch and bind over all of the columns in the select list,
slowing down code execution |
A better way to perform the above task would be to
utilize bulk binding, for both the fetch and the insert
statements. We have two new PL/SQL
operators to accomplish this. The BULK COLLECT (for SELECT and
FETCH) statement is used to specify bulk
out-binds; while the FORALL
(for INSERT, UPDATE and DELETE) statement is used to
provide bulk in-binds for DML statements.
According to the documentation, FORALL
is defined as:
"The keyword FORALL instructs the PL/SQL engine to bulk-bind input
collections before sending them to the SQL engine. Although the
FORALL statement contains an iteration scheme, it is not a FOR
loop. Its syntax follows:
FORALL
index IN lower_bound..upper_bound
INSERT/UPDATE/DELETE Statements;
and BULK COLLECT is explained as;
"The keywords BULK COLLECT tell the SQL engine to bulk-bind output
collections before sending them to the PL/SQL engine. You can use
these keywords in the SELECT INTO, FETCH INTO, and RETURNING
INTO clauses. Here is the syntax:
... BULK
COLLECT INTO collection_name[, collection_name] ..."
The index can be referenced only within the FORALL statement
and only as a collection subscript. The SQL statement must be an
INSERT, UPDATE, or DELETE statement that references collection
elements. And, the bounds must specify a valid range of consecutive
index numbers. The SQL engine executes the SQL statement once for
each index number in the range."
So the previous query could be re-defined as:
--Archive historical data
DECLARE
-- Here I defined a type based on a field of one table
TYPE sales_typ IS TABLE OF sales.sales_id%TYPE INDEX BY
BINARY_INTEGER;
--Define sales_ids as the sales_typ type
sales_ids sales_t;
v_customer_id NUMBER := 1234;
max_rows CONSTANT
NUMBER := 100;
CURSOR sales_cur (p_customer_id NUMBER) IS
SELECT sales_id FROM sales
WHERE customer_id = p_customer_id;
BEGIN
OPEN sales_cur(v_customer_id);
LOOP
EXIT WHEN sales_cur%NOTFOUND;
FETCH sales_cur BULK COLLECT INTO sales_ids
LIMIT max_rows;
FORALL i IN 1..sales_ids.COUNT
INSERT INTO sales_hist
(customer_id, detail_id, process_date)
VALUES(v_customer_id, sales_ids(i), sysdate);
END LOOP;
CLOSE sales_cur;
END;
--Elapsed:
00:00:08.02
for 360,000 records
--The
insert was executed 72 times only
In this example, the fetch statement
returns with the sales_ids array populated with all of the values
fetched for the current iteration, with the maximum number of rows
fetched set to 10,000. Using this
method, only a single context switch is required for the SELECT
statement to populate the sales_ids array and another switch to
bind all of the fetched values to the INSERT statements. Note also that the FORALL statement is
not a looping construct – the array of values is given over in
batch to the SQL engine for binding and execution.
This
second implementation will run at approximately 15 times the speed
of the first, illustrating the importance of efficient binding in
data driven code.
One potential issue with the bulk binding
technique is the use of memory by the PL/SQL array variables. When a BULK COLLECT statement returns,
all of the fetched values are stored in the target array. If the number of values returned is very
large, this type of operation could lead to memory issues on the
database server. The memory consumed
by PL/SQL variables is private memory, allocated dynamically from
the operating system. In dedicated
server mode it would be the server process created for the current
session that allocates memory. In the
case where such allocation becomes extreme, either the host will
become memory bound or the dedicated server process will reach a
size where it tries to allocate beyond its addressing limits,
normally 2 GB on many platforms. In
either case the server processes call to malloc()
will fail raising an ORA-04030 out of process memory error.
To prevent this possibility when loading
anything larger than a small reference table, use the
optional LIMIT ROWS operator to control the ‘batch size’ of
each BULK COLLECT operation. In the
code example below the cursor will iterate though batches of 100
rows fetching in the values and inserting 100 rows.
Do not go over 500. On the final iteration, the cursor will
fetch the remaining balance. Placement
of the EXIT WHEN clause should be before the FETCH statement or
the last, incomplete batch will not be processed.
Oracle9i
Release 2 also allows updates using record definitions by using
the ROW keyword:
DECLARE
TYPE test1_tab IS
TABLE OF test1%ROWTYPE;
t_tab test1_tab
:= test1_tab();
BEGIN
FOR i IN 1 .. 10000
LOOP
t_tab.extend;
t_tab(t_tab.last).id
:= i;
t_tab(t_tab.last).description := 'Description: ' || To_Char(i);
END LOOP;
FOR i IN t_tab.first
.. t_tab.last LOOP
UPDATE
test1
SET ROW = t_tab(i)
WHERE id = t_tab(i).id;
END LOOP;
COMMIT;
END;
/
SELECT with RECORD bind
Please remember that you could BULK SELECT INTO a
collection of records. For example, suppose that we would like to
retrieve
all employees hired before June 25, 1997, and then
give them all big, fat raises.
With Oracle9i Release 2,
our program becomes much shorter, intuitive and maintainable. What
you see below is all we need to write to take advantage of BULK
COLLECT to populate a single associative array of records:
DECLARE
v_emprecs
emp_util.emprec_tab_t;
CURSOR cur IS SELECT
* FROM employees
WHERE hire_date <
'25-JUN-97';
BEGIN
OPEN cur;
FETCH cur BULK COLLECT
INTO v_emprecs LIMIT 10;
CLOSE cur;
emp_util.give_raise
(v_emprecs);
END;
[Note:
the clause limit 10 is equivalent
to where rownum
<= 10.]
Even more
wonderful, we can now combine BULK COLLECT fetches into records
with NATIVE DYNAMIC SQL.
Here is an example, in which we give raises to employees for a
specific schema:
CREATE
OR
REPLACE PROCEDURE give_raise (schema_in IN VARCHAR2)
IS
v_emprecs
emp_util.emprec_tab_t;
cur
SYS_REFCURSOR;
BEGIN
OPEN cur FOR
'SELECT
* FROM ' || schema_in ||
'.employees' || 'WHERE hire_date < :date_limit' USING '25-JUN-97';
FETCH cur BULK COLLECT
INTO v_emprecs LIMIT 10;
CLOSE cur;
emp_util.give_raise ( schema_in, v_emprecs);
END;
SYS_REFCURSOR
is
a
pre-defined
weak
REF
CURSOR type.
INSERT
with RECORD bind
Since Oracle9i Release 2 we can now take advance of simple, intuitive and
compact syntax to bind an entire record to a row in an insert.
This is shown below:
DECLARE
v_emprec employees%rowtype
:= Emp_Util.Get_One_Row;
BEGIN
INSERT INTO employees_retired
VALUES v_emprec;
END;
Notice
that we do not put the record inside parentheses. You are,
unfortunately, not able to use this technique with Native Dynamic
SQL. You can, on the other hand, insert using a record in the
highly efficient FORALL statement. This technique is valuable when
you are inserting a large number of rows.
Take a look at the following example. The following table explains
the interesting parts of the retire_them_now procedure
CREATE OR REPLACE
PROCEDURE retire_them_now
IS
--Declare an Exception
bulk_errors EXCEPTION;
PRAGMA EXCEPTION_INIT (bulk_errors, -24381);
--Declare an associative array as rowtype of the
employees table.
TYPE employees_t IS TABLE OF employees%ROWTYPE
INDEX BY PLS_INTEGER;
retirees employees_t;
BEGIN
--Load up the array with the information for all
employees who are over 40 years of age
FOR rec IN (SELECT * FROM employees
WHERE
hire_date
<
ADD_MONTHS
(SYSDATE,
-1 * 18 * 40))
LOOP
retirees (SQL%ROWCOUNT) := rec;
END LOOP;
-- Added a clause to "catch" exceptions
FORALL indx IN retirees.FIRST .. retirees.LAST
SAVE EXCEPTIONS
INSERT INTO employees
VALUES
retirees (indx);
EXCEPTION
--Catch the exception
WHEN bulk_errors THEN
FOR j IN 1 ..
SQL%BULK_EXCEPTIONS.COUNT
LOOP
DBMS_OUTPUT.PUT_LINE (
'Error from element #'
||TO_CHAR(SQL%BULK_EXCEPTIONS(j).error_index) || ': '
||SQLERRM(SQL%BULK_EXCEPTIONS(j).error_code));
END LOOP;
END;
UPDATE SET ROW with RECORD
bind
Oracle9i Release 2 now
gives you an easy and powerful way to update an entire row in a
table from a record: the SET ROW clause. The ROW keyword is
functionally equivalent to *. It is most
useful when the source of the row is one table and the target is a
different table with the same column specification, for example in
a scenario where rows in an application table are updated once or
many times and may eventually be deleted, and where the latest
state of each row (including when it has been deleted) must be
reflected in an audit table. (Ideally we’d use MERGE with a RECORD
bind, but this isn’t supported yet.). The new
syntax for the Static SQL, single row case is obvious and compact:
DECLARE
v_emprec employees%ROWTYPE := emp_util.get_one_row;
BEGIN
v_emprec.salary := v_emprec.salary
* 1.2;
UPDATE
employees_2 SET ROW = v_emprec
WHERE
employee_id = v_emprec.employee_id;
END;
DELETE and
UPDATE with RETURNING with RECORD bind
You can
also take advantage of rows when using the RETURNING clause in both DELETEs and UPDATEs.
The RETURNING clause
allows you to retrieve and return information that is processed in
the DML statement without using a separate, subsequent query.
Record-based functionality for RETURNING means that you can return
multiple pieces of information into a record, rather than
individual variables. Example: RETURNING multiple
rows of information from an UPDATE statement.
DECLARE
v_emprecs
emp_util.emprec_tab_t;
BEGIN
UPDATE employees SET
salary = salary * 1.1
WHERE
hire_date < = '25-JUN-97'
RETURNING employee_id, first_name,
last_name, email, phone_number, hire_date,
job_id,
salary, commission_pct, manager_id, department_id
BULK COLLECT INTO
v_emprecs;
END;
Another Example returning
just 1 row:
DECLARE
TYPE EmpRec IS
RECORD (last_name employees.last_name%TYPE,
salary
employees.salary%TYPE);
emp_info
EmpRec;
emp_id NUMBER := 100;
BEGIN
UPDATE
employees SET salary = salary * 1.1
WHERE
employee_id = emp_id
RETURNING
last_name, salary INTO emp_info;
DBMS_OUTPUT.PUT_LINE('Just gave a raise to ' ||
emp_info.last_name ||', who now makes ' || emp_info.salary);
ROLLBACK;
END;
/
Another Example returning several rows:
drop table
emp_temp;
CREATE TABLE emp_temp AS
SELECT * FROM employees;
DECLARE
TYPE NumList IS
TABLE OF NUMBER;
depts NumList
:= NumList(10,20,30);
TYPE enum_t IS
TABLE OF employees.employee_id%TYPE;
TYPE dept_t IS
TABLE OF employees.department_id%TYPE;
e_ids enum_t;
d_ids dept_t;
BEGIN
FORALL j IN
depts.FIRST..depts.LAST
DELETE
FROM emp_temp WHERE department_id = depts(j)
RETURNING employee_id,
department_id
BULK COLLECT INTO e_ids,
d_ids;
DBMS_OUTPUT.PUT_LINE('Deleted ' || SQL%ROWCOUNT || ' rows:');
FOR i IN e_ids.FIRST
.. e_ids.LAST
LOOP
DBMS_OUTPUT.PUT_LINE('Employee #' || e_ids(i) || ' from dept #'
|| d_ids(i));
END LOOP;
END;
/
NOTE:
You cannot use returning bulk collect into a for INSERT ....
INTO ... SELECT.... statement. It can be used for update and
delete.
"insert as select" is currently not supported with the returning
clause, you can ONLY use insert with the "values" clause.
So the following statement will return errors:
create
table emp_temp as select * from emp where 1 = 0;
declare
type num_type
is table of number;
l_empno
num_type := num_type();
begin
insert into
emp_temp(empno,ename)
select
empno,ename from
emp
returning empno
bulk collect into l_empno;
end;
/
You currently have to two step this by
a) selecting bulk collecting the data
b) forall i bulk inserting the data.
declare
type num_type
is table of number;
l_empno
num_type;
type var_type
is table of varchar(10);
l_empname
var_type;
begin
select
empno,ename bulk collect into l_empno,l_empname from emp;
forall i in
l_empno.first..l_empno.last
insert
into emp_temp(empno,ename)
values(l_empno(i),l_empname(i));
end;
/
Bulk BinDing in Native dynamic SQL
In-Binding
Both the EXECUTE IMMEDIATE
and FORALL (for bulk
DML operations) offer a USING
clause to bind variable values into the SQL statement. Let's
follow the progression of explicit row-from-row processing to bulk
binding to bulk binding in native dynamic DML to see how the USING
clause is deployed.
We start with this kind of explicit FOR loop in our Oracle7 and
Oracle8 code base:
FOR indx
IN employee_ids.FIRST .. employee_ids.LAST
LOOP
UPDATE
employees SET salary = salary * 1.1
WHERE employee_id = employee_ids (indx);
END
LOOP;
Then, with Oracle8i, we
get rid of most of the context switches by moving to FORALL:
FORALL
indx IN employee_ids.FIRST .. employee_ids.LAST
UPDATE employees SET salary = salary * 1.1
WHERE
employee_id = employee_ids (indx);
And that handles all of our needs-unless, once again, we need or
would like to perform this same operation on different tables,
based on location (or for any other kind of dynamic SQL
situation). In this case, we can combine FORALL with EXECUTE
IMMEDIATE, with these wonderful results:
CREATE
OR REPLACE PROCEDURE upd_employees (
loc_in IN VARCHAR2,
employees_in IN employees_t )
IS
BEGIN
FORALL
indx in employees_in.first..employees_in.last
EXECUTE IMMEDIATE 'UPDATE ' ||
loc_in || ' employees SET salary = salary*1.1' || ' WHERE
employee_id = :the_id'
USING employee_in (indx);
END;
Notice that in the USING clause, we must include both the name of
the collection and the subscript for a single row using the same
FORALL loop index variable.
Out-Binding
Let's again follow the progression from individual row updates to
bulk bind relying on BULK COLLECT INTO to retrieve information,
and finally the dynamic approach possible in Oracle 9i.
Oracle8 enhanced DML capabilities by providing support for the
RETURNING clause. Shown in the following FOR loop, it allows us to
obtain information (in this case, the updated salary) from the DML
statement itself (thereby avoiding a separate and expensive
query).
BEGIN
FOR
indx IN employee_ids.FIRST .. employee_ids.LAST
LOOP
UPDATE employees SET
salary = salary * 1.1
WHERE employee_id = employee_ids (indx)
RETURNING salary INTO salaries (indx);
END
LOOP;
END;
Starting with Oracle8i, we can take advantage of FORALL to
improve performance dramatically:
BEGIN
FORALL
indx IN employee_ids.FIRST .. employee_ids.LAST
UPDATE employees SET
salary = salary * 1.1
WHERE employee_id = employee_ids (indx)
RETURNING salary BULK
COLLECT INTO salaries;
END;
There's one seemingly odd aspect of this code you should
remember: Inside the DML statement, any reference to the
collection that drives the FORALL statement must be subscripted as
in:
WHERE employee_id = employee_ids (indx)
In the RETURNING clause, however, you BULK COLLECT INTO the
collection and not a single subscripted row of the collection.
That's all well and good, but what if (not to sound like a broken
record) we want to execute this same update for any of the
employee tables for different locations? Time to go to NDS and use
the RETURNING BULK COLLECT clause:
CREATE
OR REPLACE PROCEDURE upd_employees (loc_in IN VARCHAR2,
employees_in IN employees_t)
IS
my_salaries
salaries_t;
BEGIN
FORALL
indx in employees_in.first.. employees_in.last
EXECUTE
IMMEDIATE
'UPDATE
'||
loc_in
||
'
employees SET salary = salary*1.1' || ' WHERE
employee_id = :the_id
RETURNING salary INTO :salaries'
USING employee_in (indx)
RETURNING BULK COLLECT INTO my_salaries;
END;
Handling and Reporting Exceptions
You can now trap errors that happen in your FORALL statement by
taking advantage of the SAVE EXCEPTIONS clause.
FORALL
indx IN words.first..words.last SAVE
EXCEPTIONS
INSERT INTO vocabulary(text)
VALUES ( words(indx) );
Use of SAVE EXCEPTIONS allows the FORALL to continue through all
the rows indicated by the collection; it "saves up" the exceptions
as it encounters them. This saving step begs the obvious question:
How can you, the developer, get information about the errors that
were "saved"? By taking advantage of the new SQL%BULK_COLLECTIONS
pseudo-collection, as demonstrated in the code shown in the
following example:
DECLARE
bulk_errors EXCEPTION;
PRAGMA EXCEPTION_INIT (bulk_errors, -24381);
BEGIN
FORALL indx IN words.FIRST .. words.LAST SAVE EXCEPTIONS
INSERT INTO t (text) VALUES (words (indx));
EXCEPTION
WHEN bulk_errors THEN
--For
each
error
found,
try
to identify the cause of that error
FOR j IN 1
.. SQL%BULK_EXCEPTIONS.COUNT
LOOP
Dbms_Output.Put_Line
('Iteration Number ' || sql%bulk_exceptions(j).error_index);
Dbms_Output.Put_Line ('Error ' ||
Sqlerrm(sql%bulk_exceptions(j).error_code*-1));
--Detecting Unique Constraint Violation
if
substr(Sqlerrm(SQL%BULK_EXCEPTIONS(J).ERROR_CODE * -1),1,9) =
'ORA-00001' then
v_RowsDuplicated := v_RowsDuplicated + 1;
else
Dbms_Output.Put_Line ('Other type of Error on Issuedata
Import');
end if;
END LOOP;
v_newerrors := SQL%BULK_EXCEPTIONS.COUNT;
v_errors := v_errors + v_newerrors;
dbms_output.put_line('Total Errors=
' || to_char(v_errors));
END;
Each row of this pseudo-collection is a record consisting of two
fields: ERROR_INDEX and ERROR_CODE. The ERROR_INDEX shows which
index in the original bulk-load collection causes the failure.
ERROR_CODE is the error number encountered.
You must both use the SAVE EXCEPTIONS construct and handle the
BULK_ERRORS exception to get the intended benefit (that is, that all
non-erroring rows are inserted).
Implementation
restriction
It is not allowed to use the FORALL statement and an UPDATE order
that use the SET ROW functionality
Declare
TYPE TAB_EMP is table of EMP%ROWTYPE ;
emp_tab TAB_EMP ;
Cursor CEMP
is Select * From EMP ;
Begin
Open CEMP;
Fetch CEMP BULK
COLLECT Into emp_tab ;
Close CEMP ;
Forall i in
emp_tab.first..emp_tab.last
Update EMP
set row = emp_tab(i) where EMPNO = emp_tab(i).EMPNO ; --
ILLEGAL
End ;
/
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 ;
Restrictions
on BULK COLLECT
The following restrictions apply to the BULK COLLECT clause:
- You cannot bulk collect into an associative array that has a
string type for the key.
- You can use the BULK COLLECT clause only in server-side programs
(not in client-side programs). Otherwise, you get the error this
feature is not supported in client-side programs.
- All targets in a BULK COLLECT INTO clause must be collections, as
the following example shows:
DECLARE
TYPE NameList IS TABLE OF emp.ename%TYPE;
names NameList;
salary emp.sal%TYPE;
BEGIN
SELECT ename, sal BULK COLLECT INTO names, salary
-- illegal target
FROM emp WHERE ROWNUM < 50;
...
END;
- Composite targets (such as objects) cannot be used in the
RETURNING INTO clause. Otherwise, you get the error unsupported
feature withRETURNING clause.
- When implicit datatype conversions are needed, multiple composite
targets cannot be used in the BULK COLLECT INTO clause.
- When an implicit datatype conversion is needed, a collection of a
composite target (such as a collection of objects) cannot be used in
the BULKCOLLECT INTO clause.
Another new feature is the capability of multi-dimensional arrays,
which Oracle has implemented as collections of collections.Technically,
all
collection
types
support
only
a single dimension, however by allowing a collection element to
become a collection, one has the effectively the same data
structure. The following code shows the
way to declare and reference a two-dimensional array of numbers.
DECLARE
TYPE
element IS TABLE OF
NUMBER INDEX BY BINARY_INTEGER;
TYPE twoDimensional IS TABLE OF element INDEX BY BINARY_INTEGER;
twoD twoDimensional;
BEGIN
twoD(1)(1) := 123;
twoD(1)(2) := 456;
END;
At first one would think that, while an interesting capability, it
has no potential impact on performance but it will be shown later in
this paper how the combination of this capability along with the use
of packaged variables can open up the door to dramatically speeding
up PL/SQL code.
Another
Example
To demonstrate
this, let's use the example of students in a college or
university.
In the real world
the information we would want to know about students would most
likely include :
- Addresses
(home/holiday, term-time), more than one
- Phone numbers
(home, term-time, mobile)
- Email addresses
(home, college, other)
- First and Last
Name
- etc
But let's keep
things simple for this example and restrict ourselves to just
the first and last name and 2 addresses simply to demonstrate
the principles of nesting collections.
This means that
we have 1 entity - student - with the following attributes :
first & last name and 2 addresses. In a database, entities
translate to a table, so we have our first data structure: a
table (or collection in PL/SQL terms).
The attributes
would normally translate to columns in the table but Oracle's
PL/SQL doesn't have the concept of columns so we have to use the
record structure.
Now we have a
problem though. Our student record needs to contain 2 addresses,
how do we handle this in PL/SQL ? We could just have two
different address attributes (a home address and an term-time
address) but we might need to add more addresses later so we'll
use a PLSQL collection for the addresses.
The next problem
is that each address has many lines, how do we handle that?
Again, no problem. We simply define an address as a collection
of address lines.
So let's take a
look at our data structures.
We have addresses
which are tables of address lines, but to declare this in PL/SQL
we need to create the address lines type first and then create
the address type, as follows:
TYPE address_lines IS TABLE OF VARCHAR2(50) INDEX
BY BINARY_INTEGER;
TYPE address_tab IS TABLE OF address_lines INDEX
BY VARCHAR2(10);
Note that we are
indexing address_lines by binary_integer and addresses by
varchar2 so that we can look up addresses by name (e.g. 'home'
or 'term-time') rather than by number.
Now we have
nested our collections (associative arrays in this case) - we
have a table of addresses and each address consists of a
collection of address lines. Note that we can't use any of these
data structures yet, as we've only defined the types, not
declared any variables of those types.
The next thing to
do is to define our record structure to hold the student
information as follows:
TYPE student IS
RECORD (
first_name
VARCHAR2(50),
last_name VARCHAR2(50),
addresses address_tab);
So far, so good,
but we only have the ability to store information about 1
student at the moment so we need to define another type - a
table (collection) of students:
TYPE students_tab IS TABLE OF student INDEX BY
BINARY_INTEGER;
Note that this
time we're indexing by binary integer, which could be the
numeric student id, as we're already storing the first and last
names.
The final data
structure declaration is that of the variable to store the data.
students students_tab;
Now we have
PL/SQL collections nested 3 deep - we have a collection of
students, each of which has a collection of addresses (home,
term-time, etc), each of which in turn has a collection of
address lines. We could extend the nesting by having say a
collection of colleges, each of which has a collection of
students and so on, but just because we can do things it doesn't
necessarily mean we should do them!
Let's put this all
together now and write some code to store and retrieve some
data.
DECLARE
TYPE
address_lines IS TABLE OF VARCHAR2(50) INDEX BY BINARY_INTEGER;
TYPE
address_tab IS TABLE OF address_lines INDEX BY VARCHAR2(10);
TYPE
student IS RECORD (
first_name
VARCHAR2(50),
last_name VARCHAR2(50),
addresses address_tab);
TYPE
students_tab IS TABLE OF student INDEX BY BINARY_INTEGER;
students
students_tab;
BEGIN
students(1).first_name := 'Mike';
students(1).last_name := 'Johnson';
students(1).addresses('HOME')(2) := 'Bristol';
students(1)Addresses('HOME')(1) := '1 The Avenue';
students(1)Addresses('HOME')(3) := 'BS99 3SJ';
students(2)First_name := 'Mike';
students(2)Last_name := 'Bliss';
students(2)Addresses('HOME')(2) := 'Leicester';
students(2)Addresses('HOME')(1) := '11 The
Street';
students(2)Addresses('HOME')(3) := 'LE64 50Q';
FOR student_no IN students.first..students.last
LOOP
FOR address_line_no IN
students(student_no)Addresses('HOME')First
..students(student_no)Addresses('HOME')Last LOOP
dbms_output.put_line('Student('||student_no||')
home
address line('||address_line_no||') '||
students(student_no)Addresses('HOME')(address_line_no));
END LOOP;
END LOOP;
END;
/
Student(1) home address line(1) 1 The Avenue
Student(1) home
address line(2) Bristol
Student(1) home
address line(3) BS99 3SJ
Student(2) home
address line(1) 1 The Street
Student(2) home
address line(2) Leicester
Student(2) home
address line(3) LE64 50Q
Note how access
to the innermost collection is achieved :
students(student_no)Addresses('HOME')(address_line_no)
The first
collection is a set of records, the part of the record we are
interested in, is in turn another collection each element of
which is another collection. The (address_line_no)clause gives
us the access to the base level collection but we can't access
it by name because it doesn't have one, the type declaration is
only a place holder in effect.
There are different approaches for returning results sets:
1- Returning a single row
2- Returning cursors using REF CURSOR
3- Returning collections
4- Return collections with dynamic SQL
5- Return using table functions.
1- Returning a Single Row
PL/SQL makes it very easy to query a row of data from a table and
then return that row as a record.
CREATE OR REPLACE FUNCTION
or_employee (employee_id_in IN employee.employee_id%TYPE)
RETURN employee%ROWTYPE
IS
CURSOR onerow_cur IS
SELECT employee_id, last_name, first_name
FROM employee
WHERE employee_id = employee_id_in;
retval
employee%ROWTYPE;
BEGIN
OPEN onerow_cur;
FETCH
onerow_cur INTO oretval;
CLOSE onerow_cur;
RETURN retval;
END or_employee;
/
The advantage of this approach is that it’s very simple code, easily
understood by all PL/SQL developers.
The downsides of this approach include:
• The calling environment needs to be able to work with PL/SQL
records, which aren’t universally recognized.
• This approach forces you to work with a single row at a time. If
the calling program needs to retrieve lots of data, this will be a
relatively slow alternative.
2- Returning
Cursor Variables using REF CURSOR
One of the best ways to isolate an application from SQL
dependencies is to write a package of PL/SQL functions that return
the REF CURSOR type to the calling programs written in other host
languages such as Java or C. Cursor
variables can be either weakly typed,
which are more flexible, or strongly typed, which provide greater
type safety. Of course, the application must
know the number and data types of the returned columns as well as
their semantics in order to use the data, but it can be totally
isolated from the way the data is stored.
The following function returns a weakly typed cursor using
the new 9i type SYS_REFCURSOR.
FUNCTION email_cur RETURN sys_refcursor IS
rc sys_refcursor;
BEGIN
OPEN rc FOR
SELECT *
FROM emp;
RETURN rc;
END;
An application can call the function and bind the returned open
cursor to a local cursor variable. The
application then iterates through the result set as if it had been
defined and opened locally.
The advantages of the cursor variable approach include the
following:
• The cursor variable can be passed back to a non-PL/SQL host
environment such as Java, which can then fetch individual rows
through the cursor variable, and close it when completed.
• Within PL/SQL, you can manipulate the cursor variable using
traditional and familiar syntax.
The downsides of this approach include:
• Cursor variables require row-by-row querying from the database, so
it’s relatively slow compared to BULK COLLECT.
• Relatively few PL/SQL developers are familiar with (and even fewer
comfortable with) the REF CURSORs and cursor variables. The learning
curve, however, is quite short.
3-
Returning Collections
Another approach to returning result sets is to write a function
that explicitly returns a PL/SQL collection type.
As a native PL/SQL datatype, a collection can also be passed as an
argument in a parameter list and returned by a function.
Also, since there are no predefined collection
types in PL/SQL, the returned type must either be declared in a
shared package header or be a SQL type declared globally in the
database.
Let’s take a look at a simple example. Suppose that I need to
write a function that returns a collection containing all the rows
in the employee table. I’ll first create a package specification
that contains two elements:
• The TYPE statement that defines the structure of the collection
• A function to return a collection of that type
CREATE OR REPLACE PACKAGE
many_employees
IS
TYPE employee_tc IS TABLE OF employee%ROWTYPE INDEX BY BINARY_INTEGER;
FUNCTION
allrows RETURN employee_tc;
END;
/
The package body contains the implementation of that function by
relying on the fast and concise BULK COLLECT query.
Note that with BULK COLLECT, a SELECT INTO will not raise
NO_DATA_FOUND if no rows are found. Instead, you must check to see
how many rows are defined in the collection to determine whether
the query returned any rows.
CREATE OR REPLACE PACKAGE
BODY many_employees
IS
FUNCTION allrows RETURN employee_tc
IS
l_employees employee_tc;
BEGIN
SELECT * BULK COLLECT INTO
l_employees
FROM employee;
RETURN l_employees;
END allrows;
END many_employees;
/
From within PL/SQL, I can call this function and then iterate
through the returned rows of the collection. In the example shown
here, I display the last name of each employee found in the
collection:
DECLARE
l_employees many_employees.employee_tc;
l_row PLS_INTEGER;
BEGIN
l_employees := many_employees.allrows;
l_row :=
l_employees.FIRST;
WHILE (l_row IS
NOT NULL)
LOOP
DBMS_OUTPUT.put_line
(l_employees(l_row).last_name);
l_row
:= l_employees.NEXT(l_row);
END LOOP;
END;
/
The advantages of the collection approach include:
• BULK COLLECT allows you to greatly improve the performance of
querying the data.
• The code is concise, highly readable, and easy to maintain.
The downsides of the collection approach include:
• The calling environment needs to be able to work with PL/SQL
collections, which aren’t universally recognized.
• Relatively few PL/SQL developers are familiar with (and even
fewer comfortable with) the collections data structure.
Collections are fabulously useful and very efficient.
• Collections consume PGA (program global area) memory. If you’re
building a multi-user application and working with large tables,
you’ll need to be conscious of the size of your collections. You
can apply the LIMIT clause to BULK COLLECT to manage the memory
consumed by your program.
4- Return
Collections with Dynamic SQL
Suppose, for example, that I need to write a function that returns
the result set from the OM_EMPLOYEE table as specified by a dynamic
WHERE clause.
I’ll add a new “all rows by” program to the many_employees package I
created before:
CREATE OR REPLACE PACKAGE
many_employees
IS
TYPE employee_tc
IS TABLE OF om_employee%ROWTYPE INDEX BY BINARY_INTEGER;
FUNCTION allrows
RETURN employee_tc;
-- Use OPEN FOR
to retrieve rows for
dynamic WHERE clause
FUNCTION
allrows_by (where_in IN VARCHAR2 DEFAULT NULL)
RETURN employee_tc;
END many_employees;
/
We could do row-by-row querying into the collection or bulk
processing. Let’s start with the “oldfashioned” row-by-row
processing. This version is shown here:
FUNCTION allrows_by (where_in
IN VARCHAR2 DEFAULT NULL)
RETURN
employee_tc
IS
TYPE weak_rc IS
REF CURSOR;
allrows_cur
weak_rc;
l_rows
PLS_INTEGER;
retval
employee_tc;
BEGIN
IF where_in IS
NULL THEN
RETURN allrows;
ELSE
OPEN allrows_cur FOR 'SELECT * FROM om_EMPLOYEE WHERE ' ||
where_in;
LOOP
FETCH
allrows_cur
INTO
retval
(retval.COUNT
+ 1);
EXIT WHEN allrows_cur%NOTFOUND;
END LOOP;
RETURN retval;
END IF;
END allrows_by;
Here’s an explanation of the code:
• I’ll use the OPEN FOR statement with the dynamic query, so I need
to declare a REF CURSOR (weak_rc) and associated cursor variable
(allrows_cur).
• If the optional WHERE clause is null, I’ll simply call the
original allrows function to return all the rows.
• If I have a WHERE clause, then I’ll use the OPEN FOR statement
with the dynamic query (this feature became available in Oracle8i;
previously, OPEN FOR could only be used with static SQL).
• I then use a simple loop to fetch each of the rows from the cursor
and deposit that data into my collection in sequentially filled
rows.
Now, having done and seen all of that code, I must also caution you
that whenever you face the prospect of querying data on a row-by-row
basis, you should consider switching to BULK COLLECT. This construct
generally will allow you to write less code (if you were already
using collections) and execute your code much more efficiently. In
this implementation, if my WHERE clause isn’t null, then I simply
construct the query, execute within the EXECUTE IMMEDIATE statement,
and then BULK COLLECT INTO my collection. With that single
statement, all the data in om_employee is transferred to the
collection (and the rows are populated sequentially, starting with
1).
FUNCTION allrows_by (where_in
IN VARCHAR2 DEFAULT NULL)
RETURN
employee_tc
IS
l_rows
PLS_INTEGER;
retval
employee_tc;
BEGIN
IF where_in IS
NULL THEN
RETURN allrows;
ELSE
EXECUTE IMMEDIATE 'SELECT * FROM om_EMPLOYEE ' ||'WHERE ' ||
where_in
BULK COLLECT INTO retval;
RETURN retval;
END IF;
END allrows_by;
5- Table Function
Most client programs however, don’t really want to deal with trying
to bind to a PL/SQL user defined type; instead, they want a cursor. The TABLE function provides a way to take
a function like the one above and return its results to the caller
directly as a cursor. Recall that the
TABLE function takes a variable of a globally defined collection
type as an argument, therefore a
function with a return type of the same collection type, like the
one above, can be used as an argument to the TABLE function as well. Without modifying the above procedure, a
program can return its output collection as a cursor using the
following syntax. Views can be wrapped
around this kind of SQL statement to make life easier for a calling
application.
SELECT * FROM TABLE( CAST( get_email_demo(45) AS
email_demo_nt_t ));
Pipelined
Table
Functions
While that approach works, it is really only appropriate for smaller
result sets of perhaps a few thousand rows. When
the function executes to populate the result set, the data is
buffered in the local variable of the procedure.
Only after the procedure has finished executing, will the
rows be returned to the calling application. Memory
to store the buffered data is dynamically allocated from the
operating system by the server process executing the procedure If the result set was very large, operating
system memory could become depleted.
Pipelined table functions are an Oracle 9i
facility that address this issue by providing a mechanism
to stream the values from the function back to the calling
application while the function is executing. A
small amount of data remains buffered in the function’s address
space so that result sets can be sent back in batches, which is
faster than row-by-row processing. This
is a far more scalable design for this functionality since the
operating system memory footprint is independent of the size of the
result set.
To utilize this feature, the function must be declared as PIPELINED
and collection objects must be returned one at a time via a new
function called PIPE ROW. The function
contains a RETURN statement without arguments that is used to
terminate the cursor. The function can
now be rewritten to take advantage of pipelining.
FUNCTION get_email_demo RETURN email_demo_nt_t PIPELINED IS
CURSOR email_demo_cur IS
SELECT email_demo_obj_t(email_id, demo_id, value)
FROM email_demographic;
eml_dmo_nt email_demo_nt_t;
BEGIN
OPEN email_demo_cur;
LOOP
FETCH email_demo_cur BULK COLLECT INTO eml_dmo_nt LIMIT 1000;
EXIT WHEN
email_demo_cur%NOTFOUND;
FOR
i IN 1..eml_dmo_nt.COUNT LOOP
/* Apply some business logic on the object here, and return
a row. */
PIPE ROW (eml_dmo_nt(i));
END
LOOP;
END LOOP;
RETURN;
END;
Note that while the return type of the function is still the
collection type, the local variable being assigned is the object
type. In this example, the fetch is
performed using the BULK COLLECTfeature. The documents illustrate the much slower
row-by-row fetch. Since the signature
of the procedure has not been changed, only the implementation, it
can be called the same way as the previous table function using the
TABLE and CAST functions.
Cursor Attributes
More information can be found HERE
Specific for BULK and FORALL:
- For FORALL, %FOUND and %NOTFOUND reflect the overall results,
not the results of an individual statement, including the last.
In other words, if any one of the statements executed in the
FORALL modified at least one row, %FOUND returns TRUE and
%NOTFOUND returns FALSE.
- For FORALL, %ISOPEN always returns FALSE because the cursor is
closed when the FORALL statement terminates.
- For FORALL, %ROWCOUNT returns the total number of rows
affected by all the FORALL statements executed, not simply the
last statement.
- For BULK COLLECT, %FOUND and %NOTFOUND always return NULL and
%ISOPEN returns FALSE because the BULK COLLECT has completed the
fetching and closed the cursor. %ROWCOUNT always returns NULL,
since this attribute is only relevant for DML statements.
- The nth row in this pseudo index-by table stores the number of
rows processed by the n th execution of the DML operation in the
FORALL statement. If no rows are processed, then the value in
%BULK_ROWCOUNT is set to 0.
The %BULK_ROWCOUNT attribute is a handy device, but it is also quite
limited. Keep the following in mind:
- Even though it looks like an index-by table, you cannot apply
any methods to it.
- %BULK_ROWCOUNT cannot be assigned to other collections. Also,
it cannot be passed as a parameter to subprograms.
- The only rows defined for this pseudo index-by table are the
same rows defined in the collection referenced in the FORALL
statement.
Oracle10g enhancements for
nested tables
The essential advance made in Oracle10g is that Oracle is now
treating nested tables more like the multisets that they are. This
means that it’s providing high-level set operations that can be
applied to nested tables (and only, for the time being, to nested
tables). More information HERE
FORALL
Support for Non-Consecutive Indexes (Sparse Collections)
Oracle 10g introduces support for the FORALL syntax with
non-consecutive indexes in collections.
The INDICES OF clause allows the FORALL syntax to be used with
sparse collections, while the VALUE OF clause is used for
collections of indexes pointing to other collections. The following
are examples of their usage.
DECLARE
TYPE t_tab1 IS TABLE OF
tab1%ROWTYPE;
TYPE t_tab2 IS TABLE OF
BINARY_INTEGER;
l_tab1 t_tab1 :=
t_tab1();
l_tab2 t_tab2 :=
t_tab2();
BEGIN
FOR i IN 1 .. 1000 LOOP
l_tab1.extend;
l_tab1(l_tab1.last).id := i;
IF MOD(i,
100) = 0 THEN
l_tab2.extend;
l_tab2(l_tab2.last) := i;
END IF;
END LOOP;
l_tab1.delete(301);
l_tab1.delete(601);
l_tab1.delete(901);
-- This would fail due
to sparse collection.
--FORALL i IN
l_tab.first .. l_tab.last
-- INSERT INTO
tab1 VALUES l_tab(i);
-- This works fine with
sparse collections.
FORALL i IN INDICES OF
l_tab1
INSERT INTO
tab1 VALUES l_tab1(i);
-- This works fine for
collections of indexes pointing to elements of another collection.
FORALL i IN VALUES OF
l_tab2
INSERT INTO
tab1 VALUES l_tab1(i);
END;
/
Nested
Table Enhancements
Nested tables in PL/SQL now support more operations than before.
Collections can be assigned directly to the value of another
collection of the same type, or to the result of a set expression.
SET SERVEROUTPUT ON
DECLARE
TYPE t_colors IS TABLE
OF VARCHAR2(10);
l_col_1 t_colors :=
t_colors('Red', 'Green', 'Blue', 'Green', 'Blue');
l_col_2 t_colors :=
t_colors('Red', 'Green', 'Yellow', 'Green');
l_col_3 t_colors;
PROCEDURE display
(p_text IN VARCHAR2,
p_col IN t_colors) IS
BEGIN
DBMS_OUTPUT.put_line(CHR(10) || p_text);
FOR i IN
p_col.first .. p_col.last LOOP
DBMS_OUTPUT.put_line(p_col(i));
END LOOP;
END;
BEGIN
-- Basic assignment.
l_col_3 := l_col_1;
display('Direct
Assignment:', l_col_3);
-- Expression
assignments.
l_col_3 := l_col_1
MULTISET UNION l_col_2;
display('MULTISET
UNION:', l_col_3);
l_col_3 := l_col_1
MULTISET UNION DISTINCT l_col_2;
display('MULTISET UNION
DISTINCT:', l_col_3);
l_col_3 := l_col_1
MULTISET INTERSECT l_col_2;
display('MULTISET
INTERSECT:', l_col_3);
l_col_3 := l_col_1
MULTISET INTERSECT DISTINCT l_col_2;
display('MULTISET
INTERSECT DISTINCT:', l_col_3);
l_col_3 := l_col_1
MULTISET EXCEPT l_col_2;
display('MULTISET
EXCEPT:', l_col_3);
l_col_3 := l_col_1
MULTISET EXCEPT DISTINCT l_col_2;
display('MULTISET
EXCEPT DISTINCT:', l_col_3);
END;
/
Direct Assignment:
Red
Green
Blue
Green
Blue
MULTISET UNION:
Red
Green
Blue
Green
Blue
Red
Green
Yellow
Green
MULTISET UNION DISTINCT:
Red
Green
Blue
Yellow
MULTISET INTERSECT:
Red
Green
Green
MULTISET INTERSECT DISTINCT:
Red
Green
MULTISET EXCEPT:
Blue
Blue
MULTISET EXCEPT DISTINCT:
Blue
Comparissions
between collections
Comparisons between collections have also improved with the addition
of NULL checks, equality operators and set operations.
SET SERVEROUTPUT ON
DECLARE
TYPE t_colors IS TABLE
OF VARCHAR2(10);
l_col_1 t_colors :=
t_colors('Red', 'Green', 'Blue');
l_col_2 t_colors :=
t_colors('Red', 'Green', 'Green');
l_col_3 t_colors;
BEGIN
IF (l_col_3 IS NULL)
AND (l_col_1 IS NOT NULL) THEN
DBMS_OUTPUT.put_line(CHR(10) || '(l_col_3 IS NULL) AND (l_col_1 IS
NOT NULL): TRUE');
END IF;
l_col_3 := l_col_1;
IF (l_col_3 = l_col_1)
AND (l_col_3 != l_col_2) THEN
DBMS_OUTPUT.put_line(CHR(10) || '(l_col_3 = l_col_1) AND (l_col_3
!= l_col_2): TRUE');
END IF;
IF (SET(l_col_2)
SUBMULTISET l_col_1) AND (l_col_1 NOT SUBMULTISET l_col_2) THEN
DBMS_OUTPUT.put_line(CHR(10) || '(SET(l_col_2) SUBMULTISET
l_col_1) AND (l_col_1 NOT SUBMULTISET l_col_2): TRUE');
END IF;
DBMS_OUTPUT.put_line(CHR(10) || 'CARDINALITY(l_col_2): ' ||
CARDINALITY(l_col_2));
DBMS_OUTPUT.put_line(CHR(10) || 'CARDINALITY(SET(l_col_2)): ' ||
CARDINALITY(SET(l_col_2)) || ' - Duplicates removed');
IF l_col_2 IS NOT A SET
THEN
DBMS_OUTPUT.put_line(CHR(10) || 'l_col_2 IS NOT A SET: TRUE -
Contains duplicates');
END IF;
IF l_col_3 IS NOT EMPTY
THEN
DBMS_OUTPUT.put_line(CHR(10) || 'l_col_3 IS NOT EMPTY: TRUE');
END IF;
END;
/
(l_col_3 IS NULL) AND (l_col_1
IS NOT NULL): TRUE
(l_col_3 = l_col_1) AND
(l_col_3 != l_col_2): TRUE
(SET(l_col_2) SUBMULTISET
l_col_1) AND (l_col_1 NOT SUBMULTISET l_col_2): TRUE
CARDINALITY(l_col_2): 3
CARDINALITY(SET(l_col_2)): 2 -
Duplicates removed
l_col_2 IS NOT A SET: TRUE -
Contains duplicates
l_col_3 IS NOT EMPTY: TRUE
Remove
Duplicates
The SET function removes duplicate entries from your nested
table, in a similar way to the SQL DISTINCT aggregate function.
SET SERVEROUTPUT ON
DECLARE
TYPE t_colors IS TABLE
OF VARCHAR2(10);
l_col_1 t_colors :=
t_colors('Red', 'Green', 'Blue', 'Green', 'Blue');
l_col_2 t_colors;
PROCEDURE display
(p_text IN VARCHAR2,
p_col IN t_colors) IS
BEGIN
DBMS_OUTPUT.put_line(CHR(10) || p_text);
FOR i IN
p_col.first .. p_col.last LOOP
DBMS_OUTPUT.put_line(p_col(i));
END LOOP;
END;
BEGIN
-- Basic assignment.
l_col_2 := l_col_1;
display('Direct
Assignment:', l_col_2);
-- SET assignments.
l_col_2 := SET(l_col_1);
display('MULTISET
UNION:', l_col_2);
END;
/
Direct Assignment:
Red
Green
Blue
Green
Blue
MULTISET UNION:
Red
Green
Blue
Support
for in and not in Operators
In Oracle 10g, it is possible to use in and not in operators to
determine if a given nested table appears in another nested table.
The in and not in operators return a Boolean value depending
on the result of the execution of the operator.
Below is an example of using this functionality. This example
re-creates the T_EMP_INFO table, adding another column.
It then creates two records to test with. Finally, it issues two
queries. In the first query, using the in operator, one record
matches the query, because it is looking for a promotion date that
is the same as either the REVIEW_DATE date or the
SALARY_LAST_INCREASE date, which is the case with EMP_ID 2. The
second query, using the not in operator, matches the record for
EMP_ID 1, because the not in query predicate eliminates the record
that has different dates within it.
-- First, drop and recreate
the t_emp_info table from the earlier example.
DROP TABLE t_emp_info;
CREATE TABLE t_emp_info
(
emp_id
NUMBER PRIMARY KEY,
review_date
TY_REVIEW,
salary_last_increase TY_REVIEW,
promotion_date
TY_REVIEW,
hire_date
DATE,
last_update_date DATE )
NESTED TABLE review_date STORE
AS emp_tab
NESTED TABLE
salary_last_increase STORE AS sup_tab
NESTED TABLE promotion_date
STORE AS promo_tab;
-- Insert 2 records.
INSERT INTO t_emp_info values
(1,
TY_REVIEW
(T_REVIEW_DATES(SYSDATE-365),
T_REVIEW_DATES(SYSDATE-300),
T_REVIEW_DATES(SYSDATE-270),
T_REVIEW_DATES(SYSDATE-200) ),
TY_REVIEW
(T_REVIEW_DATES(SYSDATE-365),
T_REVIEW_DATES(SYSDATE-300) ),
TY_REVIEW
(T_REVIEW_DATES(SYSDATE)),
SYSDATE, SYSDATE);
INSERT INTO t_emp_info VALUES
(2,
TY_REVIEW
(T_REVIEW_DATES(SYSDATE), T_REVIEW_DATES(SYSDATE-30),
T_REVIEW_DATES(SYSDATE-60),
T_REVIEW_DATES(SYSDATE-90) ),
TY_REVIEW
(T_REVIEW_DATES(SYSDATE), T_REVIEW_DATES(SYSDATE-30),
T_REVIEW_DATES(SYSDATE-60),
T_REVIEW_DATES(SYSDATE-90) ),
TY_REVIEW
(T_REVIEW_DATES(SYSDATE), T_REVIEW_DATES(SYSDATE-30),
T_REVIEW_DATES(SYSDATE-60),
T_REVIEW_DATES(SYSDATE-90) ),
SYSDATE, SYSDATE);
-- Now, query using the in
operator
COLUMN review_date FORMAT A20
COLUMN salary_last_increase
FORMAT A20
SELECT emp_id FROM t_emp_info
WHERE promotion_date
IN (review_date, salary_last_increase);
EMP_ID
----------
2
SELECT emp_id FROM
t_emp_info WHERE
promotion_date NOT IN (review_date, salary_last_increase);
EMP_ID
----------
1
Creating
Temporary
Tables with VARRAY Columns
In Oracle 10g, it is possible to include VARRAY columns in a
temporary table. Here is an example of such an operation:
Create type ty_cust_info as
object
( first_name
varchar2(30), last_name varchar2(30),
address varchar2(30),
city varchar2(30),
state varchar2(2),
zip varchar2(5) )
/
create type nt_cust_info as
varray(10) of ty_cust_info;
/
create global temporary table
t_cust_info
(
customer_information nt_cust_info,
created_date
date,
last_update_date date );
Note that nested tables are still not supported in temporary tables.
Changing
the
VARRAY Size
Oracle 10g introduces the ability to alter the size of a VARRAY
element, as shown in this example:
create or replace type test as
object (test_id number);
/
create or replace type ty_test
as varray(20) of test;
/
create table t_test_info
(
test_id_no
ty_test,
created_date
date,
last_update_date date );
insert into t_test_info values
(ty_test(test(1)), sysdate, sysdate );
Now, to modify the VARRAY so that it will be able to hold more
elements, simply issue an alter type command:
alter type ty_test modify
limit 1000 cascade;
There are two options available when altering types (neither of
which is a default value):
- invalidate - Invalidates all dependent objects when the
operation takes place and does so without any checks. This can
be dangerous, as it is possible to inadvertently drop an
attribute that is critical, so use invalidate carefully.
- cascade - Propagates the change to all dependent types and
tables, and an error will occur if any errors are found.
Finally, the size of a VARRAY can only be increased. Attempting to
make it smaller will result in an error.
Defining
the
Tablespace of a Nested Table
In Oracle 10g it is possible to define a different tablespace for
the storage table of a nested table.
By default, the storage table will be created where the parent table
has been created. The tablespace parameter can be used in the store
as clause to define a different tablespace, as shown below:
create or replace type test as
object (test_id number);
/
create or replace type ty_test
as table of test;
/
create table t_test_info
(
test_id_no
ty_test,
created_date
date,
last_update_date date )
nested table test_id_no store
as test_id_tab
(tablespace users);
It is also possible to alter the existing tablespace of the table of
a nested table collection with the alter table command, as shown in
this example:
Alter table test_id_no move
tablespace new_users;
ANSI Support for Nested Tables and VARRAY Columns
Oracle 10g introduces ANSI-related functionality (equivalent to the
array and multiset ANSI functionality) to nested tables and VARRAY
columns. This includes:
• Support for equality and inequality predicates
• Support for in and not in operators
• New operators
Support for Equality and Inequality Predicates
Nested tables in Oracle 10g support the use of equality and
inequality predicates via the use of the equal and not equal
operators. The result of the comparison is a Boolean value that
indicates the success or failure of the comparison. Here is an
example:
CREATE OR REPLACE TYPE
t_review_dates AS OBJECT
(review_date DATE,
MAP MEMBER FUNCTION
convert RETURN DATE);
/
CREATE OR REPLACE TYPE BODY
t_review_dates AS
MAP MEMBER FUNCTION convert
RETURN DATE IS
BEGIN
RETURN review_date;
END;
END;
/
CREATE OR REPLACE TYPE
ty_review AS TABLE OF t_review_dates;
/
CREATE TABLE t_emp_info
(
emp_id
NUMBER PRIMARY KEY,
review_date
TY_REVIEW,
salary_last_increase TY_REVIEW,
hire_date
DATE,
last_update_date DATE )
NESTED TABLE review_date STORE
AS emp_tab
NESTED TABLE
salary_last_increase STORE AS sup_tab;
INSERT INTO t_emp_info VALUES
(1,
TY_REVIEW
(T_REVIEW_DATES(SYSDATE), T_REVIEW_DATES(SYSDATE-30),
T_REVIEW_DATES(SYSDATE-60),
T_REVIEW_DATES(SYSDATE-90) ),
TY_REVIEW
(T_REVIEW_DATES(SYSDATE-30), T_REVIEW_DATES(SYSDATE-90) ),
SYSDATE, SYSDATE);
INSERT INTO t_emp_info VALUES
(2,
TY_REVIEW
(T_REVIEW_DATES(SYSDATE), T_REVIEW_DATES(SYSDATE-30),
T_REVIEW_DATES(SYSDATE-60),
T_REVIEW_DATES(SYSDATE-90) ),
TY_REVIEW
(T_REVIEW_DATES(SYSDATE), T_REVIEW_DATES(SYSDATE-30),
T_REVIEW_DATES(SYSDATE-60),
T_REVIEW_DATES(SYSDATE-90) ),
SYSDATE, SYSDATE);
Note that it was necessary to create a map method for the object,
which is required. After the objects have been created and the data
has been inserted, it is possible to query the collection items in
the table, as shown below:
COLUMN review_date FORMAT a20
COLUMN salary_last_increase
FORMAT a20
SQL> SELECT emp_id FROM
t_emp_info WHERE review_date=salary_last_increase;
EMP_ID
----------
2
Improvements in
Oracle 11g
PLS-00436
Restriction
in FORALL Statements Removed
The PLS-00436 restriction has been removed, which means you can now
reference the individual elements of a collection within the SET and
WHERE clauses of a DML statement in a FORALL construct. To see this
in action, create and populates a test table using the following
code.
CREATE TABLE forall_test (
id NUMBER,
amount NUMBER,
description
VARCHAR2(50)
);
INSERT INTO forall_test VALUES
(1, 10, 'ONE');
INSERT INTO forall_test VALUES
(2, 20, 'TWO');
INSERT INTO forall_test VALUES
(3, 30, 'THREE');
INSERT INTO forall_test VALUES
(4, 40, 'FOUR');
INSERT INTO forall_test VALUES
(5, 50, 'FIVE');
COMMIT;
The PL/SQL block below populates a collection with the existing
data, amends the data in the collection, then updates the table with
the amended data.
The final query displays the changed data in the table.
DECLARE
TYPE t_forall_test_tab
IS TABLE OF forall_test%ROWTYPE;
l_tab
t_forall_test_tab;
BEGIN
-- Retrieve the
existing data into a collection.
SELECT * BULK COLLECT INTO l_tab
FROM
forall_test;
-- Alter the data in
the collection.
FOR i IN l_tab.first ..
l_tab.last LOOP
l_tab(i).description := 'Description for ' || i;
END LOOP;
-- Update the table
using the collection.
FORALL i IN l_tab.first
.. l_tab.last
UPDATE
forall_test SET
description = l_tab(i).description,
amount
=
l_tab(i).amount * 1.10
WHERE id = l_tab(i).id;
COMMIT;
END;
/
SELECT * FROM forall_test;
ID
AMOUNT
DESCRIPTION
---------- ---------- -------------------
1 11 Description
for 1
2 22 Description
for 2
3 33 Description
for 3
4 44 Description
for 4
5 55 Description
for 5
In versions prior to 11g, the lines with the UPDATE would raise
PLS-00436 because they reference attributes within the forall_test
record variable.
In Oracle Database 10g and below, you would have had to define
separate collection variables for the columns and in the process may
have had to update several times, based on your exact logic.
In Oracle 11g, this is not only makes the program more readable and
maintainable; but performant as well, since you may not need to
issue multiple UPDATE statements.
Notice both the SET and WHERE clauses contain references to
individual columns in the collection. This makes using bulk-binds
for DML even easier as we no longer need to maintain multiple
collections if we need to reference columns in the WHERE clause. It
can also improve performance of updates, as previous versions
required updates of the whole row using the ROW keyword, which
included potentially unnecessary updates of primary key and foreign
key columns.
Improvements in
Oracle 12c
Setup
The examples in this article follow the same pattern. They populate
a collection, then instantly select from the collection using the
TABLE operator, and print out the contents. The code is merely to
demonstrate the fact the local collection is accessible using the
TABLE operator.
CREATE TABLE DEPT (
DEPTNO NUMBER(2) CONSTRAINT PK_DEPT PRIMARY KEY,
DNAME VARCHAR2(14),
LOC VARCHAR2(13)
) ;
CREATE TABLE EMP (
EMPNO NUMBER(4) CONSTRAINT PK_EMP PRIMARY KEY,
ENAME VARCHAR2(10),
JOB VARCHAR2(9),
MGR NUMBER(4),
HIREDATE DATE,
SAL NUMBER(7,2),
COMM NUMBER(7,2),
DEPTNO NUMBER(2) CONSTRAINT FK_DEPTNO REFERENCES
DEPT
);
INSERT INTO DEPT VALUES (10,'ACCOUNTING','NEW YORK');
INSERT INTO DEPT VALUES (20,'RESEARCH','DALLAS');
INSERT INTO DEPT VALUES (30,'SALES','CHICAGO');
INSERT INTO DEPT VALUES (40,'OPERATIONS','BOSTON');
INSERT INTO EMP VALUES
(7369,'SMITH','CLERK',7902,to_date('17-12-1980','dd-mm-yyyy'),800,NULL,20);
INSERT INTO EMP VALUES
(7499,'ALLEN','SALESMAN',7698,to_date('20-2-1981','dd-mm-yyyy'),1600,300,30);
INSERT INTO EMP VALUES
(7521,'WARD','SALESMAN',7698,to_date('22-2-1981','dd-mm-yyyy'),1250,500,30);
INSERT INTO EMP VALUES
(7566,'JONES','MANAGER',7839,to_date('2-4-1981','dd-mm-yyyy'),2975,NULL,20);
INSERT INTO EMP VALUES
(7654,'MARTIN','SALESMAN',7698,to_date('28-9-1981','dd-mm-yyyy'),1250,1400,30);
INSERT INTO EMP VALUES
(7698,'BLAKE','MANAGER',7839,to_date('1-5-1981','dd-mm-yyyy'),2850,NULL,30);
INSERT INTO EMP VALUES
(7782,'CLARK','MANAGER',7839,to_date('9-6-1981','dd-mm-yyyy'),2450,NULL,10);
INSERT INTO EMP VALUES
(7788,'SCOTT','ANALYST',7566,to_date('13-JUL-87','dd-mm-rr')-85,3000,NULL,20);
INSERT INTO EMP VALUES
(7839,'KING','PRESIDENT',NULL,to_date('17-11-1981','dd-mm-yyyy'),5000,NULL,10);
INSERT INTO EMP VALUES
(7844,'TURNER','SALESMAN',7698,to_date('8-9-1981','dd-mm-yyyy'),1500,0,30);
INSERT INTO EMP VALUES
(7876,'ADAMS','CLERK',7788,to_date('13-JUL-87',
'dd-mm-rr')-51,1100,NULL,20);
INSERT INTO EMP VALUES
(7900,'JAMES','CLERK',7698,to_date('3-12-1981','dd-mm-yyyy'),950,NULL,30);
INSERT INTO EMP VALUES
(7902,'FORD','ANALYST',7566,to_date('3-12-1981','dd-mm-yyyy'),3000,NULL,20);
INSERT INTO EMP VALUES
(7934,'MILLER','CLERK',7782,to_date('23-1-1982','dd-mm-yyyy'),1300,NULL,10);
COMMIT;
Associative Array
Example
This example uses an
associative array based on a %ROWTYPE defined
in the package specification.
CREATE OR REPLACE PACKAGE test_api AS
TYPE t_tab IS TABLE OF emp%ROWTYPE
INDEX BY BINARY_INTEGER;
PROCEDURE test1;
END;
/
CREATE OR REPLACE PACKAGE BODY test_api AS
PROCEDURE test1 IS
l_tab1 t_tab;
BEGIN
SELECT *
BULK COLLECT INTO l_tab1
FROM emp
WHERE deptno = 10;
DBMS_OUTPUT.put_line('Loop Through Collection');
FOR cur_rec IN (SELECT *
FROM TABLE(l_tab1))
LOOP
DBMS_OUTPUT.put_line(cur_rec.empno || ' : ' || cur_rec.ename);
END LOOP;
END;
END;
/
SET SERVEROUTPUT ON
EXEC test_api.test1;
Loop Through Collection
7782 : CLARK
7839 : KING
7934 : MILLER
PL/SQL procedure successfully completed.
SQL>
Nested Table Example
This example uses a
nested table based on a %ROWTYPE defined in the
package specification.
CREATE OR REPLACE PACKAGE test_api AS
TYPE t_tab IS TABLE OF emp%ROWTYPE;
PROCEDURE test1;
END;
/
CREATE OR REPLACE PACKAGE BODY test_api AS
PROCEDURE test1 IS
l_tab1 t_tab;
BEGIN
SELECT *
BULK COLLECT INTO l_tab1
FROM emp
WHERE deptno = 10;
DBMS_OUTPUT.put_line('Loop Through Collection');
FOR cur_rec IN (SELECT *
FROM TABLE(l_tab1))
LOOP
DBMS_OUTPUT.put_line(cur_rec.empno || ' : ' || cur_rec.ename);
END LOOP;
END;
END;
/
SET SERVEROUTPUT ON
EXEC test_api.test1;
Loop Through Collection
7782 : CLARK
7839 : KING
7934 : MILLER
PL/SQL procedure successfully completed.
SQL>
Varray Example
This example uses a
varray based on a %ROWTYPE defined in the
package specification.
CREATE OR REPLACE PACKAGE test_api AS
TYPE t_tab IS VARRAY(10) OF emp%ROWTYPE;
PROCEDURE test1;
END;
/
CREATE OR REPLACE PACKAGE BODY test_api AS
PROCEDURE test1 IS
l_tab1 t_tab;
BEGIN
SELECT *
BULK COLLECT INTO l_tab1
FROM emp
WHERE deptno = 10;
DBMS_OUTPUT.put_line('Loop Through Collection');
FOR cur_rec IN (SELECT *
FROM TABLE(l_tab1))
LOOP
DBMS_OUTPUT.put_line(cur_rec.empno || ' : ' || cur_rec.ename);
END LOOP;
END;
END;
/
SET SERVEROUTPUT ON
EXEC test_api.test1;
Loop Through Collection
7782 : CLARK
7839 : KING
7934 : MILLER
PL/SQL procedure successfully completed.
SQL>
Supported Types
The previous examples
all use a %ROWTYPE of
a schema table for the collection type. It is also possible to use %TYPE, a scalar
and a local record type.
The following example
uses an associated array based on a %TYPE. Notice the
column is referenced as COLUMN_VALUE.
CREATE OR REPLACE PACKAGE test_api AS
TYPE t_tab IS TABLE OF emp.empno%TYPE
INDEX BY BINARY_INTEGER;
PROCEDURE test1;
END;
/
CREATE OR REPLACE PACKAGE BODY test_api AS
PROCEDURE test1 IS
l_tab1 t_tab;
BEGIN
SELECT empno
BULK COLLECT INTO l_tab1
FROM emp
WHERE deptno = 10;
DBMS_OUTPUT.put_line('Loop Through Collection');
FOR cur_rec IN (SELECT *
FROM TABLE(l_tab1))
LOOP
DBMS_OUTPUT.put_line(cur_rec.column_value);
END LOOP;
END;
END;
/
SET SERVEROUTPUT ON
EXEC test_api.test1;
Loop Through Collection
7782
7839
7934
PL/SQL procedure successfully completed.
SQL>
The following example
uses an associated array based on a scalar. Notice the column is
referenced as COLUMN_VALUE.
CREATE OR REPLACE PACKAGE test_api AS
TYPE t_tab IS TABLE OF NUMBER
INDEX BY BINARY_INTEGER;
PROCEDURE test1;
END;
/
CREATE OR REPLACE PACKAGE BODY test_api AS
PROCEDURE test1 IS
l_tab1 t_tab;
BEGIN
SELECT empno
BULK COLLECT INTO l_tab1
FROM emp
WHERE deptno = 10;
DBMS_OUTPUT.put_line('Loop Through Collection');
FOR cur_rec IN (SELECT *
FROM TABLE(l_tab1))
LOOP
DBMS_OUTPUT.put_line(cur_rec.column_value);
END LOOP;
END;
END;
/
SET SERVEROUTPUT ON
EXEC test_api.test1;
Loop Through Collection
7782
7839
7934
PL/SQL procedure successfully completed.
SQL>
The following example
uses an associated array based on a local record type.
CREATE OR REPLACE PACKAGE test_api AS
TYPE t_row IS RECORD (
empno NUMBER(4),
ename VARCHAR2(10)
);
TYPE t_tab IS TABLE OF t_row
INDEX BY BINARY_INTEGER;
PROCEDURE test1;
END;
/
CREATE OR REPLACE PACKAGE BODY test_api AS
PROCEDURE test1 IS
l_tab1 t_tab;
BEGIN
SELECT empno, ename
BULK COLLECT INTO l_tab1
FROM emp
WHERE deptno = 10;
DBMS_OUTPUT.put_line('Loop Through Collection');
FOR cur_rec IN (SELECT *
FROM TABLE(l_tab1))
LOOP
DBMS_OUTPUT.put_line(cur_rec.empno || ' : ' || cur_rec.ename);
END LOOP;
END;
END;
/
SET SERVEROUTPUT ON
EXEC test_api.test1;
Loop Through Collection
7782 : CLARK
7839 : KING
7934 : MILLER
PL/SQL procedure successfully completed.
SQL>
Location of Type
Definition
For this new
functionality to work, the type must be defined in package
specification. The examples below show how alternate locations
fail.
The following example
moves the type definition to the package body, which results in a
compilation failure.
CREATE OR REPLACE PACKAGE test_api AS
PROCEDURE test1;
END;
/
CREATE OR REPLACE PACKAGE BODY test_api AS
TYPE t_tab IS TABLE OF emp%ROWTYPE
INDEX BY BINARY_INTEGER;
PROCEDURE test1 IS
l_tab1 t_tab;
BEGIN
SELECT *
BULK COLLECT INTO l_tab1
FROM emp
WHERE deptno = 10;
DBMS_OUTPUT.put_line('Loop Through Collection');
FOR cur_rec IN (SELECT *
FROM TABLE(l_tab1))
LOOP
DBMS_OUTPUT.put_line(cur_rec.empno || ' : ' || cur_rec.ename);
END LOOP;
END;
END;
/
Warning: Package Body created with compilation errors.
SQL> show errors
Errors for PACKAGE BODY TEST_API:
LINE/COL ERROR
-------- -----------------------------------------------------------------
15/21 PL/SQL: SQL Statement ignored
16/28 PL/SQL: ORA-22905: cannot access rows from a non-nested table
item
16/34 PLS-00382: expression is of wrong type
18/7 PL/SQL: Statement ignored
18/28 PLS-00364: loop index variable 'CUR_REC' use is invalid