You are on page 1of 16

Oracle

Solutions for High-End


Oracle® DBAs and Developers Professional

Multi-Dimensional
Arrays in PL/SQL
Steven Feuerstein and John Beresniewicz
In this first of two articles appearing over the next several issues, Steven
Feuerstein and John Beresniewicz demonstrate the basic techniques of applying
multi-dimensional collections in your code. Next time, they’ll take a look at a
more complex scenario and bring the full power of Oracle9i collections to bear
on their solution.
March 2004

P
L/SQL developers are a generally whiny lot. We complain a lot, but then, Volume 11, Number 3

we have had a fair amount about which to complain. Today, PL/SQL is


an incredibly robust and rich language, but that wasn’t always the case. 1 Multi-Dimensional Arrays
in PL/SQL
We’re also, however, a fiercely loyal group of developers. We understand that Steven Feuerstein and
while PL/SQL isn’t perfect (and never will be), it’s also an elegant, powerful John Beresniewicz
language that’s very accessible and a joy to use.
One complaint for years has been around the lack of traditional array 7 The RBO: Separating Fact
from Fiction
structures. And over the years, Oracle has provided variants of an array, Dan Hotka
first called PL/SQL tables, then index-by tables, and now collections, to fill
the gap. In Oracle9i, some very key restrictions were lifted from collections 11 The StringArray Object Type
(which are, simply, lists of information). You can now index (some types Gary Menchen
of) collections by strings. You can also create and manipulate multi- 14 Tip: Having Fun with PL/SQL
dimensional collections. Anunaya Shrivastava
Of course, there’s a big difference between Oracle adding a new feature
to PL/SQL and developers being able to understand and take advantage 16 March 2004 Downloads
of that feature. Working with these new advances in collections can be
quite challenging, in particular multiple dimensions. This month, we take
a look at the basic techniques of applying multi-dimensional collections in
your code. Indicates accompanying files are available online at
Continues on page 3 www.pinnaclepublishing.com.
D:
GEA
PA
L
FUL IOUG

2 Oracle Professional March 2004 www.pinnaclepublishing.com


Multi-Dimensional Arrays... Y, Z) coordinate system. The following block illustrates
the sequential declarations necessary to accomplish this.
Continued from page 1
DECLARE
SUBTYPE temperature IS NUMBER;
Collection basics
Let’s do a quick review of the basic nature of collections. SUBTYPE coordinate_axis IS
PLS_INTEGER;
(Previous Oracle Professional articles have discussed
collections, so we’ll skim lightly over some of these TYPE temperature_x IS TABLE OF temp
INDEX BY coordinate_axis;
concepts. Check the online archives at the Oracle
Professional Web site for more information. In addition, TYPE temperature_xy IS TABLE OF tempx
INDEX BY coordinate_axis;
Oracle PL/SQL Programming, 3rd Edition, from O’Reilly &
Associates, offers a fat chapter on the topic and explains TYPE temperature_xyz IS TABLE OF tempxy
INDEX BY coordinate_axis;
this in much greater detail.) PL/SQL collections come in
the following four flavors: temperature_3d temperature_xyz;
BEGIN
• VARRAY temperature_3d (1) (2) (3) := 45;
• NESTED TABLE END;
/
• Associative TABLE indexed by PLS_INTEGER
• Associative TABLE indexed by VARCHAR2 Here the subtype and type names are used to
provide clarity as to the usage of the contents of the
The two associative table collection types are similar actual collection (temperature_3d): the collection types
in that they’re sparsely indexed (you can have a value in (temperature_X, temperature_XY, temperature_XYZ) as
row 1 and in row 17,436, but nothing “in between”), don’t well as the collection indexes (coordinate_axis).
require initialization prior to usage, and can’t be used as Note that although our careful naming makes it
database column data types. The VARRAY and NESTED clear what each of the collection types contains and is
TABLE collection types are similar in that they can be used for, we don’t have corresponding clarity when it
used as database columns, require initialization prior to comes to referencing collection elements by subscript;
usage, and are limited to positive integer indexing. in other words, in what order do we specify the
VARRAYs are always densely filled (the opposite of dimensions? It isn’t obvious from our code whether
sparse), while NESTED TABLEs may contain gaps, but by the temperature 45 degrees is assigned to the point
default aren’t sparse. We can create collections of pretty (X:1, Y:2, Z:3) or to (X:3, Y:2, Z:1).
much any kind of PL/SQL data type, including:
• Atomic data types, like NUMBER, VARCHAR2, Initializing multi-dimensional collections
and DATE (initialize or perish...)
• PL/SQL records, either with the %ROWTYPE Any collections that rely on the VARRAY or NESTED
declaration or based on user-defined record TYPEs TABLE collection types require explicit initialization
• Objects before they can be referenced in your code; associative
• SYS.ANYDATA arrays do not need to be initialized before use. If you
• Other collections don’t initialize, you’ll get the following error:

Multi-dimensional collections are simply another ORA-06531: Reference to uninitialized collection


name for this last case: As of Oracle9i, you can now
create collections of collections (of collections (of (This exception is also mapped to the name
collections...)). You can also create collections of a record COLLECTION_NOT_NULL.)
or object, one of whose fields or attributes is a collection, With multi-dimensional collections, the increased
and so on. This is all good news—except that these multi- number of dimensions escalates the potential for
level structures can be very challenging to work with. initialization confusion, so a quick review is worthwhile.
Let’s take a look at how you go about declaring multi- Basically, a VARRAY or NESTED TABLE collection
dimensional collections. can be in one of three states:
• Not initialized—this is also called atomically null
Declaring multi-dimensional collections • Initialized and empty
Multi-dimensional collections are declared in stepwise • Initialized and containing data
fashion, adding a dimension at each step (quite different
from the syntax used to declare an array in a 3GL). For Atomically null collections are the ones to avoid,
example, suppose we want to record temperatures within so it’s best to initialize collections upon declaration,
some three-dimensional space organized using some (X, whenever appropriate. These collections are initialized by

www.pinnaclepublishing.com Oracle Professional March 2004 3


calling the constructor method for the collection, either with differences between slicing out an XY plane, an XZ
or without data. The constructor method has the same plane, or a YZ plane in this fashion from a symmetric
name as the collection’s type, followed by an optional cube of data? If there are significant differences, it
parenthesized and comma-separated list of data elements could affect how we choose to organize our multi-
type-matched to the collection’s contents. dimensional collections.
The following block illustrates declaring and To explore this question, we create a simple package
initializing simple one and two-dimension VARRAY to encapsulate operations on a three-dimensional
variables. associative table storing VARCHAR2 elements indexed
in all dimensions by PLS_INTEGER. The following
DECLARE declarations constitute some basic building blocks for
type va1 is VARRAY(10) of integer;
type va2 is VARRAY(10) of va1; the package:
X1 va1; -- atomically null
CREATE OR REPLACE PACKAGE multdim
X2 va1 := null; -- also atomically null
IS
X3 va1 := va1(null); -- init. with 1 null element
TYPE dimX_t IS TABLE OF VARCHAR2 (32767)
X4 va1 := va1(); -- init. with 0 elements
INDEX BY PLS_INTEGER;
Y1 va2; -- atomically null
TYPE dimY_t IS TABLE OF dimX_t
Y2 va2 := null; -- atomically null
INDEX BY PLS_INTEGER;
Y3 va2 := va2(X1); -- init. with 1 atomically null
-- element TYPE dimZ_t IS TABLE OF dimY_t
Y4 va2 := va2(X4); -- init. with 1 element which INDEX BY PLS_INTEGER;
-- is initialized but empty
Y5 va2 := va2(va1()); -- same as above PROCEDURE setcell (
Y6 va2 := va2(X1, X4); -- init. with 1 atomically array_in IN OUT dimZ_t,
-- null collection and one dimX_in PLS_INTEGER,
-- empty collection dimY_in PLS_INTEGER,
BEGIN dimZ_in PLS_INTEGER,
IF y6(2).COUNT > 0 THEN NULL; END IF; value_in IN VARCHAR2
END; );
/
FUNCTION getcell (
array_in IN dimZ_t,
Notice in particular that for multi-dimensional dimX_in PLS_INTEGER,
collections we can make nested constructor calls (Y5), and dimY_in PLS_INTEGER,
dimZ_in PLS_INTEGER
we can initialize a collection with atomically null member )
elements (Y3, Y6). RETURN VARCHAR2;
In the executable block, testing the collection COUNT FUNCTION EXISTS (
method is used to identify whether the collection or array_in IN dimZ_t,
dimX_in PLS_INTEGER,
component has been initialized or is atomically null. dimY_in PLS_INTEGER,
Note also that the EXISTS method doesn’t raise dimZ_in PLS_INTEGER
)
COLLECTION_NOT_NULL but rather returns FALSE RETURN BOOLEAN;
when invoked on an atomically null collection.
We’ve defined the three collection types progressively
Multi-dimensional example: 3-space as before:
We can use a three-dimensional collection to model the • Type DimX_t is a one-dimensional associative
standard coordinate axis used to display functions in table of VARCHAR2 elements.
3-space. Each data point in our collection will correspond • Type DimY_t is an associative table of DimX_t
to a point in the XYZ space, as suggested by our earlier elements.
temperature example. We’ll now create a comprehensive • Type DimZ_t is an associative table of DimY_t
package to create and manipulate such a collection. elements.
The multdim package allows you to declare your
own three-dimensional array, as well as set and retrieve Thus, 3-space is modeled as cells in a collection of
values from individual cells. We’ll also take a further planes that are each modeled as a collection of lines.
step and implement support for “slicing” of that 3-D This is consistent with common understanding, which
collection, in which we fix one dimension and isolate indicates a good model. Of course, our collections are
the two-dimensional plane determined by the fixed sparse and finite, while geometric 3-space is considered
dimension. A slice from a temperate grid would give us, to be dense and infinite, so the model has limitations.
for example, the range of temperatures along a certain However, for our purposes we’re concerned only
latitude or longitude. with a finite subset of points in 3-space, and the model
Beyond the challenge of writing the code for slicing, is adequate.
an interesting question presents itself: Will there be any We equip our three-dimensional collection type

4 Oracle Professional March 2004 www.pinnaclepublishing.com


with a basic interface to get and set cell values, as well same VALUE_ERROR exception for so many different
as the ability to test whether a specific cell value exists in error conditions.
a collection. With the EXISTS function, we get to some code that’s
a bit more interesting. EXISTS will return TRUE if the cell
Basic operations identified by the coordinates is contained in the collection
Let’s look at the basic interface components. The and FALSE otherwise.
procedure to set a cell value in a 3-D array given its
coordinates couldn’t be much simpler: FUNCTION EXISTS (
array_in IN dimZ_t,
dimX_in PLS_INTEGER,
PROCEDURE setcell ( dimY_in PLS_INTEGER,
array_in IN OUT dimZ_t, dimZ_in PLS_INTEGER
dimX_in PLS_INTEGER, )
dimY_in PLS_INTEGER, RETURN BOOLEAN
dimZ_in PLS_INTEGER, IS
value_in IN VARCHAR2 l_value varchar2(32767);
) BEGIN
IS l_value := array_in(dimZ_in )(dimY_in )(dimX_in);
BEGIN RETURN TRUE;
array_in(dimZ_in )(dimY_in )(dimX_in) := value_in; EXCEPTION
END; WHEN NO_DATA_FOUND
THEN
RETURN FALSE ;
Despite the simplicity of this code, there’s significant END;
added value in encapsulating the assignment statement,
as it relieves us of having to remember the order of This function traps the NO_DATA_FOUND exception
reference for the dimension indexes. It’s not obvious raised when the assignment references a non-existent cell
when directly manipulating a DimZ_t collection whether and converts it into the appropriate Boolean. This is a
the Z coordinate is the first index or the last. Whatever is very simple and direct method to obtaining our result,
not obvious in code will result in bugs sooner or later. The and illustrates a creative reliance on exception handling to
fact that all of the collection indexes have the same data handle the “conditional logic” of the function.
type complicates matters, as mixed-up data assignments
won’t raise exceptions but rather just generate bad results Slicing and dicing
somewhere down the line. If our testing isn’t thorough, With the basics now covered, we can move on to more
these are the kinds of bugs that make it to production interesting tasks. Recall that we were interested in slicing
code and wreak havoc on data and our reputations. out two-dimensional “planes” from our 3-D collections by
Our function to return a cell value is likewise trivial specifying either an X or Y or Z value and returning the
but valuable. cells with that fixed value.
So we’ll add the following function declarations to
FUNCTION getcell (
array_in IN dimZ_t, the multdim package:
dimX_in PLS_INTEGER,
dimY_in PLS_INTEGER, FUNCTION dimXplane (
dimZ_in PLS_INTEGER array_in IN dimZ_t,
)
dimXval_in PLS_INTEGER -- fixed value of dimX
RETURN VARCHAR2
)
IS
RETURN dimY_t;
BEGIN
RETURN array_in(dimZ_in )(dimY_in )(dimX_in);
END; FUNCTION dimYplane (
array_in IN dimZ_t,
dimYval_in PLS_INTEGER -- fixed value of dimY
If there’s no cell in array_in corresponding to the )
RETURN dimY_t;
supplied coordinates, getcell will raise NO_DATA_
FOUND. However, if any of the coordinates supplied are FUNCTION dimZplane (
array_in IN dimZ_t,
null, the following less friendly exception is raised: dimZval_in PLS_INTEGER -- fixed value of dimZ
)
ORA-06502: PL/SQL: numeric or value error: RETURN dimY_t;
NULL index table key value
There’s an expected symmetry in the declarations:
In the future we may want to enhance the module to Each takes a 3-D array and a dimension value as input
assert a precondition requiring all coordinate parameter and returns an array of type dimY_t, which is our two-
values to be not null. At least Oracle now provides us dimensional collection type.
with an improved error message, informing us that a Symmetry isn’t the case, however, at the
null index value was responsible for the exception. It implementation level. The code to slice out an XY plane
would be even better, though, if Oracle didn’t use the for a given Z value is significantly different from that

www.pinnaclepublishing.com Oracle Professional March 2004 5


which slices out an XZ plane given a Y value. Let’s take a END LOOP;

closer look. RETURN dimY_tbl;


END dimYplane;

Function dimZplane
The Z dimension was the last in our stepwise declaration Studying this code gives an appreciation for the
of collection types (the third dimension). The dimZ_t type challenges of keeping indexing straight in multi-
is our 3-D collection, and for any given index value in the dimensional collections, and for the fact that all
Z dimension the element for that value will be of type dimensions are not equal even in what seems on the
dimY_t. That is, the element at any given value for surface to be a highly symmetrical requirement.
dimension Z is precisely the 2-D plane of interest defined
as a collection of the type returned by the function. Function dimXplane
Having learned an important complexity lesson in
FUNCTION dimZplane ( dimYplane, it shouldn’t be a surprise to find that our
array_in IN dimZ_t, third function to slice out a YZ plane given an X value
dimZval_in PLS_INTEGER -- fixed value of dimZ
) is yet more complex. In this case we do the following:
RETURN dimY_t 1. Loop over both the Z and Y dimensions from
IS
dimY_tbl dimY_t; beginning to end.
BEGIN 2. If an entry exists in the collection for the X value
IF array_in.EXISTS(dimZval_in)
THEN specified and current loop indexes for Z and Y, add
dimY_tbl := array_in(dimZval_in); the value of this entry to a 2-D array using the Z and
END IF;
RETURN dimY_tbl; Y indexes.
END dimZplane;
FUNCTION dimXplane (
So in this case, our function has a simple job to do: array_in IN dimZ_t
, dimXval_in PLS_INTEGER -- fixed value of dimX
Just find the proper element and return it. No muss, no )
fuss—precisely because this particular slicing corresponds RETURN dimY_t
IS
directly to the way that the multi-dimensional collection dimY_tbl dimY_t;
is constructed. If only it were always this simple. indx1 PLS_INTEGER;
indx2 PLS_INTEGER;
BEGIN
Function dimYplane—all dimensions are indx1 := array_in.FIRST;

not equal WHILE indx1 IS NOT NULL


Life gets a bit more complicated when we want to LOOP
indx2 := array_in (indx1).FIRST;
return the 2-D plane defined by a given Y dimension
value. We need to find Z and X value pairs for the given WHILE indx2 IS NOT NULL
LOOP
value of Y and assemble them into a dimY_t array. The IF array_in (indx1) (indx2).EXISTS (dimXval_in)
logic is as follows: THEN
dimY_tbl (indx1) (indx2) :=
1. Loop through the Z dimension from beginning array_in (indx1) (indx2) (dimXval_in);
to end. END IF;

2. If an entry exists for the given Y value at this Z indx2 := array_in (indx1).NEXT (indx2);
value, add the one-dimensional array of values END LOOP;

defined by this Y and Z as a new element in a indx1 := array_in.NEXT (indx1);


2-D return array. END LOOP;

RETURN dimY_tbl;
FUNCTION dimYplane ( END dimXplane;
array_in IN dimz_t
, dimYval_in PLS_INTEGER -- fixed value of dimY
) Once again, the code reveals a certain structural
RETURN dimY_t symmetry, but is far from trivial (as compared to the first
IS
dimY_tbl dimY_t; slicing program for the X dimension).
indx PLS_INTEGER;
BEGIN
indx := array_in.FIRST; Conclusion
Multi-dimensional PL/SQL collections are extremely
WHILE indx IS NOT NULL
LOOP powerful data structures, offering tremendous flexibility.
IF array_in (indx) (dimYval_in).COUNT > 0 There is, however, a clear trade-off: Manipulation of
THEN
dimY_tbl (indx) := array_in (indx) (dimYval_in); elements within the various dimensions can be very
END IF; complex. Generally, the level of complexity corresponds
indx := array_in.NEXT (indx); Continues on page 16

6 Oracle Professional March 2004 www.pinnaclepublishing.com


Oracle
Professional

The RBO: Separating Fact


from Fiction
Dan Hotka
Oracle continues to support both the Cost-Based Optimizer relies heavily upon the existence of indexes, ranging from
(CBO) and the Rule-Based Optimizer (RBO). It’s rumored that unique indexes, partial key lookups, on to inequality
Oracle10g will also continue to support the RBO, although conditions—but still using indexes—to sorting, functions,
possibly undocumented, and many people find that the RBO and then the full table scans.
continues to outperform the CBO in some instances. There So, how does the RBO make its decisions? Both
are many misconceptions on how the RBO makes its optimizers parse the SQL from the end to the beginning.
decisions—Dan Hotka aims to prove or dispel those theories The RBO focuses on the tables in the FROM clause. Since
in this article. it parses from end to beginning, the first table it comes to
in the FROM clause is the last one in the list. If there’s

I
N doing my two-day workshops over the past year, only one table in the FROM clause, the RBO’s processing
I’ve discovered that most companies continue to use a is a bit simpler. Let’s review this scenario first.
mix of both the Cost-Based Optimizer and the Rule- The RBO simply looks for the existence of indexes
Based Optimizer, pretty much separated by applications. on columns in the WHERE clause and picks the predicate
I’ll start this article with a quick review of the RBO, and to start processing based on the rules in Table 1. The
then I’ll introduce many of the theories I’ve encountered lowest rank wins. If two columns have indexes that come
over the years and conclude with a quick comparison of out to the same rank, the one with the newest creation
the RBO vs. the CBO. date wins.
If the SQL has two or more tables being joined, the
RBO: Review RBO will then make several passes through the WHERE
The Rule-Based Optimizer makes its decisions based on clause predicates to find the relationships and determine
the text of the SQL statement itself, the presence or what, if any, indexes exist on those relationships. It starts
absence of indexes, the order of the
tables in the FROM clause, and data Table 1. RBO rules.
dictionary information.
The RBO uses a set of 19 rules to Rank WHERE clause rule
1 ROWID = constant
make its decisions; they’re displayed
2 unique indexed column = constant
in Table 1. The fastest way to access 3 entire unique concatenated index = constant
any single row in an Oracle database 4 entire cluster key = cluster key of object in same cluster
is to supply a valid ROWID. This, of 5 entire cluster key = constant
6 entire nonunique concatenated index = constant
course, ranks the highest on the rule
7 nonunique index = constant
list. Full table scans rank the lowest, 8 entire noncompressed concatenated index >= constant
although a full table scan might be 9 entire compressed concatenated index >= constant
the best access method. 10 partial but leading columns of noncompressed concatenated index
11 partial but leading columns of compressed concatenated index
The RBO really has only a few
12 unique indexed column using the SQL statement BETWEEN or LIKE options
decisions to make with each SQL 13 nonunique indexed column using the SQL statement BETWEEN or LIKE options
statement. I’ve found that only the 14 unique indexed column < or > constant
data dictionary tends to have 15 nonunique indexed column < or > constant
16 sort/merge
“clustered” objects. Most objects
17 MAX or MIN SQL statement functions on indexed column
don’t have such a variety of different 18 ORDER BY entire index
types of indexes either. The RBO 19 full table scans

Example databases and some of the scripts used in this article come with permission from
Tim Gorman (www.Sagelogix.com) and Jonathan Lewis (www.jlcomp.demon.co.uk).

www.pinnaclepublishing.com Oracle Professional March 2004 7


with the last table in the FROM clause, and picks the Java. A variety of data about the objects is just a click
one just to its left. It searches the WHERE clause for away, and a list of common hints is also available with the
conditions that join these tables together. If it finds a press of a button. I use this tool in my workshops when
condition, it uses it. If there’s more than one condition my clients don’t have a tool of choice.
that joins the tables, it uses the RANK (from the list of
rules in Table 1) of the condition to break any ties. If Theory 1: WHERE clause predicate order matters
there’s a tie in the rank of two different conditions, the An interesting thing I keep hearing is that the RBO makes
RBO then looks at the creation dates of the indexes its driving table decisions based on predicates in the
involved and breaks the tie using the predicate with the WHERE clause. The driving table is the one that’s selected
index that has the newer creation date. first in the processing of a SQL statement that has two or
more tables in the FROM clause. The driving table is also
Review: For a thorough review of both optimizers, useful information in a nested loop (the RBO likes using
please review my article “Oracle Optimizers: How nested loops when joining tables together). The RBO
Do They Work?” in the September 2002 issue of makes a decision based on the rules again for the order in
Oracle Professional. which tables appear in a nested loop.
Let’s look at an example (with permission from Tim
I’ll use the Java SQL Tuner tool shown in Figure 1, Gorman) of three tables: A, B, and C. Table B has 100
which I designed to easily work with SQL and explain rows, and tables A and C have 1,000 each. All statuses
plans (the tool is available as a freeware download from are ‘OPEN’, with indexes on B.STATUS, C.B_ID, and
www.DanHotka.com). It’s written in Java, so it works A.STATUS. I’ll start with the following SQL statement;
equally well in all computing environments that support the explain plan it produces is shown in Figure 2.

SELECT count(*)
from A, B, C
WHERE A.STATUS = B.STATUS
AND A.B_ID = B.ID
AND B.STATUS = 'OPEN'
AND B.ID = C.B_ID
AND C.STATUS = 'OPEN';

The first column of numbers


gives the steps of the explain plan,
and the next column is the parent
step (that is, which line in the explain
plan the line supports). Notice in this
explain plan that the driving tables
in the nested loop at Step 4 are B and
C (Step 5 and Step 7). I can tell this
from both the indentation of the
explain text and the fact that both of
their parent IDs are Step 4.
The RBO parses backwards. So,
if we move the A table predicate
being joined to the B table predicate
to the end of the SQL statement, we
Figure 1. Java SQL Tuner.
should see a change in the explain
plan. Right? Well, let’s see with the
following query:
Figure 2. SQL with
explain plan. SELECT count(*)
from A, B, C
WHERE A.STATUS = B.STATUS
AND B.STATUS = 'OPEN'
AND B.ID = C.B_ID
AND C.STATUS = 'OPEN'
AND A.B_ID = B.ID;

The result is an explain plan


identical to the one in Figure 2. In

8 Oracle Professional March 2004 www.pinnaclepublishing.com


fact, no matter how we change the WHERE clause order key index on DEPT.DEPTNO, I couldn’t get the explain
of predicates in this SQL statement, we’ll get the same plan to change. Following the rules, Oracle always drove
explain plan. off of EMP using the INDEX of the DEPT table to loop in
So does the order of WHERE clause predicates the nested loop. Figure 5 shows the explain plan from the
matter to the RBO? Not in this SQL statement. We’ll see following query:
whether the order of the WHERE clause predicates helps
the multi-key index usage or full-table usage discussed select emp.ename, dept.loc
from emp, dept
later in this article. where dept.deptno = emp.deptno;

Theory 2: FROM clause table order matters Theory 3: Index creation date matters
Does the RBO make its driving table decisions from the Following close behind the example in Figure 3, the RBO
order of the FROM clause? Let’s look at the A, B, and C seemed to pick B over A as the driving table no matter
tables again. Notice in Figure 2 that the tables in the which side of the “=” condition the A and B predicates
FROM clause are in the order A, B, C. Let’s change the appeared on. It seemed to pick the B table because of the
order and see what happens to our explain plan. In the creation date of the indexes on the STATUS columns. In
following query, we’ll move the A table to the end of the this example, however, the RBO always picked the B table
FROM clause from the beginning: to drive off of in a nested loop, even with the indexes
being created at different times and with the B.STATUS
SELECT count(*) index dropped!
from B, C, A
WHERE A.STATUS = B.STATUS So what’s the answer? The RBO doesn’t seem
AND B.STATUS = 'OPEN' to be making decisions based solely on the existence
AND B.ID = C.B_ID
AND C.STATUS = 'OPEN' of indexes. It seems to pick the nested loop join
AND A.B_ID = B.ID; mechanism when there’s at least one index involved,
while picking the merge join mechanism when no
The results are shown in Figure 3. Notice that when indexes are involved.
the RBO starts with the A table, there’s no condition in In the A, B, C table example, it always picked the B
the WHERE clause that compares A and C directly. The table to drive off of until there were no indexes on either
RBO then moves to the B table and finds two different the B table or the C table. The RBO seems to favor driving
predicates, A.B_ID = B.ID and A.STATUS = B.STATUS.
It picks the STATUS column, not because of its position
in the WHERE clause (as some believe) but because it
tied on rule 7 and the index creation date broke the tie
in favor of the B table (see the output from the script
Index_Info.sql in Figure 4). When statistics have been run,
more information appears in this script (a topic for a
future article!). Since we’re using the RBO, we can’t have
statistics collected for the purposes of these examples.
Yes, the order of tables in the FROM clause has a
distinct and predictable affect on the RBO.
Let’s look at just two tables in the FROM clause.
Using the traditional EMP and DEPT, with only a primary

Figure 4. Output from the Index_Info.sql script.

Figure 3. Different FROM clause with explain plan. Figure 5. Two-table FROM clause.

www.pinnaclepublishing.com Oracle Professional March 2004 9


off of tables that have more selection criteria in the the keys generated in groups. TBL2 has 14 rows in it.
WHERE clause, although the order of the criteria and the Both tables have the same columns as shown in Figure 6.
side in which the “=” predicates appear don’t seem to TBL1 has a variety of indexes as shown in Figure 7, while
have any effect on the driving table selection. TBL2 has no indexes. The tables are populated with this
PL/SQL routine:
Theory 4: Multi-key index matters
The theory behind multi-key is that the RBO may DECLARE
loop_counter NUMBER := 0;
pick the concatenated key index if the WHERE clause BEGIN
is coded so that the columns appear in the correct order FOR rec IN (SELECT *
FROM emp)
as the index. LOOP
This section will use two tables: MULTI_KEY_TBL1 insert into multi_key_tbl2
values (30, 300, rec.empno, rec.ename);
and MULTI_KEY_TBL2. TBL1 has 140,700 rows in it, FOR key1_counter in 1 .. 50
LOOP
loop_counter := 0;
FOR key2_counter in 200 .. 400
LOOP
loop_counter := 0;
insert into multi_key_tbl1
values (key1_counter, key2_counter,
rec.empno,round(rec.sal *.025) );
commit;
END LOOP;
END LOOP;
END LOOP;
commit;
END;

TBL2 has 14 rows with the key values 30, 300,


EMPNO, and ENAME. TBL1 has key values from 1
Figure 6. TBL1 and TBL2 columns.
to 50 in the first column, values 200 through 400
(descending) in the second column, and EMPNO again
in the third column.
We’ll start our investigation with a simple SQL
statement. We want to sum some fields; notice that
the WHERE clause is in the order of the KEY123 multi-
key index.

select sum(sales_tot)
from multi_key_tbl1 a, mutli_key_tbl2 b
where a.key1 = b.key1
and a.key2 = b.key2
and a.key3 = b.key3;

As you can see in Figure 8, the RBO used the multi-


column index.
Now let’s try the same query with the WHERE clause
Figure 7. TBL1 indexes.
shuffled a bit:

select sum(sales_tot)
from multi_key_tbl1 a, multi_key_tbl2 b
where a.key2 = b.key2
and a.key3 = b.key3
and a.key1 = b.key1;

The resulting explain plan is the


same as the one in Figure 8. Oracle’s
RBO seems to find the multi-keyed
index no matter how I code the
WHERE clause—it’s following the
19 rules, not the coding style of the
WHERE clause.
Figure 8. Explain plan from the multi-key example. Continues on page 14

10 Oracle Professional March 2004 www.pinnaclepublishing.com


Oracle
Professional

The StringArray Object Type


Gary Menchen
Object types are particularly suited to building components Htp.tableData(pList(i));
End Loop;
that provide the developer with an interface for managing End;
data structures. In this article, Gary Menchen starts with a
VARRAY of VARCHAR2 and wraps it inside an object type. The This procedure can be invoked by:
result is an object that resembles a JavaScript array, or a
Delphi TStringList, which is very useful for managing strings fillHtpTableData( StringList( rec.val1, rec.val2,
.. rec.valn));
and provides a convenient interface with CLOBs.
Notice that the VARRAY is declared and initialized

L
ET’S begin by declaring the VARRAY as shown in the on the fly, within the parameter list of the calling
following code. Oracle’s VARRAY collection type function. This is one instance where conciseness doesn’t
provides PL/SQL with a traditional array—that is, sacrifice readability.
non-sparse and strongly typed.
Declaring the StringArrray type specification
CREATE OR REPLACE
TYPE StringList The object type specification in Listing 3 is reasonably rich
AS VARYING ARRAY (10000) OF VARCHAR2(2000) in member procedures and functions. It does not, and
/
need not, contain every process that you or any of your
The array limitation of 10000 is arbitrary, and serves fellow developers will ever want to perform upon an
only as a constraint—any attempt to extend beyond that array of strings. With Oracle9i Release 2, object types with
limit will raise the Oracle exception “ORA-06532: dependents can now be altered. This isn’t as convenient as
Subscript outside of limit.” The declaration doesn’t buy CREATE OR REPLACE (available only for object types
any apparent initialization. without dependents)—think of the difficulties of adding
The StringList type is useful by itself—just consider comments using the alter syntax—and there are bugs in
any procedure that takes an unknown number of the ALTER command that may not be fixed until version
parameters. Instead of having a long list of optional 10g, so it’s still very necessary to carefully plan your
parameters like the procedure in Listing 1, you can simply object type design, and to have a drop and re-create
declare a StringList parameter as in Listing 2, which is strategy in case a hierarchy of types have to be rebuilt.
convenient both within the procedure and when calling it. A second way of adding functionality is by
inheritance, which I think is preferable once an object type
has passed a preliminary shake-down phase. A brief
Listing 1. The traditional manner of handling a varying number discussion of inheritance appears at the end of this article.
of parameters.

Procedure fillHtpTableRow(p1 in varchar2, Listing 3. The StringArray object type specification.


p2 in varchar2 default null,
p3 in varchar2 default null,… )
CREATE TYPE STRINGARRAY AS OBJECT
is
(
begin
alist stringlist,
htp.tabledata(p1);
-- standard constructor
If p2 is not null then
constructor function stringArray
ptp.tabledata(p2);
return self as result,
end if;
-- methods to return information about the size of
if p3 is not null then
-- the array
htp.tabledata(3);
member function getLength return number,
end if
member function first return number,

member function last return number,

-- methods to add strings to the array


Listing 2. Using a VARRAY to process an unknown number -- assign replaces the existing array with <pStrings>
of parameters. member procedure assign(pStrings in StringList),
-- add a single string at the end of the array
member procedure addElement( pString in varchar2),
Procedure fillHtpTableRow(pList in StringList) is -- add a varray of strings at end of array
Begin member procedure addStringList(pList in stringList),
For I in pList.first .. pList.last loop -- add text, breaking it up into separate strings

www.pinnaclepublishing.com Oracle Professional March 2004 11


-- based upon a deliminter character is always 1 unless the array is empty. The member
member procedure addText( pText in varchar2,
pDelim in varchar2 default ','), functions getLength and last both resolve to aList.last.
-- addClob depends upon line termination characters
-- to divide the content into array elements. If Using functions rather than directly accessing attributes
-- none are present, then the maximum string length of a type is always a good practice, at least outside of the
-- of 2000 is used.
member procedure addClob( pClob in clob), TYPE BODY.
-- Push is identical to addElement, but provides
-- symmetry for pop
member procedure push( pString in varchar2 ), Adding strings to the array
-- methods to retieve elements from the array
member function getElement( pIndex in number) There are four different procedures that add to the array:
return varchar2, one for a single VARCHAR2, one for a StringList, one
-- pop requires an explicit declarations of self as
-- an in out parameter since it is a function that for a VARCHAR2 that may contain a delimiter used to
-- modifies one of its own attributes. By default parse it into some number of array elements, and one
-- SELF is implicitly passed as an IN parameter with
-- member functions for a CLOB that breaks up the content based on line-
member function pop( self in out StringArray ) return
varchar2, termination characters. I use CLOBs to store all kinds
-- various other methods to alter the contents of of textual material in the database, and find it very
-- the array
-- setLength may add or delete elements atend convenient not to have to worry about what type of line
member procedure setLength( pLength in number ), termination character I used. The addClob procedure
-- setElement requires that the array element already
-- exist. continuously probes for the next potential line terminator,
member procedure setElement(pIndex in number,
pString in varchar2), whether it be carriage return and line feed, or a single
member procedure deleteElement(pIndex in number), instance of either of those two characters. This makes the
-- purge truncates the array
member procedure purge, procedure more complicated than if a single known line-
member procedure insertElement(pIndex in number,
pString in varchar2),
terminator is used, and in some environments it may be
member function find( ptext in varchar2, reasonable to take a simpler approach.
pFrom in number default 1,
pInstance in number default 1)
return number, Find and sort
-- case insensitive version of find
member function findci(ptext in varchar2, There are two flavors of the find function, one that does a
pFrom in number default 1,
pInstance in number default 1)
straight equivalency and another that’s case-insensitive.
return number, Other variations would be useful, such as the index of the
member procedure swap( pIndex1 in number,
pIndex2 in number), element less than or greater than the comparison element.
member procedure sort( pOrder in varchar2 The sort procedure uses BULK COLLECT, TABLE,
default 'asc'), -- or desc
and CAST and is worth examining. The sort is done by
-- and some other exchange functions
-- with asText, each element from the array is
selecting into a locally declared StringList variable, and
-- separated bythe delimiter. then assigning the local variable to the attribute aList.
member function asText(pDelim in varchar2
default ',') return varchar2,
member function asClob( pDelim in varchar2 select column_value bulk collect into vList from
default chr(13)||chr(10)) return clob, table(self.aList ) order by 1;
-- The static procedure test is used the validate
-- the type. Most results are sent through
-- dbms_output It would have been nice to select directly into place—
static procedure test
) that is, select into self.aList rather than to the variable
/ vList—but while that compiled and ran, the result was
that aList was empty. column_value can be used as the
A few notes on the specification name of the column returned when the VARRAY or
I use a single constructor, with no parameters. Object nested table is CAST to table. It’s also possible to do a
types automatically have a constructor that matches the SELECT * FROM a table cast of a VARRAY, but that
attribute list in the specification. If the StringList needs to requires that the embedded VARRAY contain an explicit
be initialized, the implicit constructor can be used as CAST as well.
shown in the following script: It’s possible to implement the find member functions
using the table cast method as well, but I couldn’t find a
-- Anonymous block showing use of a default
-- constructor not explicitly declared in the measurable speed advantage in doing so. Interested or
-- specification. skeptical readers can check an anonymous block in the
declare
vStrings StringArray; listings available in the Download file that attempts to
begin
vStrings := StringArray( compare the performance for find on a VARRAY using an
StringList('1','2','3','4','5') ); iterative loop and a SELECT from a table cast.
dbms_output.put_line(vStrings.asText);
end;
Interfaces to other data types
First, last, and getLength The asText and asClob functions are the mirror image of
Because this is a VARRAY, the value of the function first the addText and addClob procedures. In particular, I find

12 Oracle Professional March 2004 www.pinnaclepublishing.com


the two-way interface with CLOBs very convenient. The type and manipulated directly. It’s true that the aList
two-way interfaces with text (that is, a VARCHAR2) and attribute could be accessed directly, but, as I’ve mentioned
with CLOBs are also useful if you need to rebuild a table before, direct access of attributes should be discouraged.
that contains an object type. They’ll allow you to load the I was surprised to discover that with both the
contents of a table containing an object type into a “flat” StringList and the StringArray, doing a single extend,
table, drop and re-create the table containing the object instead of the 4000 individual extends, didn’t make a
type, and then reload that table. significant difference in performance and in some
Finally, the specification ends with a static procedure, iterations of the script actually took longer.
test. Any type—or any package—can benefit by having a
built-in test procedure that attempts to validate whatever Inheritance and class hierarchies
processes are being performed. In this case, the test I began this article by discussing the uses of VARRAY
procedure repeatedly loads the array, manipulates, and of VARCHAR2. VARRAYs of all the standard scalar
then displays the contents using the DBMS_OUTPUT types, and of some specialized types (for instance,
Oracle supplied package. NUMBER(10,2) for money) are useful as well. Once the
VARRAY of each type is declared, it makes sense to
A few notes on performance provide other object types that wrap each VARRAY in
To get a sense of the performance of StringArrays, I the same way that StringArray wraps the StringList
compared the timing for inserts into the StringList VARRAY in this article.
VARRAY and the StringArray object type. There’s a cost It should then follow that good programming
for using the object type wrapper instead of directly practice is to define a non-implementable base type
accessing the VARRAY. (call it BaseArray), define the methods we implemented
Listing 4 shows a portion of the script I used to for StringArray—most of which won’t be implementable
perform the comparison. within BaseArray—and then create StringArray and
any other types of array we wish to implement under
BaseArray. We’d then have a consistent set of procedures
Listing 4. Performing the comparison.
and functions available for all of our array types. This
declare would make them easier to document and easier for
vList StringList; other developers to learn to use. Of course, some types
vArr StringArray;
vStart integer; would have methods not appropriate for other types. A
vDuration number; numeric array, for example, could have various analytic
vMax integer := 4000;
begin mathematical functions implemented using the TABLE
vList := new StringList(); casting function (see Miscellaneous Script#4 in the source
vArr := new StringArray();
dbms_output.put('StringList with '||to_char(vMax) included in the Download).
||' individual extends ');
vStart := dbms_utility.get_time;
for i in 1..vMax loop Adding or modifying functionality through
vList.extend; inheritance
vList( vList.last ) := 'This is row number ' ||
to_char(i); A long-standing goal of programming teams is to have a
end loop; common toolbox of procedures for shared use. This goal
vDuration := (dbms_utility.get_time - vStart) / 100;
dbms_output.put_line(to_Char(vDuration,'990.999')); is often not realized very well because enforcement of
common shared procedures often means enforcing the
dbms_output.put('StringArray with ' || to_Char(vMax)
||' individual pushes '); lowest common denominator. Object types are ideal for
vStart := dbms_utility.get_time; shared programming tools because they’re extensible. If a
for i in 1..vMax loop
vArr.push('This is row number ' || to_char(i)); programmer needs an additional procedure, or has a
end loop; method of implementing a standard procedure that works
vDuration := (dbms_utility.get_time - vStart) / 100;
dbms_output.put_line( to_Char(vDuration,'990.999')); better in a specific circumstance, he or she can create a
end; new type under the existing type, add new methods, and
override existing procedures as desired, while still using
On my laptop, the timing I got for adding 4000 strings the basic tool and—even more important—not disturbing
was consistently between 0.02 and 0.03 seconds using the anyone else’s use of the tool. In my mind, object types are
StringList VARRAY, and doing individual extends. The far superior to packages for this purpose. ▲
timing for the StringArray was generally over three full
seconds, so about 100 times longer. This is probably not 403MENCHEN.ZIP at www.pinnaclepublishing.com
significant in most uses—but after finding this result I
added the assign procedure and the asStringList function, Gary Menchen is a senior programmer analyst at Dartmouth College in
so that the VARRAY could be retrieved from the object New Hampshire. Gary.E.Menchen@Dartmouth.edu.

www.pinnaclepublishing.com Oracle Professional March 2004 13


Tip: Having Fun with PL/SQL
Anunaya Shrivastava

What fun can we have with PL/SQL? One of my friends posed me set serveroutput on size 1000000
declare
this puzzle: Suppose it’s circa 1950 and you have $100 to spend x number; -- num of chicken
y number; -- num of sheep
at a livestock fair. You’re in the market to buy chickens, sheep, and z number; -- num of pigs
pigs. The number of animals that you have to buy is 100. Each begin
--check every value of pig in 1..100
chicken costs 10 cents, a sheep costs $2, and a pig costs $5. How For z in 1..100
loop
many of each should you buy? --For each value of pig check every value of sheep
--in 1..100
Solution: Mathematically, there can be two linear equations For y in 1..100
that can be developed with the aforesaid information with loop

three variables. --For each value of pig and sheep


--check every value of chicken in 1..100
Let’s say that the number of chickens that can be bought is For x in 1..100
X, the number of sheep that can be bought is Y, and the number loop

of pigs is Z. So we get two equations: X + Y + Z = 100 and 0.1X + --If the equations are satisfied break the loop
If ((x+20*y+50*z =1000) and (x+y+z =100)) then
2Y + 5Z = 100 (or X + 20Y + 50Z = 1,000). Two linear equations dbms_output.put_line('The number of chickens is '||x);
with three variables can’t give an exact answer by solving them. dbms_output.put_line('The number of sheep is '||y);
dbms_output.put_line('The number of pigs is '||z);
However, an exact solution does exist. The other information we exit;
end if;
have is that X, Y, and Z are greater than 0 and are all positive end loop;
integers. We can try different values for X, Y, and Z and see end loop;
end loop;
whether the values of X, Y, and Z satisfy both of the equations. end;
/
However, it’s a Sisyphean task to find those values by trial and
error. To achieve this, we can use PL/SQL loops that achieve this And the answers are: 70 chickens, 19 sheep, 11 pigs. ▲
task in a few seconds.
We can check all combinations possible by running the Anunaya Shrivastava, PMP, OCP Financials, OCP Internet Developer, OTC,
values in loops and breaking the loop whenever the two criteria has been working with Oracle technology for more than seven years. He
are met, as I do here: works for HCL Enterprise Solutions. anunaya@hotmail.com.

The RBO... take a look at the A, B, and C tables with no indexes


using the following query:
Continued from page 10
SELECT count(*)
from A, B, C
Theory 5: Full table scans matter WHERE B.STATUS = A.STATUS
This final example uses tables with no indexes. The AND A.B_ID = B.ID
AND C.STATUS = 'OPEN'
theory is that when there are no indexes present to help AND C.B_ID = B.ID
the RBO with decisions, the order of the WHERE clause AND B.STATUS = 'OPEN';

predicates and which side of the “=” operator they appear


on matter in determining which table in the FROM clause
gets processed first. In other words, the RBO uses the
order of the WHERE clause to break the FULL-TABLE
scan ties.
We’ll start with this simple SQL statement, which
produces the explain plan shown in Figure 9:

select emp.ename, dept.loc


from emp, dept
where dept.deptno = emp.deptno;

Where the DEPT.DEPTNO appeared in the WHERE


clause didn’t change in this explain plan. Now let’s Figure 9. WHERE clause, no index.

14 Oracle Professional March 2004 www.pinnaclepublishing.com


The resulting explain plan is shown in Figure 10. decisions are usually difficult to change. The fact that
Notice that the first table accessed is the last one in the some SQL statements continue to run better in the RBO is
WHERE clause. Using FULL-TABLE scans, the RBO just plain luck. You should be running explain plans on
seems to be ignoring the multiple WHERE clause these SQL statements and switching them to the CBO
predicates of the B table, although I couldn’t get the using hints to produce a similar explain plan.
RBO to access the B table in step 10!
No matter how I changed the WHERE clause, the Summary
order of the tables being accessed didn’t change. I In this article you learned that the RBO seems to pick
wasn’t able to get the RBO to use a nested loop; the the driving table based more on the frequency the table
FULL-TABLE scans always seemed to use the merge join is accessed in the WHERE clause than the order in which
condition. I checked this on both an Oracle8i database it appears in the FROM clause. The order of things in
and an Oracle9i database. the WHERE clause doesn’t seem to make any difference
to the RBO. When using multi-key indexes, again, the
RBO vs. CBO: What you are missing by using the RBO always picks the multi-key index (as it should),
Rule-Based Optimizer? no matter how the WHERE clause was coded. The order
What does this article really prove? If you want better of things in the FROM clause makes a difference in the
control over the execution plan for SQL in Oracle, you execution plan that the optimizer selects, while the
should be using the Cost-Based Optimizer. The RBO order of things in the WHERE clause doesn’t seem to
hasn’t been enhanced for years. As my research proves make any difference. ▲
here, the RBO makes very predictable decisions. These
403HOTKA.SQL at www.pinnaclepublishing.com

Figure 10. Multiple


Dan Hotka is a training specialist who has more than 25 years of
WHERE clauses,
experience in the computer industry and more than 20 years working
no index. with Oracle products. He’s an internationally recognized Oracle expert
with experience dating back to the Oracle v4.0 days. His current book,
TOAD Handbook (from SAMS), is now available on Amazon.com. Dan
is also the author of Oracle9i Development By Example and Oracle8i
from Scratch (both from Que) and has co-authored five other popular
books. He’s frequently published in Oracle trade journals and regularly
speaks at Oracle conferences and user groups around the world.
www.DanHotka.com, dhotka@earthlink.net.

Don’t miss another issue! Subscribe now and save!


Subscribe to Oracle Professional today and receive a special one-year introductory rate:
Just $179* for 12 issues (that’s $20 off the regular rate)

NAME ❑ Check enclosed (payable to Pinnacle Publishing)


❑ Purchase order (in U.S. and Canada only); mail or fax copy
COMPANY
❑ Bill me later
❑ Credit card: __ VISA __MasterCard __American Express
ADDRESS
CARD NUMBER EXP. DATE

CITY STATE/PROVINCE ZIP/POSTAL CODE


SIGNATURE (REQUIRED FOR CARD ORDERS)

COUNTRY IF OTHER THAN U.S.


Detach and return to:
Pinnacle Publishing ▲ 316 N. Michigan Ave. ▲ Chicago, IL 60601
E-MAIL Or fax to 312-960-4106

* Outside the U.S. add $30. Orders payable in


403INS
PHONE (IN CASE WE HAVE A QUESTION ABOUT YOUR ORDER) U.S. funds drawn on a U.S. or Canadian bank.

Pinnacle, A Division of Lawrence Ragan Communications, Inc. ▲ 800-493-4867 x.4209 or 312-960-4100 ▲ Fax 312-960-4106

www.pinnaclepublishing.com Oracle Professional March 2004 15


Multi-Dimensional Arrays... string-based indexing. ▲

Continued from page 6 403FEUER.ZIP at www.pinnaclepublishing.com

to the dimensional level at which the manipulation Steven Feuerstein is considered one of the world’s leading experts on the
takes place. Higher-level dimensional access produces Oracle PL/SQL language, having written nine books on PL/SQL, including
simpler code structures, as complex structures are Oracle PL/SQL Programming and Oracle PL/SQL Best Practices (all from
manipulated as a unit. Directly addressing and O’Reilly & Associates). Steven has been developing software since 1980
manipulating elements lower down in the dimension and serves as a senior technology advisor to Quest Software. His current
chain results in more complex code. It can be quite a projects include Swyg (www.Swyg.com) and the Refuser Solidarity
challenge to keep straight all of the different dimensional Network (www.refusersolidarity.net), which supports the Israeli military
indexes, and the order in which they must be specified. refuser movement. steven@stevenfeuerstein.com.

One important conclusion to draw is that you should


John Beresniewicz is a consulting member of technical staff at Oracle
encapsulate access to these data structures, providing a
Corporation. Previously he was with Precise Software and Savant
simple, unambiguous API to getting and setting values at
Corporation. He’s been developing, investigating, and writing about
various levels in the structure. Oracle software since 1987. John is a frequent speaker at Oracle
conferences large and small, a past member of the IOUG-A University
Next time Master Class faculty, and co-author with Steven Feuerstein of two books
In a future issue of Oracle Professional, we’ll continue on Oracle PL/SQL from O’Reilly & Associates. John is also a devotee of Adi
our examination of multi-dimensional collections with Da Samraj and invites everyone to investigate the Way of Adidam at
a less abstract example that also brings into the picture www.adidam.org. john.beresniewicz@oracle.com.

March 2004 Downloads


• 403FEUER.ZIP—Source code to accompany Steven Hotka’s article.
Feuerstein and John Beresniewicz’s article.
• 403MENCHEN.ZIP—Source code to accompany Gary
• 403HOTKA.SQL—Source code to accompany Dan Menchen’s article.

For access to current and archive content and source code, log in at www.pinnaclepublishing.com.

Editor: Garry Chan (gchan@procaseconsulting.com) Oracle Professional (ISSN 1525-1756)


Contributing Editor: Bryan Boulton is published monthly (12 times per year) by:
CEO & Publisher: Mark Ragan
Pinnacle Publishing
Group Publisher: Michael King A Division of Lawrence Ragan Communications, Inc.
Executive Editor: Farion Grove 316 N. Michigan Ave., Suite 300
Chicago, IL 60601
Questions?
POSTMASTER: Send address changes to Lawrence Ragan Communications, Inc., 316
Customer Service: N. Michigan Ave., Suite 300, Chicago, IL 60601.

Phone: 800-493-4867 x.4209 or 312-960-4100 Copyright © 2004 by Lawrence Ragan Communications, Inc. All rights reserved. No part
Fax: 312-960-4106 of this periodical may be used or reproduced in any fashion whatsoever (except in the
Email: PinPub@Ragan.com case of brief quotations embodied in critical articles and reviews) without the prior
written consent of Lawrence Ragan Communications, Inc. Printed in the United States
of America.
Advertising: RogerS@Ragan.com
Oracle, Oracle 8i, Oracle 9i, PL/SQL, and SQL*Plus are trademarks or registered trademarks of
Editorial: FarionG@Ragan.com Oracle Corporation. Other brand and product names are trademarks or registered trademarks
of their respective holders. Oracle Professional is an independent publication not affiliated
Pinnacle Web Site: www.pinnaclepublishing.com with Oracle Corporation. Oracle Corporation is not responsible in any way for the editorial
policy or other contents of the publication.

Subscription rates This publication is intended as a general guide. It covers a highly technical and complex
subject and should not be used for making decisions concerning specific products or
applications. This publication is sold as is, without warranty of any kind, either express or
United States: One year (12 issues): $199; two years (24 issues): $348 implied, respecting the contents of this publication, including but not limited to implied
Other:* One year: $229; two years: $408 warranties for the publication, performance, quality, merchantability, or fitness for any particular
purpose. Lawrence Ragan Communications, Inc., shall not be liable to the purchaser or any
Single issue rate: other person or entity with respect to any liability, loss, or damage caused or alleged to be
caused directly or indirectly by this publication. Articles published in Oracle Professional
$27.50 ($32.50 outside United States)* reflect the views of their authors; they may or may not reflect the view of Lawrence Ragan
Communications, Inc. Inclusion of advertising inserts does not constitute an endorsement by
* Funds must be in U.S. currency. Lawrence Ragan Communications, Inc., or Oracle Professional.

16 Oracle Professional March 2004 www.pinnaclepublishing.com

You might also like