Collections and
Bulk Binds
Introduction to Object Types and Records
Collections
Using Collection Methods (Count,
First, Last, etc)
Moving from
Cursor-Loops to
Collections
Bulk Binding
Handling and Reporting Exceptions
Multi-Dimensional Arrays
When to use What
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
Introduction to Object Types and Records
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 TYPE food_t 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
food_t := food_t ('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';
IF INSTR (my_favorite_vegetable_rec.grown_in,
'yard') > 0 THEN
--Pass an object
as a parameter
order_seeds
(my_favorite_vegetable_rec);
END IF;
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 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;
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 Example:
DECLARE
-- Declare a basic table type type
TYPE a_char_data IS TABLE OF VARCHAR2(10) INDEX BY BINARY_INTEGER;
-- Declare a complex record type
TYPE r_data IS RECORD (
ssn VARCHAR2(9) NOT NULL := -1,
name a_char_data, -- Notice the table_type used here
dob DATE );
-- Declare a index by table using the complex record type
TYPE a_multi IS TABLE OF r_data INDEX BY BINARY_INTEGER;
-- Declare a variable using the complex array
v_data a_multi;
BEGIN
-- Set some values
v_data(1).ssn := '123456789';
v_data(1).dob := '01-JAN-1900';
-- Notice the second subscript
v_data(1).name(1) := 'Lewis';
v_data(1).name(2) := 'Joe';
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;
/
Let me walk you through exactly what this example is doing:
- In the declare section:
- First we declare our very basic array of VARCHAR2(10).
- Next we declare a record type with an embedded index by
table.
- So now we have a ragged record type. A single record with a
dimensional name column.
- Then we declare a new table based on the complex record type.
This creates an array of ragged records.
- And finally, we create the complex variable.
- Executeable section:
- We populated the ssn and dob columns of the first record of
the v_data variable.
- Next we populated the first and second rows of the name table
in the first row of the v_data variable.
- Finally, we displayed 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.
There are three flavors of collection types, Associative Arrays, Nested
Tables
and Varrays (or Varrying Arrays).
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.
1-Associative Arrays (ALSO CALLED
PL/SQL Tables (oRACLE 7) OR INDEX_BY_TABLES (ORACLE 8)
TYPE type_name IS TABLE OF element_type [NOT NULL]
INDEX BY [BINARY_INTEGER | PLS_INTEGER | VARCHAR2(size_limit)];
INDEX BY
key_type;
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_array;
TYPE emp_array
IS TABLE OF emp%ROWTYPE INDEX BY
PLS_INTEGER;
my_emp emp_array;
BEGIN
FOR i
IN 1..100 LOOP
my_num_array(i) := power(2, i);
END LOOP;
FOR rec
IN (select * from emp)
LOOP
my_emp(rec.empno) := rec;
END LOOP;
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).
In previous versions of Oracle, the only wat to declare an associative
arrays was using the "index by binary_integer", that meant that the
only index allowed on an associative array was the row number. These
restrictions have now been lifted. You can now declare
associative arrays to be indexed by BINARY_INTEGER, PLS_INTEGER,
VARCHAR2 (like the second loop before) and even anchored declarations
of those types using %TYPE. You can even use a
user-defined subtype, thus:
DECLARE
SUBTYPE my_integer
IS PLS_INTEGER NOT NULL;
TYPE array_t4 IS
TABLE OF NUMBER
INDEX BY my_integer;
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.
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 assigning 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. For many PL/SQL programs, these
two added requirements make associative arrays a better choice
for
basic array functionality in code, however that is not always the case.
DECLARE
TYPE nest_tab_t
IS TABLE OF
NUMBER;
--initialization of this type
my_nt nest_tab_t := nest_tab_t();
TYPE emp_ntt
IS TABLE OF
emp%ROWTYPE;
my_emp emp_ntt := emp_ntt();
BEGIN
FOR i
IN 1..100 LOOP
nt.EXTEND;
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.
New functions
(Multi-set operations) on 10g for Nested Tables
3-Varrays
TYPE type_name IS {VARRAY | VARYING
ARRAY} (size_limit)
OF element_type [NOT
NULL];
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. It should be noted that both
varray types as well as nested table types can define the column type
of a SQL
table. In the former case, if the size
of the varray type is 4000 bytes or less,
it can be
stored in-line in the data block along with other column values. In contrast,
the column data for a nested table is
stored in a system managed child table making it very similar to a
normal
parent/child table relationship. Because
they have a shared type, 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. Example:
declare
type v is varray(50) of number;
Examples
for nested tables and varrays
set
serveroutput on
declare
type nestab is table of
number;
type varr is varray(50) of
varchar2(30);
someNumbers nestab;
someNames varr;
i
binary_integer;
begin
someNumbers :=
tn(10,4,6,9,2,5);
someNames :=
v('Fred','Joe','Caesar');
i:=3;
if someNumbers(i) = 6 then
dbms_output.put_line ('someNumbers(' || i || ') = 6');
else
dbms_output.put_line ('someNumbers(' || i || ') <> 6');
end if;
someNumbers(i) := 7;
if someNumbers(i) = 6 then
dbms_output.put_line ('someNumbers(' || i || ') = 6');
else
dbms_output.put_line ('someNumbers(' || i || ') <> 6');
end if;
someNumbers.delete(1); --delete element 1
someNumbers.delete(4); --delete element 4
--More
Ways to delete -- If an element doesn't exist no
exception rais
--
someNumbers.delete(20,30); --delete elements 20 through 30
--someNumbers.delete;
--delete entire PL/SQL Table
i := someNumbers.first();
while i is not null loop
dbms_output.put_line (i || ': ' || someNumbers(i));
i :=
someNumbers.next(i);
end loop;
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;
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
applied to atomically null collections. 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
Returns the maximum number of elements that a varray can contain.
Return NULL for Nested tables and Associative Arrays
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
Returns the first or last subscript of a collection. If the collection
is empty, FIRST and LAST returns NULL
Declare
TYPE TYP_TAB IS TABLE OF PLS_INTEGER INDEX BY
VARCHAR2(1);
my_tab TYP_TAB;
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 ) ;
End ;
/
First= A Last= E
PRIOR(index) and NEXT(index)
Returns the previous or next subscript of the index element. If the
index element has no predecessor, PRIOR(index) returns NULL. Likewise,
if index has no successor, NEXT(index) returns NULL.
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
EXTEND[(n[,i])]
Used to extend a collection (add new elements)
· 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.
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.
Collection exceptions
DECLARE
TYPE NumList IS
TABLE OF NUMBER;
nums NumList;
-- atomically null
BEGIN
/* Assume execution
continues despite the raised exceptions. */
nums(1) :=
1; --
raises COLLECTION_IS_NULL (1)
nums :=
NumList(1,2); -- initialize table
nums(NULL) :=
3 -- raises
VALUE_ERROR
(2)
nums(0) :=
3; --
raises SUBSCRIPT_OUTSIDE_LIMIT (3)
nums(3) :=
3; --
raises SUBSCRIPT_BEYOND_COUNT (4)
nums.DELETE(1);
-- delete element 1
IF nums(1) = 1 THEN
... -- raises
NO_DATA_FOUND
(5)
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. Oracle
makes
the distinction between values that are passed back via a RETURNING
clause in
SQL as opposed to when values are passed back by during a fetch
operation but
for the purpose of this paper I will refer to both of these operations
as
out-binds.
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 returning 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;
emp_util.show_all
(v_emprecs);
END;
Bulk
Binfing 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).
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 now 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.
Package
Variables
PL/SQL packages offer a number of advantages over stand-alone
code or anonymous blocks. While some
of
the advantages come from the familiar ability to organize code into
logical
collections of related procedures and functions, an often ignored
aspect is the
use of package-level variables and package initialization sections.
A package variable is essentially a variable that is declared
globally, typically at the top of the package body outside of any
procedure
definition. Once set, a package variable
will maintain its state for the life of the session, as opposed to
variables
local to procedure definitions that only exist for the duration of the
procedure call. Every package body
implementation can optionally include a block of code at the end of the
specification referred to as the initialization section.
This code will run only once – when the
package is first referenced and is normally used to initialize any
package
variables that are used.
The use of package variables is a powerful technique to speed up
SQL statements that has been used for many years. Consider
a procedure that is called
repeatedly in a busy OLTP database that inserts a row into a table. One of the values passed to the procedure is
a key that is used to first lookup another value, which in turn, is
used in the
insert. Most PL/SQL code will first
execute a select statement for the row of interest, binding the value
to a
local variable that will then be used on the subsequent insert
statement. A unique indexed table look-up
is relatively
quick but if an application is being driven hard, the cost can be more
than one
would expect. Referencing a
pre-initialized array value is approximately 20 times faster than an
index
based table look-up, making this technique a real time saver for
intensive operations.
Using package variables, the lookup can be avoided entirely by
first initializing an array with the desired values.
For example, in a marketing application a
procedure is passed a zip code as an argument and must perform an
insert into a
fact table that requires the city name as a denormalized
value in the table. Assume also that
there is a lookup table, with a numeric zip code as the primary key as
well as
the city and state information that the zip code maps to.
Code to avoid all of the look-ups would look
like this.
CREATE OR REPLACE PACKAGE
BODY direct_mkt
AS
TYPE zip_array
IS TABLE OF VARCHAR2(30) INDEX BY BINARY_INTEGER;
zip_deref zip_array;
PROCEDURE do_insert(f_name VARCHAR2, l_name VARCHAR2,
zip NUMBER)IS
BEGIN
INSERT INTO user_data (f_nm, l_nm, city_nm)
VALUES (f_name, l_name, zip_deref(zip));
COMMIT;
END;
--
Package
initialization section.
BEGIN
FOR rec
IN (SELECT zip_code, city FROM dma)
LOOP
zip_deref(rec.zip_code)
:= rec.city;
END LOOP;
END;
Until Oracle 9i Release 2, this technique couldn’t be used
if the look-up key was non-numeric or composite, but now with the
combination
of VARCHAR2 associative arrays and multi-dimensional arrays, it can be
extended
to almost any look-up table of a reasonable size. For
example consider a table of individuals
mapped to email addresses that has as it’s
primary
key, a composite index of numeric user id and an email address that
they are
associated with. The following code
shows how to implement this.
CREATE OR REPLACE PACKAGE
BODY email_push AS
TYPE email_array IS TABLE OF VARCHAR2(30)
INDEX BY VARCHAR2(30);
TYPE usermail_array
IS TABLE OF email_array INDEX BY
BINARY_INTEGER;
user_emails usermail_array;
FUNCTION lookup(p_userid NUMBER, p_email VARCHAR2) RETURN VARCHAR2
IS
BEGIN
RETURN user_emails(p_userid)(p_email);
END;
BEGIN
FOR rec
IN (SELECT user_id, email, l_name
FROM user_emails) LOOP
user_emails(rec.user_id)(rec.email) := rec.l_name;
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
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
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.
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 any individual statement, including the last (this
contradicts Oracle documentation). 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.
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