Vous êtes sur la page 1sur 120

Inheritance in Database

Oracle9i's Support for Object Type Inheritance

With the release of Oracle8 (my, does that seem like a long time ago), Oracle became an object-relational
database. It supports both a traditional, relational model and one based on object types (known in other
languages as classes). Now, it's entirely possible that you did know that because the Oracle8 (and Oracle8i)
implementation of these object types or classes was woefully inadequate. In particular, one of the main
drawbacks with the first iteration of Oracle's object implementation was its lack of support for "inheritance."
Inheritance refers to the ability of an object type to inherit attributes and methods from previously defined
object types. As a result, very few development shops ever used object types.

That's all about to change, though. With Oracle9i, Oracle has fixed one of the most glaring problems with
object types by adding support for inheritance.

Quick Review of Object Types

Object types consist of a combination of attributes (think "column" from relational tables) and methods
(think "procedures" and "functions" from packages). Members are also commonly referred to as methods.
To use an object type, you instantiate a specific object from the type. Here's a very simple example of such a
process:

1. Define an object-type specification. A person object type has three attributes: name, weight, and date
of birth (defined using the new TIMESTAMP datatype of Oracle9i). It has a single member, a function
that returns the age of a person (relying on the new INTERVAL datatype of Oracle9i).
2. CREATE OR REPLACE TYPE person_ot
3. IS OBJECT
4. (
5. NAME VARCHAR2 (100),
6. weight NUMBER,
7. dob TIMESTAMP (3),
8.
9. MEMBER FUNCTION age
10. RETURN INTERVAL YEAR TO MONTH
);

11. Define an object-type body that contains the implementation of the object-type members.
12. CREATE OR REPLACE TYPE BODY person_ot
13. IS
14. MEMBER FUNCTION age
15. RETURN INTERVAL YEAR TO MONTH
16. IS
17. retval INTERVAL YEAR TO MONTH;
18. BEGIN
19. RETURN (SYSDATE - SELF.dob)
20. YEAR TO MONTH;
21. END;
END;

22. Instantiate an object and run some code: Notice that I call a constructor function (same name as the
object type) to initialize the object. Then, if Steven is more than 18 years old, it's time for him to get
a job.
23. DECLARE
24. steven person_ot :=
25. person_ot (
26. 'Steven', 175, '23-SEP-1958');
1
27. poppa person_ot :=
28. person_ot (
29. 'Sheldon', 200, '07-NOV-1929');
30. BEGIN
31. IF steven.age >
32. INTERVAL '18' YEAR
33. THEN
34. DBMS_OUTPUT.PUT_LINE (
35. 'Time for ' || steven.name ||
36. ' to get a job!');
37. END IF;
END;

Notice that I was also able to instantiate or declare two persons from the same object type: Steven and his
father, Sheldon. This is one of the key advantages of object types over packages: packages are static chunks
of code. Object types are templates for individual objects to which the members are applied.

What Is Inheritance?

Inheritance is well understood in the real world. When a person dies, his or her worldly assets can be passed
on to or inherited by other people, usually the children of that person. Inheritance in the virtual world of
software programming is much more benign. No death is required to inherit assets. Instead, I can define a
hierarchy of object types (similar to the genealogy of a family tree), in which there are supertypes
(ancestors, more general types) and subtypes (descendents, more specific types).

With inheritance, subtypes inherit all of the attributes and methods from their supertypes (and not just the
immediate subtype, but any subtype or ancestor in the hierarchy). What's the big deal about this? Inheritance
allows you to implement business logic at low levels in the hierarchy and then make them automatically
available in all object types derived from those supertypes. You don't have to code that business rule
multiple times to make it available in the different object types in the hierarchy.

Consider the following hierarchy, where I define a root object type of living thing. From it, I have two
subtypes of person and fish. A person, in turn, can be a citizen corporation (legal precedent in this country
has granted many of the rights of persons to corporations) or employee. A subtype of citizen is a war
criminal. There are two kinds of employees: salaried and hourly. There are three kinds of salaried
employees: management, administrative, and professional.

Why would I bother to define such a hierarchy? Regardless of the type of person I'm working with (citizen,
employee, war criminal), they all share common properties, such as their age and name. But there are also
specific characteristics of a citizen that aren't shared with all persons, such as nationality. On the employee
side of things, I identify management as a separate type of employee, since they have a very different
compensation model and different types of responsibilities.

One other powerful aspect of the object-type hierarchy is that it's active. Subtypes automatically reflect any
changes made to attributes or methods in any parent.

2
Features of Inheritance in Oracle9i :

Here are some of the ways you can take advantage of inheritance with object types:

• A subtype can be defined from a supertype either directly or indirectly, through multiple levels of
other subtypes.
• Oracle supports only single inheritance, which means that a subtype can be derived only from a
single supertype. A supertype can have more than one sibling subtype (all subtypes of the same
supertype), but a subtype can have no more than one direct parent supertype.
• Subtypes inherit all attributes and members from all of their supertypes.
• Subtypes can define new attributes and methods. These new elements can be in addition to the
inherited ones. They can also override or replace existing methods.
• When you call an object-type method, PL/SQL automatically executes the appropriate version of the
method, based on the type of the object. This is known as dynamic dispatch or dynamic
polymorphism.
• You can define an object type (and even an individual member in an object type) to be FINAL, which
means that you can't define a subtype from that type (override the member in a subtype). If an object
type is NOT FINAL, it can be a supertype of another object type.
• You can define an object type as NOT INSTANTIABLE, which means that you won't be able to
instantiate objects from the type. Such an object type can only act as a supertype for other types.
• You can declare an individual method as NOT INSTANTIABLE, which means that it exists only as a
"template" for subtype implementations. You specify, in other words, only the header of the method
without providing an implementation.

There are many other nuances in the ways that you can define and use object types, but these concepts will
be enough for a single article.

Defining an Object Hierarchy in PL/SQL

Let's take a look at how you set up a hierarchy with Oracle object types. Here's my definition of the root
living thing entity:

CREATE OR REPLACE TYPE living_thing_ot


IS OBJECT (
species VARCHAR2 (100),

NOT INSTANTIABLE MEMBER


PROCEDURE showpoliticalpower
)
NOT INSTANTIABLE
NOT FINAL;
/

All of my other objects are subtypes of this single object type. It's made up of a single attribute, species, and
a single member, the showpoliticalpower method. Here are some of the special characteristics of this
object type:

• It's NOT INSTANTIABLE, which means that I can't declare an object based on this type. If I try to, as
shown here, I'll get an error:
• /* Instantiate the un-instantiable */
• DECLARE
• squirrel living_thing_ot

3
• := living_thing_ot ();
• BEGIN
• IF squirrel.dob < SYSDATE
• THEN
• DBMS_OUTPUT.put_line (
• 'Senior citizen'
• );
• END IF;
• END;
• /

• ERROR:
• PLS-00713: attempting to instantiate a type that is NOT
INSTANTIABLE

The living_thing_ot object type can only be used as the starting point for other object types.

• It's NOT FINAL, which means that I can declare subtypes from the object type. Note that the
combination of NOT INSTANTIABLE and FINAL would result in a thoroughly useless object type.
• The showpoliticalpower method is defined as NOT INSTANTIABLE. This means that I won't
provide an implementation of this procedure in the living_thing_ot type (There is, in fact, no
body for living_thing_ot). Instead, I'm requiring that any object type declared as a subtype of
living_thing_ot must provide its own implementation of the procedure. In Java, this is called an
abstract method, and living_thing_ot would be called an abstract class. I like that terminology
and will use it in the rest of this article.

So let's see how I might take advantage of the living_thing_ot object type. Listing 1 shows the
specification for the person type.

Listing 1. Specification of the person type.

CREATE OR REPLACE TYPE person_ot UNDER living_thing_ot


(
name VARCHAR2 (100),
weight NUMBER,
dob TIMESTAMP (3),

-- New methods in object type


MEMBER PROCEDURE show,

MEMBER FUNCTION age RETURN INTERVAL YEAR TO MONTH,

FINAL MEMBER PROCEDURE when_crime_committed,

MEMBER PROCEDURE showpunishment,

-- Provide overriding implementation of abstract method.


OVERRIDING MEMBER PROCEDURE showpoliticalpower
)
INSTANTIABLE
NOT FINAL;
/

Some observations about the person type:

4
• Instead of specifying the type AS OBJECT, I instead use the UNDER clause to indicate that person_ot
is a subtype of living_thing_ot.
• Any object declared from this type has a total of four attributes: the species attribute from
living_thing_ot, plus the three attributes of person (name, weight, and DOB- date of birth).
• This type adds four new methods (show, age, when_crime_committed, and showpunishment) and
also overrides the abstract (unimplemented) method of the same name in living_thing_ot.
• The person type is declared as INSTANTIABLE and NOT FINAL. This means that I can declare objects
based on this object type and I can also define subtypes of person_ot.
• The when_crime_committed method is declared as FINAL. This means that if I define a subtype of
person_ot, I'm NOT allowed to override when_crime_committed with a new, more specific
definition. If I try to do so, I will raise an exception as shown here:
• CREATE OR REPLACE TYPE immigrant_ot
• under person_ot (
• from_nation VARCHAR2(200),

• OVERRIDING MEMBER PROCEDURE when_crime_committed
• )
• ;
• /
• ERROR:
PLS-00637: FINAL method cannot be overridden or hidden

By marking a method as FINAL, I make sure that whenever that method is executed for an object in
the hierarchy, it's always executing the code in which it was finalized.

The FINAL status of an object type can be modified with the ALTER TYPE statement. Suppose, for
example, I want to change the company type from FINAL to NOT FINAL. I can issue this statement:

ALTER TYPE company_ot NOT FINAL;

You can also change a type from NOT FINAL to FINAL with the ALTER TYPE command, but only if
the target type has no subtypes.

Implementing the Object Type Body:

Let's now take a look at the body of the person type, shown in Listing 2.

Listing 2. Body of person_ot object type.

CREATE OR REPLACE TYPE BODY person_ot


IS
MEMBER PROCEDURE show
IS
BEGIN
DBMS_OUTPUT.PUT_LINE ('Person named ' || SELF.name ||
' weighs ' || SELF.weight ||
' and was born on ' || SELF.dob);
END;

-- My wife has been reading Milton's Paradise Lost, so:


MEMBER PROCEDURE showpunishment
IS
BEGIN
DBMS_OUTPUT.PUT_LINE ('Leave Garden of Eden');
END;
5
FINAL MEMBER PROCEDURE when_crime_committed
IS
BEGIN
DBMS_OUTPUT.PUT_LINE ( '--Suppose '
|| name
|| ' Was Convicted of a Crime.--');
DBMS_OUTPUT.PUT_LINE (' ');
show;
DBMS_OUTPUT.PUT_LINE (' ');
DBMS_OUTPUT.PUT_LINE ('Political power?');
showPoliticalPower;
DBMS_OUTPUT.PUT_LINE (' ');
DBMS_OUTPUT.PUT_LINE ('Punishment?');
showpunishment;
END;

FINAL MEMBER FUNCTION age RETURN INTERVAL YEAR TO MONTH


IS
retval INTERVAL DAY TO SECOND;
BEGIN
RETURN (SYSDATE - SELF.dob) YEAR TO MONTH;
END;

OVERRIDING MEMBER PROCEDURE showpoliticalpower


IS
BEGIN
DBMS_OUTPUT.PUT_LINE ('The existence of a soul');
END;
END;
/

From a programming standpoint, there isn't much to note in the person_ot body. It mostly displays
information (show displays the characteristics of a person, showpunishment shows the punishment a person
receives when he or she commits a crime, and so on). The FINAL when_crime_committed calls a number
of other programs (show, showpoliticalpower, showpunishment) and will figure prominently later in our
analysis of the way PL/SQL determines which method in the hierarchy to execute.

The age function should catch your attention, as it makes use of the new INTERVAL datatype to compute the
difference between two dates. You can define two types of INTERVALS:

• DAY TO SECOND: Represent the precise difference between two datetime values.
• YEAR TO MONTH: Calculate the difference between two datetime values, where the only significant
portions are the year and month.

I'll talk about these new datatypes in more detail in a future article.

Since person_ot is a subtype of living_thing_ot, when I declare a person object, I must provide a total
of four values in the constructor. This is shown here:

DECLARE
steven person_ot :=
person_ot (
'HUMAN',
'Steven',
175,
'23-SEP-1958');
BEGIN
steven.weight := 170;
6
steven.showPoliticalPower;
END;

I provide a value for the living_thing_ot attribute, species and my three person- specific attributes. I can
then directly address any of the attributes and call my methods.

Reflections on Inheritance

The ability to inherit methods and attributes from supertypes lends much power to object types, but it also
introduces a higher level of complexity and dependency. The reason for this is that inheritance isn't a one-
time affair in object types.

Any changes made after the initial definition of the hierarchy are reflected in the subtype as well. Unless a
subtype overrides an inherited method, it always contains the same set of attributes and methods that are in
the accumulation of supertypes in the hierarchy, plus any attributes and methods that the subtype
contributes.

From this perspective, it's important to keep in mind that a subtype isn't a different type from any of its
supertypes. Instead, it's a more specific kind of that type. If, for example, the characteristics of the employee
object-type change, then the definition of the salaried employee changes as well.

This live connection means that you can make a change deep in the hierarchy that automatically affects
many object types and the tables and code built around those types. That may be just what you want, but it
also means that as you change one level in the hierarchy you need to think through the answers to such
questions as:

• If you're adding a new method, do you want to allow subtypes to redefine that method? Should all
subtypes be required to use the method unchanged?
• If the type or its subtypes are referenced in the definition of an object-relational table, do you want it
to be available through all DML operations or should access be restricted?

Extending the Hierarchy

There's no limit to the number of levels in one's object-type hierarchy. With the hierarchy described earlier
in this article, for example, I'd define my objects as follows (header lines only):

CREATE TYPE living_thing_ot IS OBJECT...


CREATE TYPE person_ot UNDER living_thing_ot...
CREATE TYPE citizen_ot UNDER person_ot...
CREATE TYPE war_criminal_ot UNDER citizen_ot...
CREATE TYPE corporation_ot UNDER person_ot...
CREATE TYPE employee_ot UNDER person_ot...
CREATE TYPE salaried_emp_ot UNDER employee_ot...
CREATE TYPE hourly_emp_ot UNDER employee_ot...
CREATE TYPE manager_ot UNDER salaried_emp_ot...
CREATE TYPE admin_ot UNDER salaried_emp_ot...
CREATE TYPE professional_ot UNDER salaried_emp_ot...

At each stage of the hierarchy, you need to decide:

• Should my object type be INSTANTIABLE or NOT INSTANTIABLE? Will this object type, in other
words, be used solely as a supertype of specialized subtypes?
• Should my object type be FINAL or NOT FINAL? Can this object type be used as a supertype of more
specialized subtypes? Or is it the "end of the line" in the hierarchy?
7
And then within each object type, you should examine your different methods and decide whether you want
them to be INSTANTIABLE (which means you can override them in a subtype) or FINAL (they can't be
overridden).

If you know that you're going to override a method in every subtype because it's defined differently at every
level, there isn't any point in providing an implementation in the supertype. So you declare the method as
INSTANTIABLE and compel all subtypes to implement the method.

The INSTANTIABLE status of an object type can be modified with the ALTER TYPE statement. Suppose, for
example, I want to change the company type from NOT INSTANTIABLE to INSTANTIABLE. I can issue this
statement:

ALTER TYPE company_ot INSTANTIABLE;

You can also change a type from INSTANTIABLE to NOT INSTANTIABLE with the ALTER TYPE command, but
only if the target type has no columns, views, tables, or instances that reference that type, either directly, or
indirectly through another type or subtype.

Options for Method Definition:

You can define or make available methods in object types in the following ways:

• Inherit a method: If the method is defined in any supertype in the hierarchy, that method is
automatically available for use with the current type. You don't need to write any additional code in
the object-type definition.
• Create a new method: You can always create an entirely new method in an object type, one that
doesn't appear in any supertype. You can, by the way, create STATIC methods that are associated
with the object type and not with any object instantiated from the object type.
• Overload a supertype method: You can create a method that has the same name as a method in a
supertype, but has either a distinct parameter list or a different method type (function vs. procedure,
for example). This constitutes an overloading (or static polymorphism) of the supertype method in
the subtype.
• Override a supertype method: You can also replace or substitute a supertype method in your
subtype by specifying OVERRIDING MEMBER in the header definition.

Overloading has, of course, been available for years in PL/SQL. You can overload multiple programs with
the same name in packages (the most common location for overloading) and any declaration section in a
block.

The interesting aspect of overloading in object types is that you might be overloading a method from a
supertype. In this case, you can't actually tell by looking at the subtype definition that what you've done is
an overloading. It's the only program with that name present. You have to be able to examine the entire
hierarchy to discover the overloading. Consider the object type for corporation shown in Listing 3.

Listing 3. Object type for corporation.

CREATE OR REPLACE TYPE corporation_ot


UNDER person_ot (

CEOcompensation NUMBER,
layoffs NUMBER,

MEMBER procedure maximizeProfits,


8
-- Show a corporation
OVERRIDING MEMBER procedure show ,

-- override of generic method.


OVERRIDING MEMBER procedure showPunishment,

-- implementation of non-instantiable method


OVERRIDING MEMBER procedure showPoliticalPower,

MEMBER FUNCTION age (merger_date_in IN DATE)


RETURN INTERVAL YEAR TO MONTH
)
INSTANTIABLE
NOT FINAL;
/

I've added an overloading of the age function (the person_ot contains the "original" age program). For
corporations, I allow you to specify a merger date. If NOT NULL, then the computation of the age of the
corporation changes from the default person age function. Here's a comparison of the two implementations,
first the person age function:

MEMBER FUNCTION age


RETURN INTERVAL YEAR TO MONTH
IS
retval INTERVAL YEAR TO MONTH;
BEGIN
RETURN (SYSDATE - SELF.dob)
YEAR TO MONTH;
END;

and now the corporation's age function:

MEMBER FUNCTION age (


merger_date_in IN DATE)
RETURN INTERVAL YEAR TO MONTH
IS
retval INTERVAL YEAR TO MONTH;
BEGIN
IF merger_date_in > SELF.dob
THEN
RETURN (SYSDATE - merger_date_in)
YEAR TO MONTH;
ELSE
RETURN (SYSDATE - SELF.dob)
YEAR TO MONTH;
END IF;
END;

In other words, if a merger date is provided, use that to calculate the age of the corporation. Otherwise, rely
on the "date of birth" of the corporation, obtained from the person-defined attribute. I do not include the
OVERRRIDING keyword in my definition of this function since it isn't overriding the person.age function.
Instead, corporation.age offers a second, overloaded implementation.

Remember that in my hierarchy, person is a supertype of corporation. I've defined a method in person to
return the age of a person.

Interestingly, I can invoke either one of the age functions against a corporation object as shown here:

9
DECLARE
oracle corporation_ot :=
corporation_ot (
'Artificial', -- species
'Oracle Corporation',
NULL,
'01-JAN-1979',
NULL,
NULL
);
BEGIN
IF -- person.age
oracle.age
>
-- corporation.age
oracle.age (SYSDATE)
THEN
DBMS_OUTPUT.PUT_LINE (
'Impossible!');
END IF;
END;

A Closer Look at Method Overriding

When you override an inherited method, you define a method in a subtype of the same name and have that
method do something different from the supertype's version.

The compensation calculation for a manager will be different, for example, from that of other employees, so
a specialized program will be needed in that object-type definition.

When a subtype overrides a method, then instances of the subtype will invoke that method rather than the
overridden one. If the subtype itself has subtypes, those more specific types inherit the override of the
method instead of the version of the supertype.

You'll only successfully override a method if your subtype's OVERRIDING member definition contains
precisely the same signature (name, parameter list, RETURN clause if a function) as the supertype method. If
the signature is different, then you've defined an overloading rather than an overriding method.

Keep the following restrictions in mind when you're figuring out when and how to overload methods:

• You can override only methods that aren't declared to be final in the supertype.
• Order methods (methods used to provide a sense of order or sequence to objects instantiated from
the type) may be defined only in the root type of a type hierarchy. You can't override these methods.
• You can't define an overridden method as a static method (one that's associated with the type itself
and not with an object instantiated from the type).
• A member method in a subtype isn't permitted to override a static method in the supertype.
• If a method being overridden contains default values for parameters, then the overriding method
must provide the same default values for the same parameters. The header of the programs must be
entirely identical.

Polymorphism and Dynamic Method Dispatch

Polymorphism is the name given to a language's ability to choose from multiple methods of the same name
and execute the appropriate method. There are two kinds of polymorphism:

10
1. Static polymorphism: The decision about which method to execute is made at the time the code is
compiled. In PL/SQL, static polymorphism is also known as overloading.
2. Dynamic polymorphism: The decision about which method to execute is made at the time the code
is executed, at runtime. This is also known as dynamic method dispatch and is available for the first
time in PL/SQL with support for object-type inheritance.

Both types of polymorphism are very powerful features. Static polymorphism or overloading makes it easy
for us to create simple, intuitive APIs or application programmatic interfaces for developers to use. Rather
than having to remember 10 different names for "define a dynamic SQL column," for example, I can simply
call the DBMS_SQL.DEFINE_COLUMN procedure; the PL/SQL compiler automatically figures out which
program I need to run based on the parameter list.

Dynamic polymorphism is dramatically more useful and intriguing because it gives us so much more
flexibility in the way we manipulate our objects. Suppose my PL/SQL block invokes a method on a type.
The execution engine then uses the type of the object instance that invokes it to determine which
implementation of the method to use. There can be multiple implementations due to either overloading or
overriding. The call is then dispatched to that type's implementation for execution. This process of selecting
a method implementation is called dynamic method dispatch since it's done at runtime, not at compile time.

PL/SQL dispatches a method call to the "nearest" implementation, working up the inheritance hierarchy
from the current or specified type to its supertypes, if any.

An Example of Dynamic Polymorphism:

Let's take a look at how I can take advantage of dynamic polymorphism with my hierarchy of different types
of persons. I need to write a program to show what happens when different types of persons (which, in the
U.S., include corporations by legal precedent) commit a crime. I'll walk through the individual steps for
such a program.

First, I'll define my different types of persons:

1. A pre-citizenship person, Eve:


2. eve person_ot
3. := person_ot (
4. 'Human',
5. 'Eve',
6. 175 /* Rubanesque */,
7. NULL
);

8. A U.S. citizen who's currently on death row for murder, but is believed by many to be innocent of
the crime (or at least to have received a grossly unfair trial). Notice that I add two additional
attributes for a citizen, nation, and political preference.
9. ondeathrow citizen_ot
10. := citizen_ot (
11. 'Human',
12. 'Mumia Abul Jamal',
13. 150,
14. NULL,
15. 'USA',
16. 'Radical'
);

11
17. A very scary company, the likes of which we're likely to see in the next decade or so with the way
mergers and acquisitions are proceeding these days:
18. theglobalmonster corporation_ot
19. := corporation_ot (
20. 'Inhuman',
21. 'Northrup-Ford-Mattel-Yahoo-ATT',
22. NULL,
23. SYSDATE,
24. 5000000,
25. 50000000
);

26. An even scarier human being, defined as a war criminal for his direction of the carpet- bombing of
Vietnam and Cambodia:
27. wiseman warcriminal_ot
28. := warcriminal_ot (
29. 'Human?',
30. 'Henry Kissinger',
31. 175,
32. NULL,
33. 'USA',
34. 'Above the law',
35. 1000000,
36. 'Vietnam and Cambodia'
);

I've now defined four different objects, instantiated from different types in the person hierarchy: person,
citizen, corporation, and war criminal. Now for the really interesting stuff. I want to define an array or
list that contains all of these objects and then manipulate the contents of that array. Since all of the objects
derive from the person supertype, I can declare a nested table of persons and populate that array with either
person objects or objects declared from subtypes of person:

TYPE bighappyfamily_nt IS TABLE OF person_ot;

bighappyfamily bighappyfamily_nt
:= bighappyfamily_nt (
eve,
ondeathrow,
theglobalmonster,
wiseman
);

I've now completed my declarations. On to my executable section. What I need to do is show what happens
to each "person" when he or she commits a crime. What political power do they have and what is their
punishment? To do that I can simply execute the following loop:

BEGIN
FOR persindx IN
bighappyfamily.FIRST .. bighappyfamily.LAST
LOOP
DBMS_OUTPUT.PUT_LINE (' ');
bighappyfamily (persindx).when_crime_committed;
END LOOP;
END;

The output from this loop's execution is shown in Listing 4. It all looks perfectly appropriate. For each
different type of person, the information displayed is specific to that object. Yet quite a lot is going on
behind the scenes to make that happen. Let's take a closer took.

12
Listing 4. Output from displaying the contents of the bighappyfamily array.

Suppose Eve Was Convicted of a Crime.--


Person named Eve weighs 175 and was born on
Political power?
The existence of a soul
Punishment?
Leave Garden of Eden

--Suppose Mumia Abul Jamal Was Convicted of a Crime.--


Citizen Mumia Abul Jamal is a citizen of USA whose politics are Radical
Political power?
One vote
Punishment?
Go to jail, do not pass Go, do not survive.

--Suppose Northrup-Ford-Mattel-Yahoo-ATT Was Convicted of a Crime.--


Corporation Northrup-Ford-Mattel-Yahoo-ATT is a trans-national entity with
50000000 laid-off employees, paying its Chief Executive Officer 5000000
Political power?
Virtually unlimited
Punishment?
Increased political contributions.

--Suppose Henry Kissinger Was Convicted of a Crime.--


War criminal Henry Kissinger killed 1000000 in Vietnam and Cambodia
Political power?
Filling the vacuum.
Punishment?
Sometimes you win the Nobel Peace Prize.

My FOR loop iterates through every row of bighappyfamily. Recall that bighappyfamily is a nested table
of persons. Within the FOR loop, I invoke the when_crime_committed method for each element in the array.
The when_crime_committed method is declared as FINAL in the person object and implemented as follows:

FINAL MEMBER PROCEDURE when_crime_committed


IS
BEGIN
DBMS_OUTPUT.PUT_LINE ( '--Suppose '
|| name
|| ' Was Convicted of a Crime.--');
DBMS_OUTPUT.PUT_LINE (' ');
show;
DBMS_OUTPUT.PUT_LINE (' ');
DBMS_OUTPUT.PUT_LINE ('Political power?');
showPoliticalPower;
DBMS_OUTPUT.PUT_LINE (' ');
DBMS_OUTPUT.PUT_LINE ('Punishment?');
showpunishment;
END;

By defining the method as FINAL, I disallow any of the subtypes of person to override this program. Notice,
however, that when_crime_committed invokes three other methods: show, showPoliticalPower and
showPunishment. None of these methods, defined in person_ot, are defined as FINAL and, in fact, are
overridden in each of the object types citizen_ot, corporation_ot, and war_criminal_ot. Here's one
example of such an override, the citizen.showPoliticalPower method:

OVERRIDING
MEMBER PROCEDURE
showpoliticalpower
13
IS
BEGIN
DBMS_OUTPUT.PUT_LINE (
'One vote');
END;

So, at the time of compilation of the person_ot object type, references to the show, showPoliticalPower,
and showPunishment methods are all resolved to the person_ot methods. But when the
when_crime_committed method is actually executed by the PL/SQL runtime engine, dynamic method
dispatch results in a very different resolution.

When Oracle executes the following method, for example:

bighappyfamily(3).when_crime_committed

it identifies the object type as corporation_ot. Then it checks to see whether when_crime_committed is
defined as a method in corporation_ot. It's not and, in fact, is only defined in person_ot. So the runtime
engine then invokes person.when_crime_committed. As it runs that method, it encounters the
showPoliticalPower method. Once again, Oracle must identify which of the showPoliticalPower
methods to run. Even though it's currently executing a person_ot method, it hasn't forgotten where it came
from. It therefore starts its search through the hierarchy with the corporation_ot object type. And, in fact,
it finds that corporation_ot has implemented an overriding showPoliticalPower method. So it uses that
one and not the person_ot.show method.

There are two approaches to resolving invocations to methods, static and dynamic.

Dynamic polymorphism gives developers an incredible amount of flexibility. We can write generalized
programs that seemingly overlook variations in subtypes and levels in the hierarchy. We leave it to the
runtime engine to locate and execute the most appropriate definition of a method.

It's Time to Try Out Object Types

Prior to Oracle9i, there was no compelling reason to use object types unless you used Oracle Advanced
Queuing or some other Oracle utility that relied on object types. There were simply too many limitations to
its implementation. With Oracle9i and the support for inheritance, however, Oracle takes PL/SQL a big step
closer to a true object-oriented language.

Now that we can set up object-type hierarchies, it's worth revisiting object types to see how they can be used
to construct robust applications based on PL/SQL. We'll explore further nuances of inheritance in the next
article.

This article was originally published in the December 2001 issue of Oracle Professional. The material in
Feuerstein and Bryn Llewellyn's articles is based on Oracle Corporation white papers originally prepared
by Llewellyn for Oracle OpenWorld 2001 in San Francisco and OracleWorld Copenhagen in June 2002
and Oracle PL/SQL Programming, 3rd Edition.

Steven Feuerstein is considered one of the world's leading experts on the Oracle PL/SQL language.

14
Substituting and Converting Object Types in a Hierarchy
Oracle9i's PL-SQL Programming – 3rd Edition

Editor's note: In Part 2 in this series on new features in Oracle 9i, Steven Feuerstein, coauthor of Oracle
PL/SQL Programming, 3rd Edition, explores the advantages and flexibilities of object-type hierarchies by
examining substitutability and type conversion. (For a quick review of object types and how you can build
object-type hierarchies by taking advantage of inheritance, see Steven's first article, Inherit the Database:
Oracle9i's Support for Object Type Inheritance.) Beware, however, that by the time you've finished reading
this article, you may find yourself to be a bit hungry

In Part 1 in this series, I introduced one of Oracle9i's most significant enhancements to the SQL and
PL/SQL language: support for object-type inheritance. With inheritance, a subtype inherits all of the
attributes and methods from their supertypes, and not just the immediate subtype, but any subtype or
ancestor in the resulting object-type hierarchy. Inheritance allows you to implement business logic at low
levels in the hierarchy and then make them automatically available in all object types derived from those
supertypes. You don't have to code that business rule multiple times to make it available in the different
object types in the hierarchy.

Inheritance also allows developers to take advantage of "dynamic polymorphism," which means that at the
time your code is run, Oracle identifies and executes the "nearest" or most specific method in the object
hierarchy that corresponds to your method invocation.

What Is Substitutability?

When you define a type hierarchy, you start with a root type, from which all other subtypes are derived. In
the Java language, for example, all classes (roughly equivalent to Oracle "object types") derive from the root
Object class. In Oracle, where the object model has been layered on top of a relational database, there's no
built-in and global hierarchy. So every time you work with object types, you define your own root.

For this article, we'll work with a very simple type hierarchy. In this hierarchy, the food type, food_t, is the
root. The dessert type, dessert_t, is a subtype of food, and cake, represented by cake_t, is a further
subtype of dessert_t. Here are the definitions of these types (showing attributes only, and no associated
PL/SQL methods):

CREATE TYPE food_t AS OBJECT (


name VARCHAR2(100),
food_group VARCHAR2 (100),
grown_in VARCHAR2 (100)
)
NOT FINAL
;
/

CREATE TYPE dessert_t UNDER food_t (


contains_chocolate CHAR(1),
year_created NUMBER(4)
)
NOT FINAL
;
/

CREATE TYPE cake_t UNDER dessert_t (


diameter NUMBER,
inscription VARCHAR2(200)
)

15
;
/

Each type has its own type-specific attributes. Each subtype, don't forget, also inherits the attributes of its
supertype(s). So if I want to instantiate an object of type cake, I need to supply a total of seven attributes, as
shown here:

DECLARE
my_favorite cake_t
:= cake_t (
'Marzepan Delight',
'CARBOHYDRATE',
'Swedish Bakery',
'N',
1634,
8,
'Happy Birthday!'
);
BEGIN
DBMS_OUTPUT.put_line (my_favorite.NAME);
DBMS_OUTPUT.put_line (my_favorite.inscription);
END;
/

Notice that I reference and display an attribute from the base food type and an attribute from the cake
subtype. They're all equally available to me in an object instantiated from cake.

The way to think about such a hierarchy is this: A cake is a type of dessert, which in turn is a type of food.
But not all desserts are cake, and not all foods are dessert (putting aside the obvious cultural complications
here, for instance, something that's not considered a dessert in the United States may well be considered one
in, say, Ecuador). Any characteristic of food applies to cakes, but not all characteristics of a cake will
necessarily make sense for a food, such as a cucumber.

Once you've defined your hierarchy, you'll want to work with and make changes to the types in that
hierarchy. In some cases, you may wish to select and view all types across the entire hierarchy. In other
cases, you may only want to update a specific level in the hierarchy, such as all cakes. And then there are
situations where you'll want to work with, say, all desserts that are not cakes. And that leads us directly to
the concept of substitutability.

A supertype is substitutable if one of its subtypes can substitute for it in some location, such as in a column
or in a program variable, in which the declared type is the supertype (and not that particular supertype).

Suppose I create a table of objects of type food_t:

CREATE TABLE sustenance OF food_t;

I can then insert rows into this table as follows:

BEGIN
INSERT INTO sustenance
VALUES (food_t (
'Brussel Sprouts',
'VEGETABLE',
'farm'
)
);

16
INSERT INTO sustenance
VALUES (dessert_t (
'Jello',
'PROTEIN',
'bowl',
'N',
1887
)
);

INSERT INTO sustenance


VALUES (cake_t (
'Marzepan Delight',
'CARBOHYDRATE',
'bakery',
'N',
1634,
8,
'Happy Birthday!'
)
);
END;
/

After running this code, my table will contain three rows: a food object, a dessert object, and a cake object.
In this block of code, I've substituted my subtypes for the supertype in two of the inserts. This doesn't raise
an error because cakes and desserts are types of food.

I can execute a query against this table in SQL*Plus and it shows me all of (and only) the food type
attributes of the three rows.

SQL> SELECT * FROM sustenance;

NAME FOOD_GROUP GROWN_IN


------------------------- ------------------ -------------
Brussel Sprouts VEGETABLE farm
Jello PROTEIN bowl
Marzepan Delight CARBOHYDRATE bakery

I can also take advantage of substitutability inside PL/SQL blocks. In the following code, I declare a food,
but initialize it with a dessert, a more specific type of food.

DECLARE
mmm_good food_t :=
dessert_t (
'Super Brownie',
'CARBOHYDRATE',
'my oven', 'Y', 1994);
BEGIN
DBMS_OUTPUT.PUT_LINE (
mmm_good.name);
END;
/

And here's an example of substitutability within a PL/SQL collection:

DECLARE
TYPE foodstuffs_nt IS TABLE OF food_t;

fridge_contents foodstuffs_nt := (
17
food_t (
'Eggs benedict', 'PROTEIN', 'Farm'),
dessert_t (
'Strawberries and cream',
'FRUIT', 'Backyard', 'N', 2001),
cake_t (
'Chocolate Supreme', 'CARBOHYDRATE',
'Kitchen', 'Y', 2001,
8, 'Happy Birthday, Veva'));

BEGIN
FOR indx IN
fridge_contents.FIRST ..
fridge_contents.LAST
LOOP
DBMS_OUTPUT.PUT_LINE (
fridge_contents(indx).name);
END LOOP;
END;
/

Now let's take a look at an INSERT that doesn't work. Suppose I create an object table of desserts:

CREATE TABLE sweet_nothings OF dessert_t;


/

If I then try to insert an object of type food, Oracle will raise an error, as shown here:

BEGIN
INSERT INTO sweet_nothings
VALUES (dessert_t (
'Jello',
'PROTEIN',
'bowl',
'N',
1887
)
);

INSERT INTO sweet_nothings


VALUES (food_t (
'Brussel Sprouts',
'VEGETABLE',
'farm'
)
);
END;
/

PL/SQL: ORA-00932: inconsistent datatypes

I receive this error because while any dessert is a food, not any food is a dessert. I can't insert an object of
type food_t into a column of type dessert_t.

Now consider the same scenario, PL/SQL-wise. I declare an object in my program of type food and
initialize it with a dessert. Notice that I specify Y or "Yes, it sure does!" for the contains_chocolate
attribute. If I try to specify that dessert-specific attribute in my code, however, PL/SQL gives me an error:

SQL> DECLARE
2 -- Again, I substitute, but this time

18
3 -- I also try to access my cake attribute.
4 mmm_good food_t :=
5 dessert_t (
6 'Super Brownie',
7 'CARBOHYDRATE',
8 'my oven', 'Y', 1994);
9 BEGIN
10 DBMS_OUTPUT.PUT_LINE (
11 mmm_good.contains_chocolate);
12 END;
13 /
mmm_good.contains_chocolate);
*
ERROR at line 11:
ORA-06550: line 11, column 16:
PLS-00302: component 'CONTAINS_CHOCOLATE' must be declared

As one would expect, types are, as a rule, substitutable (that is, you can substitute a subtype for its
supertype). You can take advantage of substitutability with object types defined as attributes of object types,
columns of tables, or rows in tables and collections.

Oracle doesn't provide any way, within the definition of an object type itself, to turn off substitutability. Any
object type is theoretically or potentially substitutable. Oracle does, on the other hand, offer a way to
constrain substitutability and even makes it impossible, when you define usages of that object type.

How to Turn Off Substitutability

Why would I want to constrain or limit substitutability? I may want my table to contain objects of a specific
type within my hierarchy, not any subtypes. To accommodate this need, Oracle lets you turn off all
substitutability on a column or attribute, including embedded attributes and collections nested to any level.
You do this by using the following clause:

NOT SUBSTITUTABLE AT ALL LEVELS

Suppose that I create another table to define meals I serve on a given day. Here's the table:

CREATE TABLE meal (


served_on DATE,
appetizer food_t,
main_course food_t,
dessert dessert_t
)
COLUMN appetizer NOT SUBSTITUTABLE AT ALL LEVELS
;

I use the NOT SUBSTITUTABLE clause to indicate that you can't use a subtype of food when providing a value
for the appetizer column. I don't want anyone sneaking in a dessert for the appetizer.

Now consider the code in Example 1. I try to insert two different meals. In the first INSERT, I supply an
object of type food_t for appetizer, which is fine. In my second insert, I try to pass off a dessert as an
appetizer. The result, when executed, is the following error:

ERROR at line 1:
ORA-00932: inconsistent datatypes

Example 1. An attempt to define two meals.

19
BEGIN
INSERT INTO meal VALUES (
SYSDATE,
food_t ('Shrimp cocktail', 'PROTEIN', 'Ocean'),
food_t ('Eggs benedict', 'PROTEIN', 'Farm'),
dessert_t ('Strawberries and cream', 'FRUIT', 'Backyard', 'N', 2001));

INSERT INTO meal VALUES (


SYSDATE + 1,
dessert_t ('Strawberries and cream', 'FRUIT', 'Backyard', 'N', 2001),
food_t ('Eggs benedict', 'PROTEIN', 'Farm'),
cake_t ('Apple Pie', 'FRUIT', 'Baker''s Square', 'N', 2001, 8, NULL));
END;

I can also apply the NOT SUBSITUTABLE clause to an entire object table. Example 2 demonstrates this
capability. I create a table of food_t objects called brunches. I can then successfully insert an object of
type food_t, but get the same "inconsistent datatypes" error when I try to put a dessert into this table.

Example 2. Constraining substitutability in an object table.

SQL> CREATE TABLE brunches OF food_t NOT SUBSTITUTABLE AT ALL LEVELS;

Table created.

SQL>
SQL> INSERT INTO brunches VALUES (
2 food_t ('Eggs benedict', 'PROTEIN', 'Farm'));

1 row created.

SQL> INSERT INTO brunches VALUES (


2 dessert_t ('Strawberries and cream', 'FRUIT', 'Backyard', 'N', 2001));
dessert_t ('Strawberries and cream', 'FRUIT', 'Backyard', 'N', 2001))
*
ERROR at line 2:
ORA-00932: inconsistent datatypes

There are two things to remember about constraining substitutability:

• There's no mechanism to turn off substitutability for REF columns.


• A column must be a top-level column for the clause NOT SUBSTITUTABLE AT ALL LEVELS to be
applied to it. You can't apply the clause to an object-type attribute.

Constraining Substitutability to a Specific Subtype

So I can turn off all levels of substitutability, but what if I want to turn off all substitutability except for a
particular subtype? Suppose, for example, that I want to create a PL/SQL collection of desserts that can
contain only cakes. Or I want to set a rule in my meals table that all desserts must be cakes. Oracle offers the
IS OF clause for just this purpose. Here's a re-definition of the meals table that offers two different kinds of
substitutability constraints:

CREATE TABLE meal (


served_on DATE,
appetizer food_t,
main_course food_t,
dessert dessert_t
)
COLUMN appetizer NOT SUBSTITUTABLE AT ALL LEVELS,

20
COLUMN dessert IS OF (ONLY cake_t)
;

And now I'll only be able to add meals in which the dessert is defined as a cake. So the following INSERT is
rejected:

SQL> BEGIN
2 -- This will no longer work.
3 INSERT INTO meal VALUES (
4 SYSDATE,
5 food_t ('Shrimp cocktail',
6 'PROTEIN', 'Ocean'),
7 food_t ('Eggs benedict',
8 'PROTEIN', 'Farm'),
9 dessert_t ('Strawberries and cream',
10 'FRUIT', 'Backyard', 'N', 2001));
11 END;
12 /
BEGIN
*
ERROR at line 1:
ORA-00932: inconsistent datatypes

You can only use the IS OF type operator to constrain row and column objects to a single subtype, but not
several. You must also use the ONLY keyword, even though that's the only choice available now. You can
use either IS OF type or NOT SUBSTITUTABLE AT ALL LEVELS to constrain an object column, but you can't
use both for that same column. Clearly, as shown previously, you can apply those constraints to different
columns.

Widening and Narrowing Object Types

There are two very useful concepts to talk about when working with objects in a type hierarchy: widening
and narrowing, particularly in assigning an object of one type to a variable or column declared with a
different type in the hierarchy. Here are some definitions:

• Widening is an assignment in which the declared type of the source is more specific than the
declared type of the target. If I assign an object (or object-type instance, to be specific) of type
cake_t to a variable declared with type dessert_t, I've performed widening.
• Narrowing is an assignment in which the declared type of the source is more general than the
declared type of the target. If I assign an object of type dessert_t to a variable declared with type
cake_t, I've performed a narrowing operation.

Widening is actually native to Oracle's object-type hierarchies and its substitutability feature. Any cake is
also a dessert is also a food. So unless you specifically constrain substitutability, a subtype can be treated as,
stored as, and manipulated as any of its supertypes. You've already seen several examples of this process in
this article.

Let's examine how you accomplish narrowing, a more complicated step, in SQL and PL/SQL in Oracle9i.

Narrowing with TREAT

Oracle provides a special function called TREAT that allows you to perform narrowing operations. The TREAT
function explicitly changes the declared type of the source in an assignment to a more specialized target
type or subtype in the hierarchy.

21
To successfully narrow, you have to use TREAT. Without use of this function, you won't be able to reference
subtype-specific attributes and methods.

Here's the general syntax of this function:

TREAT (<object instance> AS <object type>)

where <object instance> is a column or collection row value that's of a particular supertype in an object
hierarchy, and <object type> is a subtype in that hierarchy.

Let's look at some examples of how to use TREAT. Suppose that I insert three rows into the meal table as
shown in Example 3. Notice that in the third row, I've passed in a dessert for a main course, one of my son's
favorite mealtime activities! I'm able to do this because I haven't constrained substitutability on the
main_course column.

Example 3. Populating the meal table.

BEGIN
-- Populate the meal table
INSERT INTO meal VALUES (
SYSDATE,
food_t ('Shrimp cocktail', 'PROTEIN', 'Ocean'),
food_t ('Eggs benedict', 'PROTEIN', 'Farm'),
dessert_t ('Strawberries and cream', 'FRUIT', 'Backyard', 'N', 2001));

INSERT INTO meal VALUES (


SYSDATE + 1,
food_t ('Shrimp cocktail', 'PROTEIN', 'Ocean'),
food_t ('Stir fry tofu', 'PROTEIN', 'Vat'),
cake_t ('Apple Pie', 'FRUIT', 'Baker''s Square', 'N', 2001, 8, NULL));

INSERT INTO meal VALUES (


SYSDATE + 1,
food_t ('Fried Calamari', 'PROTEIN', 'Ocean'),
-- Butter cookies for dinner? Yikes!
dessert_t ('Butter cookie', 'CARBOHYDRATE', 'Oven', 'N', 2001),
cake_t ('French Silk Pie', 'CARBOHYDRATE',
'Baker''s Square', 'Y', 2001, 6, 'To My Favorite Frenchman'));
END;
/

Suppose I want to see a list of all of the meals in which a dessert has been provided for the main course. I
use the TREAT operator in the WHERE clause as follows:

SELECT *
FROM meal
WHERE TREAT (main_course AS dessert_t) IS NOT NULL;

The TREAT function, which, by the way, in Oracle9i Release 1 can only be used in SQL statements (and not
in PL/SQL blocks), returns a NULL object type for any main courses that are not desserts.

Suppose I'd like to see whether or not any of my main courses contain chocolate. This attribute is specific to
desserts, so if I try to directly reference that attribute without TREAT, my query fails, as I show here:

SQL> SELECT main_course.contains_chocolate


2 FROM meal
3 WHERE TREAT (main_course AS dessert_t)
22
4 IS NOT NULL;
SELECT main_course.contains_chocolate
*
ERROR at line 1:
ORA-00904: invalid column name

Even though all of the main courses selected are actually desserts, there's no way for Oracle to know that; a
main_course column is declared as type food_t. So what should I do? Use the TREAT operator in the
SELECT list as well as the query. This query and the results are shown in Example 4.

Example 4. Using the TREAT operator.

SQL> SELECT TREAT (main_course AS dessert_t).contains_chocolate chocolatey


2 FROM meal
3 WHERE TREAT (main_course AS dessert_t) IS NOT NULL;

CHOCOLATEY
---------------
N

I can also use TREAT in DML operations, such as INSERTs and UPDATEs.

Suppose, for example, that I don't really want to allow you to add a row to the meal table in which a dessert
is offered as the main course. I can add a constraint to the table to stop this from happening, but I can also
remove any such definitions by using TREAT with UPDATE.

Just remember that we can't yet use TREAT outside of a SQL statement and directly in native PL/SQL code.
Perhaps they'll give us a PL/SQL TREAT in Oracle9i Release 2.

The Programming Trade-Offs of Type Hierarchies

Oracle's implementation of inheritance, without a doubt, greatly improves the usefulness and power of
object types in the PL/SQL language. Will this mean that many more PL/SQL developers will now take
advantage of object types and, in particular, these great new features? I have my doubts, and for two
reasons.

First, many developers and development groups are perfectly happy with the pure relational model. It gets
the job done, is straightforward, and is easily managed by the developers and DBAs. Sure, the object model
might offer some advantages, but probably not enough to justify the additional training costs and mental
shifts required.

Second, without a doubt, working with the object model involves writing more complex code. You have to
deal with constructors, and many special operators like TREAT, FINAL, SUBSTITUTABLE, and the like. It can
take quite a while to get proficient with the variations, and then you still end up writing code that's harder to
understand and maintain.

So by all means investigate what object-type hierarchies can do for you. You might find object types to be a
close fit and have major advantages. In that case, explore these features in depth and make sure to apply
them to their fullest. If you don't see a good fit, don't feel bad. Just stick with your relational tables and
simpler data structures. It's worked for 25 years, the anniversary that Oracle recently celebrated. It'll
probably be good for another dozen.

23
New Datatypes, New Possibilities
Oracle 9i has introduced a whole bunch of new datatypes that greatly expand the possibilities for powerful,
intuitive programming in the PL/SQL language. This article introduces many of these datatypes and offers
some examples of usage. Table 1 offers a quick review of many of the Oracle 9i datatypes.

Table 1. New Oracle 9i Datatypes


Name Description

TIMESTAMP
A new variation of DATE that's useful for storing very precise datetime (date or
timestamp) values, down to small fractions of seconds.
TIMESTAMP WITH A variation of TIMESTAMP that provides smart logic about time zones (previous
TIMEZONE support in Oracle was very weak).
TIMESTAMP WITH LOCAL
TIMEZONE A variation of TIMESTAMP that automatically uses/enforces the local time zone.

INTERVAL YEAR TO Used to represent the difference between two datetime values, where the only
MONTH significant portions are the year and month.
INTERVAL DAY TO Used to represent the precise difference between two datetime values, down to the
SECOND second or fraction of a second.
SYS.XMLTYPE Used to store, manipulate, and query XML documents natively in the database.
The root of an object type hierarchy that can be used to store URIs (Universal
Related Reading
SYS.URITYPE Resource Identifiers) to external Web pages and files, as well as to refer to data
within the database.

SYS.ANYTYPE
A generic or "Any" datatype that contains the description of any SQL type (scalar,
nested table, object type, and so forth).

SYS.ANYDATA
An instance of a given type. It contains data, plus a description of the type, and it
persists in the database.

SYS.ANYDATASET
A description of a given type, plus a set of instances of that type; it persists in the
database.

Working with Timestamps


Oracle PL/SQL
We all love the DATE datatype, but let's face it: it wasn't everything we always Programming,
wanted in a timestamp datatype. Namely, the DATE datatype: 3rd Edition
By
• Supported a timestamp only down to the nearest second. Perhaps when Steven Feuerstein
the Oracle database was first designed and released (in the early 1980s),
Table of Contents
that was good enough. But now we're on "Internet time," and fractions Index
of seconds are the cat's meow. Sample Chapter
• Offered almost no support for time zone manipulation. The NEW_TIME
function acted as though it would allow you to work with different time Read Online--Safari
zones, but it was just a stopgap. Search this book on
Safari:

Oracle has corrected these deficiencies in Oracle 9i by introducing the


TIMESTAMP datatype. With TIMESTAMP, you can specify a precision (up to nine

Code Fragments
only
24
digits) for fractions of seconds. And you can take advantage of smart, built-in time zone recognition,
manipulation, and arithmetic. Let's look at some examples.

Here's a declaration of a TIMESTAMP with a precision down to a thousandth of a second:

DECLARE
test_endpoint TIMESTAMP(3);
BEGIN
test_endpoint :=
'1999-06-22 ' ||
'07:48:53.275';

I assign a value to that checkout timestamp through an implicit conversion. This is very similar to the type
of code one might write to assign a value to a DATE variable, except that I can now also provide a fractional
value for the second (275/1000).

Of course, for the most part, we won't be assigning fractional components of seconds. Instead, such
information will be taken from system timestamp information or provided from externally-generated data
(from, say, a manufacturing assembly line).

Oracle provides a host of new built-in functions to obtain and convert timestamps, as I demonstrate in the
following script:

DECLARE
-- Grab the current timestamp,
-- restricting precision to
-- four digits
right_now TIMESTAMP (4) :=
CURRENT_TIMESTAMP;

-- Grab the current timestamp,


-- but preserve time zone info.
over_there TIMESTAMP (0)
WITH TIME ZONE:=
CURRENT_TIMESTAMP;

-- Use LOCAL TIME ZONE with


-- the timestamp
right_here TIMESTAMP (2)
WITH LOCAL TIME ZONE:=
CURRENT_TIMESTAMP;
BEGIN
-- Display the values
DBMS_OUTPUT.put_line (
SYSTIMESTAMP);
DBMS_OUTPUT.put_line (
CURRENT_TIMESTAMP);
DBMS_OUTPUT.put_line (
right_now);
DBMS_OUTPUT.put_line (
over_there);
DBMS_OUTPUT.put_line (
right_here);
END;

And here's the output displayed:

SYSTIMESTAMP
05-FEB-02 12.57.44.000000000 PM -08:00
25
CURRENT_TIMESTAMP
05-FEB-02 12.57.44.000000107 PM -08:00
TIMESTAMP (4)
05-FEB-02 12.57.44.0000 PM
TIMESTAMP (0) WITH TIME ZONE
05-FEB-02 12.57.44 PM -08:00
TIMESTAMP (2) WITH TIME ZONE
05-FEB-02 12.59.59.00 PM

Working with time zones can get very complicated, and Oracle documentation is still a bit on the skimpy
side. You'll need to make sure that your database has set a time zone, which isn't done by default. Here's the
kind of statement you'd execute (and then restart the database):

ALTER DATABASE SET time_zone = 'US/Central'

You can also set a time zone for a session as well, such as:

ALTER SESSION SET time_zone = 'US/Central'

You can also set the default time zone format used for conversion and display as follows:

ALTER SESSION
SET NLS_TIMESTAMP_TZ_FORMAT =
'DD-Mon-YYYY HH24:MI:SSXFF TZR TZD';

You can examine the full (and greatly expanded) set of Oracle-recognized time zones with the following
query:

SELECT DISTINCT tzname


FROM v$timezone_names;

Example 1 offers a procedure that you can use to set the time zone in your session and then display various
elements of the current time zone information.

Example 1. Set and Show Time Zone Information


CREATE OR REPLACE PROCEDURE tz_set_and_show (tz_in IN VARCHAR2 := null)
IS
BEGIN
IF tz_in IS NOT NULL
THEN
EXECUTE IMMEDIATE 'alter session set time_zone = '''
|| tz_in
|| '''';
END IF;

DBMS_OUTPUT.put_line ( 'SESSIONTIMEZONE = '


|| SESSIONTIMEZONE);
DBMS_OUTPUT.put_line ( 'CURRENT_TIMESTAMP = '
|| CURRENT_TIMESTAMP);
DBMS_OUTPUT.put_line ( 'LOCALTIMESTAMP = '
|| LOCALTIMESTAMP);
DBMS_OUTPUT.put_line (
'SYS_EXTRACT_UTC (LOCALTIMESTAMP) = '
|| sqlexpr ('SYS_EXTRACT_UTC (LOCALTIMESTAMP)')
);
END;
/

26
Working with Intervals

Use variables of type INTERVAL to store and manipulate deltas between two different dates or timestamps. In
the past, you would've treated this same information with a number, but then you'd have to interpret that
number in ways that greatly increased the possibility of error and greatly decreased the readability (and
therefore, maintainability) of your code.

Now, with INTERVALs, you can write very understandable code that allows for manipulation of timestamp
deltas in natural, intuitive ways.

Recognizing that there are roughly two "scales" of intervals with which humans concern themselves, you
can declare and use INTERVALs of type YEAR TO DAY and DAY TO SECOND.

With YEAR TO DAY INTERVALs, you can store and manipulate intervals of years and months. The syntax for
this interval is:

INTERVAL YEAR[(precision)] TO MONTH

where the precision can range from 0 to 4, with a default of 2. You don't get to specify a precision for
MONTH.

Use DAY TO SECOND INTERVALs to store and manipulate intervals of days, hours, minutes, and seconds.
With this interval type, you can set two levels of precision:

INTERVAL DAY[(leading_precision)]
TO
SECOND[(fractional_seconds_precision)]

The default values are 2 and 6, respectively. You must use integer literals in these declarations; you may not
use a variable or named constant.

Suppose I've created a person object type. I add a member procedure to calculate the age of a person. For
such a calculation, I really don't need to get too detailed; number of years and months is fine. So I define
this function as follows:

MEMBER FUNCTION age RETURN INTERVAL YEAR TO MONTH


IS
retval INTERVAL YEAR TO MONTH;

BEGIN
RETURN (SYSDATE - SELF.dob)
YEAR TO MONTH;

END;

Notice that I perform date arithmetic between today's date and the date of birth (dob) of the currently
instantiated object (SELF). I then express that delta as an INTERVAL.

Oracle is very flexible about allowing you to specify intervals, as shown in the following set of assignments.
In the following block, I assign my duration of 14 years, seven months working with Oracle technology (I
started with Oracle Corporation in August 1987 and lasted five years!) to an INTERVAL variable:

DECLARE
oracle_career

27
INTERVAL YEAR(2) TO MONTH;
BEGIN
-- Example of INTERVAL literal
oracle_career :=
INTERVAL '14-7' YEAR TO MONTH;

-- Implicit conversion from string


oracle_career := '14-7';

-- Assign year and month components


-- individually
oracle_career := INTERVAL '14' YEAR;
oracle_career := INTERVAL '7' MONTH;
END;

Working with XMLTypes

INTERVALs and TIMESTAMPs are interesting. XMLTypes are part of a dramatic transformation within the
Oracle database and technology. As you're probably aware, Oracle moved rapidly with the Oracle 8i release
to allow PL/SQL and Java database programmers to parse, manipulate, and store XML documents.

Oracle 9i takes a giant leap toward what Oracle is calling its "XDB," the XML DataBase, by offering a
native XML datatype, SYS.XMLTYPE. With this datatype, Oracle now allows us to perform SQL operations
on XML content and XML operations on SQL content. You can also apply standard XML functionality,
such as XPath, directly against data without the need to convert to CLOBs or other datatypes.

I can now, for example, create a database table with a column of type SYS.XMLTYPE, such as:

CREATE TABLE env_analysis (


company VARCHAR2(100),
site_visit_date DATE,
report SYS.XMLTYPE);

The XMLType datatype is actually an object type, which means that it comes with a set of methods you can
use to manipulate object instances of this type. So if I want to insert a row into the env_analysis table, I
can write code like that shown in Example 2.

Specifically, I call the CreateXML static method of the XMLType datatype to convert a string into an XML
type. Other methods in the XMLType datatype include:

• existsNode: Returns 1 if the given XPath expression returns any result nodes.
• extract: Applies an XPath expression over the XML data to return an XMLType instance
containing the resultant fragment.
• isFragment: Returns 1 if the XMLtype contains a fragment, rather than a complete document.
• getCLOBval, getStringval, and getNumberval: Return an XML document or fragment as CLOB,
string, or number, respectively.

Example 2. Inserting into an XMLType Column


INSERT INTO env_analysis VALUES (
'ACME SILVERPLATING',
TO_DATE (
'15-02-2001', 'DD-MM-YYYY'),
SYS.XMLTYPE.CREATEXML(
'<?xml version="1.0"?>
<report>
<site>1105 5th Street</site>
28
<substance>PCP</substance>
<level>1054</level>
</report>'));

Using these methods, I can perform more complex operations with XMLTypes. In the statement shown in
Example 3, I utilize XPath syntax to create a function based on the first 30 characters of the names of
substances analyzed in the environmental report.

Example 3. Creating a Function-Based Index Utilizing XMLType Methods


CREATE UNIQUE INDEX i_purchase_order_reference
ON env_analysis ea (
SUBSTR(
SYS.XMLTYPE.GETSTRINGVAL (
SYS.XMLTYPE.EXTRACT(
ea.report, '/Report/Substance/text()')),1,30))

Oracle 9i Release 2 (currently entering beta test) will add significant new features for XML document
management, including support for access control lists, foldering (allowing for the creation of hierarchies of
directories and utilities to search and manage them), and support for FTP access and WEBDav (Web-based
Distributed Authoring and Versioning, HTTP extensions for collaborative editing and management of files
on remote Web servers).

The bottom line for PL/SQL programmers: if you have to choose (or establish priorities) between learning
Java and learning XML, my recommendation is that you come up to speed as quickly as possible on XML.
Going forward, it will be an increasingly prominent feature in Oracle-based applications.

Working with "Any" Types

Let's finish up this overview of some of Oracle 9i's new datatypes with a look at the new "Any" types. Does
that sound terribly generic? It should sound that way, because it is. With Oracle 9i, the PL/SQL language is
finally given some powerful "reflection" capabilities: the ability to interrogate runtime data structures for
both data values and data structures. Why would you ever want or need something like that? When you're
building highly generic programs that are intended to be run and applied to multiple applications and
systems, making few or no assumptions in advance.

Many -- really, most -- developers will never need this capability, but it's still good to be aware of what's
possible. In this article, I'll give you a glimpse of the "Any" types. I'll explore this functionality in much
more depth in a future article.

First of all, Oracle offers a new built-in package, DBMS_TYPES, that offers named constants for all the
different SQL types supported by the database (and they're accessible via the "Any" types). Example 4
shows the current DBMS_TYPES package specification; this package is defined in the Oracle-provided
Rdbms/Admin/dbmsany.sql file.

Example 4. The DBMS_TYPES Package Specification


CREATE OR REPLACE PACKAGE DBMS_TYPES
AS
TYPECODE_DATE PLS_INTEGER := 12;
TYPECODE_NUMBER PLS_INTEGER := 2;
TYPECODE_RAW PLS_INTEGER := 95;
TYPECODE_CHAR PLS_INTEGER := 96;
29
TYPECODE_VARCHAR2 PLS_INTEGER := 9;
TYPECODE_VARCHAR PLS_INTEGER := 1;
TYPECODE_MLSLABEL PLS_INTEGER := 105;
TYPECODE_BLOB PLS_INTEGER := 113;
TYPECODE_BFILE PLS_INTEGER := 114;
TYPECODE_CLOB PLS_INTEGER := 112;
TYPECODE_CFILE PLS_INTEGER := 115;
TYPECODE_TIMESTAMP PLS_INTEGER := 187;
TYPECODE_TIMESTAMP_TZ PLS_INTEGER := 188;
TYPECODE_TIMESTAMP_LTZ PLS_INTEGER := 232;
TYPECODE_INTERVAL_YM PLS_INTEGER := 189;
TYPECODE_INTERVAL_DS PLS_INTEGER := 190;

TYPECODE_REF PLS_INTEGER := 110;


TYPECODE_OBJECT PLS_INTEGER := 108;
TYPECODE_VARRAY PLS_INTEGER := 247; /* COLLECTION TYPE */
TYPECODE_TABLE PLS_INTEGER := 248; /* COLLECTION TYPE */
TYPECODE_NAMEDCOLLECTION PLS_INTEGER := 122;
TYPECODE_OPAQUE PLS_INTEGER := 58; /* OPAQUE TYPE */

SUCCESS PLS_INTEGER := 0;
NO_DATA PLS_INTEGER := 100;

/* Exceptions */
invalid_parameters EXCEPTION;
PRAGMA EXCEPTION_INIT(invalid_parameters, -22369);

incorrect_usage EXCEPTION;
PRAGMA EXCEPTION_INIT(incorrect_usage, -22370);

type_mismatch EXCEPTION;
PRAGMA EXCEPTION_INIT(type_mismatch, -22626);
END dbms_types;
/

You'll need to make reference to one or more of these constants as you interrogate data structures.

So, let's see what kind of magic you can work with these types. Suppose I want to create a data structure that
contains heterogeneous or different kinds of data. One example of such a requirement might be if I'm using
Advanced Queuing. Rather than having to constrain each queue message to contain a certain object type, I
want it to contain different types.

I can now create a "generic table" that will hold virtually any kind of data (number, string, object type, and
so on). Here we go:

First, I'll create an object type of pets:

CREATE TYPE pet_t IS OBJECT (


tag_no INTEGER,
name VARCHAR2 (60),
breed VARCHAR2(100);
/

Now my generic table:

CREATE TABLE wild_side (


id number,
data SYS.ANYDATA);

30
Each row in this table contains an identification number and, well, just about anything, as you can easily see
in the following block doing inserts on this table:

DECLARE
my_bird pet_t :=
pet_t (5555,
'Mercury',
'African Grey Parrot');
BEGIN
INSERT INTO wild_side
VALUES (1,
SYS.ANYDATA.CONVERTNUMBER (5));

INSERT INTO wild_side


VALUES (2,
SYS.ANYDATA.CONVERTOBJECT
(my_bird));

END;

I've added two rows, one containing a number and the other a pet object instance. I accomplished this by
calling two of the convert methods associated with the AnyData object type (also defined in the dbmsany.sql
file).

That shows how to put diverse kinds of data into an AnyData column. That's fairly interesting, but even
more impressive is the ability to query rows from this data and then figure out what kind of data is sitting in
the data column.

You'll find in Example 5 the package-based specification of a function that retrieves from the generic table
only those rows that: 1) contain numbers, and 2) contain numbers that satisfy the Boolean expression (in
essence, a WHERE clause).

Example 6 shows the body of this function, with line numbers. First, we'll look at how this program can be
used. Then we'll step through the most interesting parts of the code. Here's an example of using the function:

SQL> l
1 DECLARE
2 mynums anynums_pkg.numbers_t;
3 BEGIN
4 mynums := anynums_pkg.getvals (
5 'wild_side', 'data');
6
7 mynums := anynums_pkg.getvals (
8 'wild_side', 'data', '> 100');
9 END;

On line 2, I declare a local nested table to hold the results of my retrieval. On lines 4- 5, I call the getVals
function, passing the table name wild_side and the name of the AnyData column, data. This should return
the values in every row in which the AnyData column actually contains a number, skipping everything else.
On lines 7-8, I again request numeric values from wild_side.data, but this time I specify that I only want
data whose values are greater than 100.

Example 5. Package Specification for "Any" Function


CREATE OR REPLACE PACKAGE anynums_pkg
IS
TYPE numbers_t IS TABLE OF NUMBER;

31
FUNCTION getvals (
tab_in IN VARCHAR2,
anydata_col_in IN VARCHAR2,
num_satisfies_in IN VARCHAR2 := NULL
)
RETURN numbers_t;
END anynums_pkg;
/

Example 6. Package Body for "Any" Function


1 CREATE OR REPLACE PACKAGE BODY anynums_pkg
2 IS
3 FUNCTION getvals (
4 tab_in IN VARCHAR2,
5 anydata_col_in IN VARCHAR2,
6 num_satisfies_in IN VARCHAR2 := NULL
7 )
8 RETURN numbers_t
9 IS
10 retval numbers_t := numbers_t ();
11 l_query VARCHAR2 (1000)
12 := 'SELECT '
13 || anydata_col_in
14 || ' FROM '
15 || tab_in;
16 l_type SYS.ANYTYPE;
17 l_typecode PLS_INTEGER;
18 l_value NUMBER;
19 l_dummy PLS_INTEGER;
20 l_filter VARCHAR2 (32767);
21 l_include BOOLEAN;
22 BEGIN
23 FOR rec IN (SELECT DATA
24 FROM wild_side)
25 LOOP
26 l_typecode := rec.DATA.gettype (l_type /* OUT */);
27
28 IF l_typecode = dbms_types.typecode_number
29 THEN
30 l_dummy := rec.DATA.getnumber (l_value /* OUT */);
31 l_include := num_satisfies_in IS NULL;
32
33 IF NOT l_include
34 THEN
35 l_filter :=
36 'DECLARE l_bool BOOLEAN; BEGIN l_bool := :invalue '
37 || num_satisfies_in
38 || '; IF l_bool THEN :intval := 1; ELSE :intval := 0; END IF; END;';
39 EXECUTE IMMEDIATE l_filter USING IN l_value, OUT l_dummy;
40 l_include := l_dummy = 1;
41 END IF;
42
43 IF l_include
44 THEN
45 retval.EXTEND;
46 retval (retval.LAST) := l_value;
47 END IF;
48 END IF;
49 END LOOP;
50
51 RETURN retval;
52 EXCEPTION
53 WHEN OTHERS
32
54 THEN
55 pl (SQLERRM);
56 pl (l_filter);
57 RETURN NULL;
58 END;
59* END anynums_pkg;

Now let's take a look at Example 6 and the logic that accomplishes all generic retrieval (see Table 2). To do
something as flexible as this, I need to take advantage of dynamic SQL and the generic datatype methods. I
need to dynamically evaluate each numeric value to see whether it passes the filter, which is passed as a
string; this is actually a dynamically-constructed PL/SQL block. If it passes the filter, I deposit that value
into the outgoing collection.

Table 2. Breakdown of Example 6


Lines Description
11-15 Construct the basic query to retrieve all of the AnyData columns from the table.
Call the AnyData.gettype method to query this "opaque" datatype to find out what type it actually
26
is. This is the fun part!
Compare this retrieved type against the DBMS_TYPES constant. Is it a number? If so, continue
28
evaluating.
30 We know it's a number, but what is the value? Call the AnyData.getnumber method to get it.
The user passed in a filter, so I need to see whether this numeric value passes the filter. For example,
if the user passes in "> 100", then I need to find out whether the value is greater than 100. How do I
35-40 do that? I'll have to dynamically construct an anonymous PL/SQL block that executes an assignment
to a Boolean variable from that expression. Example 7 shows precisely the dynamic block that's
constructed and executed for the fragment "> 100".
If the value passes the filter (or the filter was NULL), then I extend the nested table and assign the
43-47
value.

Example 7. Dynamic PL/SQL Block for Filter Evaluation


DECLARE
l_bool BOOLEAN;
BEGIN
l_bool := :invalue > 100;

IF l_bool
THEN
:intval := 1;
ELSE
:intval := 0;
END IF;
END;

Lots of New Possibilities

Sure, I've been having fun coding with PL/SQL for years. With Oracle 9i, though, the possibilities for truly
entertaining and exciting programming techniques expand dramatically. Now everyone can have fun with
PL/SQL!

33
Planning to work with XML? Now you can do it with native XML functionality directly inside of the
database. Frustrated with the limitations of DATE? Port your code to TIMESTAMPs and INTERVALs. Want to
impress friends, family, and managers by performing magic tricks with AnyData and AnyDataSet? Put aside
lots of time to play around with those object types, because the documentation is not only minimal, but
misleading.

34
Native Compilation, CASE, and Dynamic Bulk Binding
Introduction

We'll kick this off this second part of our series on new PL/SQL features with an exploration of Oracle 9i
PL/SQL enhancements. The most important enhancements to PL/SQL in Oracle 9i can be categorized as
falling into one of these areas:

• Its implementation (that which effects the execution characteristics of a given system of source code)
• Language features (the addition of new syntax to express powerful new semantics)
• Oracle-supplied PL/SQL library units

Some of the enhancements are transparent; for example, the change to using the same parser for compile-
time checked embedded SQL as is used for compiling SQL issued from other programming environments;
or the re-implementation of the UTL_TCP package (moving from Java to native C). You don't have to do
anything besides upgrade to Oracle 9i to take advantage of these improvements.

Some are semi-transparent, which means that you'll need to take small, declarative steps (that is, you don't
have to change any of your code). The best example of such a semi-transparent enhancement is the new
option to compile PL/SQL source to native C--a topic to be explored in a future article.

Finally, some new features introduce new semantics, either in the language itself or by virtue of new APIs in
the supplied PL/SQL library units. To take advantage of these enhancements, you'll need to first learn what's
possible and then carefully decide how to upgrade existing code or integrate these features in new
applications.

In future articles, we'll explore in much more detail the following Oracle 9i PL/SQL enhancements:

• Table functions and cursor expressions


• Multi-level collections
• Enhancements to the UTL_HTTP package
• Use of the "Any" datatypes to write highly generic code

In each case, we'll introduce you to the technology and then provide extensive, complete code samples that
provide working demonstrations for all these features. In this article, we'll cover native compilation of
PL/SQL, CASE statements and CASE expressions, and bulk binding in native dynamic SQL.

Native Compilation of PL/SQL

In pre-Oracle 9i versions, compilation of PL/SQL source code always results in a representation (usually
referred to as bytecode) that's stored in the database and interpreted at runtime by a virtual machine
implemented within Oracle that, in turn, runs natively on the given platform. Oracle 9i introduces a new
approach. PL/SQL source code may optionally be compiled into native object code that's linked into Oracle.
(Note, however, that an anonymous PL/SQL block is never compiled natively.) When would this feature
come in handy? How do you turn on native compilation? So many questions... and here come the answers.

PL/SQL is often used as a thin wrapper for executing SQL statements, setting bind variables, and handling
result sets; one example of such a wrapper can be seen in Example 1. For these kinds of programs, the
execution speed of the PL/SQL code is rarely an issue. It is, rather, the execution speed of the SQL that
determines the performance. (The efficiency of the context switch between the PL/SQL and the SQL

35
operating environments might be an issue, but that's a different discussion. See the section in this article on
bulk binding as one way of addressing this context switch issue.)

Example 1. Thin wrapper for executing SQL statements.

BEGIN
FOR department IN (SELECT department_id d, department_name
FROM departments
ORDER BY department_name)
LOOP
DBMS_OUTPUT.PUT_LINE ( CHR (10)
|| department.department_name);

FOR employee IN (SELECT last_name


FROM employees
WHERE department_id = department.d
ORDER BY last_name)
LOOP
DBMS_OUTPUT.PUT_LINE ( '- '
|| employee.last_name);
END LOOP;
END LOOP;
END;

There are many other applications and programs, however, that rely on PL/SQL to perform
computationally-intensive tasks that are all but completely independent of the database. It is, after all, a fully
functional procedural language. Consider, for example, the code shown in Example 2. This program takes
on the task of finding all right-angled triangles with all side lengths integer (a.k.a. perfect triangles). We
must count only unique triangles-that is, those whose sides are not each the same integral multiple of the
sides of a perfect triangle already found.

Example 2. Computing perfect triangles.

CREATE OR REPLACE PROCEDURE perfect_triangles (p_max IN INTEGER)


IS
t1 INTEGER;
t2 INTEGER;
long INTEGER;
short INTEGER;
hyp NUMBER;
ihyp INTEGER;

TYPE side_r IS RECORD (


short INTEGER,
long INTEGER);

TYPE sides_t IS TABLE OF side_r


INDEX BY BINARY_INTEGER;

unique_sides sides_t;
n INTEGER := 0 /* curr max elements in unique_sides */;
dup_sides sides_t;
m INTEGER := 0 /* curr max elements in dup_sides */;

PROCEDURE store_dup_sides (p_long IN INTEGER, p_short IN INTEGER)


IS
mult INTEGER := 2;
long_mult INTEGER := p_long * 2;
short_mult INTEGER := p_short * 2;
BEGIN
36
WHILE (long_mult < p_max)
OR (short_mult < p_max)
LOOP
n := n + 1;
dup_sides (n).long := long_mult;
dup_sides (n).short := short_mult;
mult := mult + 1;
long_mult := p_long * mult;
short_mult := p_short * mult;
END LOOP;
END store_dup_sides;

FUNCTION sides_are_unique (p_long IN INTEGER, p_short IN INTEGER)


RETURN BOOLEAN
IS
BEGIN
FOR j IN 1 .. n
LOOP
IF (p_long = dup_sides (j).long)
AND (p_short = dup_sides (j).short)
THEN
RETURN FALSE;
END IF;
END LOOP;

RETURN TRUE;
END sides_are_unique;
BEGIN /* Perfect_Triangles */
t1 := DBMS_UTILITY.get_time;

FOR long IN 1 .. p_max


LOOP
FOR short IN 1 .. long
LOOP
hyp := SQRT ( long * long + short * short);
ihyp := FLOOR (hyp);

IF hyp
- ihyp < 0.01
THEN
IF (ihyp * ihyp =
long * long
+ short * short
)
THEN
IF sides_are_unique (long, short)
THEN
m := m + 1;
unique_sides (m).long := long;
unique_sides (m).short := short;
store_dup_sides (long, short);
END IF;
END IF;
END IF;
END LOOP;
END LOOP;

t2 := DBMS_UTILITY.get_time;
DBMS_OUTPUT.put_line (
CHR (10)
|| TO_CHAR ((( t2
- t1
) / 100

37
), '9999.9')
|| ' sec'
);
END perfect_triangles;

This program implements an exhaustive search among candidate triangles with all possible combinations of
lengths of the two shorter sides, each in the range of one to a specified maximum. Testing whether the
square root of the sum of the squares of the two short sides is within 0.01 of an integer coarsely filters each
candidate. Exactly applying Pythagoras's theorem using integer arithmetic tests triangles that pass this test.
Candidate- perfect triangles are tested against the list of multiples of perfect triangles found so far. Each
new unique perfect triangle is stored in a PL/SQL table, and its multiples (up to the maximum length) are
stored in a separate PL/SQL table to facilitate uniqueness testing.

The implementation thus involves a doubly nested loop with these steps at its heart: several arithmetic
operations, casts and comparisons; calls to procedures implementing comparisons driven by iteration
through a PL/SQL table (with yet more arithmetic operations); and extension of PL/SQL tables where
appropriate.

So what impact does native compilation have on such code? The elapsed time was measured for p_max
=5000 (that is, 12.5 million repetitions of the heart of the loop) using interpreted and natively compiled
versions of the procedure. The times were 548 seconds and 366 seconds, respectively (on a Sun Ultra60
with no load apart from the test). Thus, the natively compiled version was about 33 percent faster.

That's not bad for a semi-transparent enhancement. In other words, no code changes were required in our
application. And, while for data-intensive programs, native compilation may give only a marginal
performance improvement, we've never seen it give performance degradation. So how do you turn on native
compilation? Read on...

One-Time DBA Setup

Native PL/SQL compilation is achieved by translating the PL/SQL source code into C source code that's
then compiled on the given platform. The compiling and linking of the generated C source code is done
using third-party utilities whose location has been specified by the DBA, typically in the init.ora parameter
file. (Check the Release Notes for your platform to see which third-party utilities are supported.)

The object code for each natively compiled PL/SQL library unit is stored on the platform's file system in
directories, similarly under the DBA's control. Thus, native compilation does take longer than interpreted-
mode compilation. Our tests have shown a factor of about times two. This is because it involves these extra
steps: generating C code from the initial output of the PL/SQL compilation; writing this to the file system;
invoking and running the C compiler; and linking the resulting object code into Oracle.

Oracle recommends that the C compiler be configured to do no optimization. Our tests have shown that
optimizing the generated C produces negligible improvement in runtime performance but substantially
increases the compilation time.

The DBA should appreciate that the utilities for compilation and linking must be under his or her strict
control, owned by the Oracle user or root (or its equivalent on non-Unix systems) and with write-access
granted only to these users. You already protect the Oracle executables in this way, for obvious reasons.
You can imagine what might happen if you didn't! Well, the risk would be just the same if a malicious user
could subvert the utilities for compilation and linking used by native compilation.

Choosing Between Interpreted and Native Compilation Modes


38
The compiler mode is determined by the session parameter plsql_compiler_flags. The user may set it as
follows:

ALTER SESSION
SET plsql_compiler_flags =
'NATIVE' /* or 'INTERPRETED' */;

The compilation mode is then set for subsequently compiled PL/SQL library units (during that session). The
mode is stored with the library unit's metadata, so that if the program is implicitly recompiled as a
consequence of dependency checking, the original mode the user intended will be used.

You can determine the compilation mode by querying the data dictionary using the SELECT statement shown
in Example 3.

Example 3. Determining the compilation mode of a program unit.

SELECT o.object_name NAME, s.param_value comp_mode

FROM USER_STORED_SETTINGS s,
USER_OBJECTS o

WHERE o.object_id = s.object_id


AND param_name = 'plsql_compiler_flags'
AND o.object_type IN ('PACKAGE', 'PROCEDURE', 'FUNCTION');

One thing to be aware of: If you use DBMS_UTILITY.COMPILE_SCHEMA to attempt to recompile all invalid
program units in your schema, it will use the current value of plsql_compiler_flags rather than the
compilation mode stored with each program unit. (In other words, it does the equivalent of alter ... compile
without reuse settings for the whole schema.)

Oracle recommends that all the PL/SQL library units that are called from a given top- level unit be compiled
in the same mode. This is because there's a cost for the context switch when a library unit compiled in one
mode invokes one compiled in the other mode, particularly when a native unit calls an interpreted unit.
Significantly, this recommendation includes the Oracle-supplied library units. These are always shipped (in
the seed databases) compiled in interpreted mode.

Of course, if you compile some critical library units in native mode while everything else is in interpreted
mode and if you measure an improvement in performance, then this is only good! But you might be missing
an opportunity for yet more improvement by not having every unit in the database native. When starting
from scratch, use the Database Configuration Assistant to set the plsql_% initialization parameters
appropriately and create a new database. To upgrade an existing database to all native, use Oracle's script.
(See the Oracle Technical Network for more information.)

Case Study: 170 Systems

170 Systems, Inc. has been an Oracle partner for 11 years and participated in the Beta Program for the
Oracle 9i Database with particular interest in PL/SQL native compilation. They've now certified their 170
MarkView Document Management and Imaging SystemT against Oracle 9i and have updated the install
scripts to optionally turn on native compilation.

The 170 MarkView Document Management and Imaging System provides Content Management, Document
Management, Imaging, and Workflow Solutions-all tightly integrated with the Oracle 9i Database, Oracle 9i
Application Server, and the Oracle E- Business Suite. Enabling businesses to capture and manage all of their

39
information online in a single, unified system-regardless of original source or format-the 170 MarkView
solution provides scalable, secure, production-quality Internet B2B and intranet access to all of an
organization's vital information, while streamlining the associated business processes and maximizing
operational efficiency.

A large-scale multi-user, multi-access system, 170 MarkViewT supports the very large numbers of
documents, images, concurrent users, and the high transaction rates required by 170 Systems customers.
Therefore, performance and scalability are especially important. 170 Systems customers include
organizations such as British Telecommunications, E*TRADE Group, the Apollo Group, and the University
of Pennsylvania. 170 MarkView uses several different mechanisms to interface to the Oracle 9i Database.
Part of the business logic, including preparation of data for presentation, is implemented in the database in
PL/SQL.

The computation involves string processing supported by stacks and lists of values modeled as PL/SQL
collections. Several PL/SQL modules implement complex logic and include intensive string manipulation
and processing. PL/SQL collections are leveraged in this complex processing.

They've observed a performance increase of up to 40 percent for computationally- intensive routines, and no
performance degradation, in line with our observations using the code in Example 1 and Example 2 of this
article. Native compilation offers compelling advantages to existing and new applications written in
PL/SQL. It's just one example of Oracle's commitment to improving PL/SQL "from the bottom on up."

CASE Statements and CASE Expressions

Over the years, the PL/SQL user community has been vocal about how they'd like to see the language
improved. These enhancements have ranged from "big picture" functionality like, "Gee, we'd really like to
have a debugger!" to very concrete requests such as, "Please let me insert into a table using a record."
Another example of a long-requested, very specific desire is support for CASE within PL/SQL. Well, with
Oracle 9i, your wish has come true!

Actually, Oracle implemented support for a CASE expression in Oracle8i, but 1) it was only available inside
SQL statements, and 2) those SQL statements weren't recognized as valid when compiled within a PL/SQL
block. With Oracle 9i, SQL that uses a CASE construct can be used in static SQL in a PL/SQL block...

BEGIN
FOR j IN (
SELECT
CASE ename
WHEN 'SMITH' THEN 'MR. SMITH'
WHEN 'ALLEN' THEN 'MR. ALLEN'
ELSE ename
END
FROM emp
)
LOOP ...; END LOOP;
END;

...since any SQL that works in the SQL environment works in the PL/SQL environment by virtue of the new
common parser. You can also write your own CASE statements and expressions within your PL/SQL code.

CASE constructs don't offer any fundamentally new semantics (anything you write in a CASE statement can
be implemented with IF). They do, however, allow a more compact notation and some elimination of
repetition with respect to what otherwise would be expressed with an IF construct. Consider the
implementation of a decision table whose predicate is the value of a particular expression. The following
40
two fragments are semantically identical, but coding best practice gurus generally recommend the CASE
formulation because it more directly models the idea behind the code:

CASE expr
WHEN 1 THEN Action1;
WHEN 2 THEN Action2;
WHEN 3 THEN Action3;
ELSE ActionOther;
END CASE;

and:

IF expr = 1 THEN Action1;


ELSIF expr = 2 THEN Action2;
ELSIF expr = 3 THEN Action2;
ELSE ActionOther;
END IF;

By pulling out the decision expression expr to the start and by mentioning it only once, the programmer's
intention is clearer. This is significant both to a person reviewing the code and to the compiler, which
therefore has better information from which to generate efficient code. For example, the compiler knows
immediately that the decision expression needs to be evaluated just once. Moreover, since the IF
formulation repeats the decision expression for each leg, there's a greater risk of typographical error that can
be difficult to spot.

Oracle offers a stand-alone CASE statement and a CASE expression (part of a larger statement, usually an
expression). Let's look at the statement first.

About CASE statement syntax

The CASE statement begins with the keyword CASE. The keyword is followed by a selector. The selector
expression can be arbitrarily complex. For example, it can contain function calls. Usually, however, it
consists of a single variable. The selector expression is evaluated only once. The value it yields can have
any PL/SQL datatype other than BLOB, BFILE, an object type, a PL/SQL record, an index-by-table, a varray,
or a nested table.

So, basically, you can use CASE statements with scalar values, like strings, dates, Booleans, intervals, and so
on.

The selector is followed by one or more WHEN clauses, which are checked sequentially. The value of the
selector determines which clause is executed. If the value of the selector equals the value of a WHEN-clause
expression, that WHEN clause is executed.

The ELSE clause works similarly to the ELSE clause in an IF statement. The ELSE clause is optional.
However, if you omit the ELSE clause, PL/SQL adds the following implicit ELSE clause:

ELSE RAISE CASE_NOT_FOUND;

If the CASE statement selects the implicit ELSE clause, PL/SQL raises the predefined exception
CASE_NOT_FOUND. So there's always a default action, even when you omit the ELSE clause.

The keywords END CASE terminate the CASE statement. These two keywords must be separated by a space.

41
Here's another example of a typical CASE statement:

CREATE OR REPLACE FUNCTION


grade_translator (grade_in IN VARCHAR2)
RETURN VARCHAR2
IS
retval VARCHAR2(100);
BEGIN
CASE
WHEN grade_in = 'A'
THEN retval := 'Excellent';
WHEN grade_in = 'B'
THEN retval := 'Very Good';
WHEN grade_in = 'C'
THEN retval := 'Good';
WHEN grade_in = 'D'
THEN retval := 'Fair';
WHEN grade_in = 'F'
THEN retval := 'Poor';
ELSE retval := 'No such grade';
END CASE;
RETURN retval;
END;

The CASE Expression

The CASE expression is a fragment of a statement that returns a value. Its syntax is similar to that of a CASE
statement with the following differences:

• Each WHEN clause does not end with a semicolon (it's all part of a single expression, whereas a
semicolon indicates the end of a logical statement).
• The END clause does not include the CASE keyword. You simply END the expression.
• You don't perform assignments within the WHEN clauses. Instead, you simply provide the value that
you want to be assigned after the CASE expression is evaluated.

Here's a rewrite of the previous grade translator using the expression syntax:

CREATE OR REPLACE FUNCTION


grade_translator (grade_in IN VARCHAR2)
RETURN VARCHAR2
IS
BEGIN
RETURN
CASE
WHEN grade_in = 'A'
THEN 'Excellent'
WHEN grade_in = 'B'
THEN 'Very Good'
WHEN grade_in = 'C'
THEN 'Good'
WHEN grade_in = 'D'
THEN 'Fair'
WHEN grade_in = 'F'
THEN 'Poor'
ELSE 'No such grade'
END;
END;

42
Notice that, in this case, we don't assign the expression to a variable. We RETURN it directly from the
function.

Searched CASE Statements and Expressions

With a "searched CASE" statement or expression, you don't provide a selector value for the CASE statement.
Instead, the WHEN clauses contain search conditions that evaluate to a Boolean value. Use this form when
you aren't checking simply for matching values, but you need to evaluate arbitrarily complex Boolean
expressions.

Here are examples of searched CASE statements and expressions:

CASE
WHEN n = 1 THEN Action1;
WHEN n = 2 THEN Action2;
WHEN n = 3 THEN Action3;
WHEN ( n > 3 and n < 8 )
THEN Action4through7;
ELSE ActionOther;
END CASE;

and:

text :=
CASE
WHEN n = 1 THEN one
WHEN n = 2 THEN two
WHEN n = 3 THEN three
WHEN ( n > 3 and n < 8 )
THEN four_through_seven
ELSE other
END;

Example 4 offers one more example of a searched CASE expression, this time using the new Oracle 9i
INTERVAL datatype to determine whether I'm too old or too young to be forced to work!

Example 4. Searched CASE expression with INTERVALs.

DECLARE -- Example of CASE searched expression


cant_play_now BOOLEAN;
how_young INTERVAL YEAR TO MONTH :=
(SYSDATE - TO_DATE ('09-23-1958', 'MM-DD-YYYY')) YEAR TO MONTH;
max_age CONSTANT INTERVAL YEAR TO MONTH := INTERVAL '16' YEAR;
min_age CONSTANT INTERVAL YEAR TO MONTH := INTERVAL '70' YEAR;
BEGIN
-- Notice: no semi-colons between WHEN clauses.
cant_play_now :=
CASE
WHEN how_young < min_age THEN FALSE
WHEN how_young > max_age THEN FALSE
ELSE TRUE
END;

IF cant_play_now
THEN
must_go_to_work;
END IF;
END;

43
Bulk Binding in Native Dynamic SQL

One of the most exciting new features in Oracle8i was the native dynamic SQL: an implementation of
dynamic SQL that relies on two native statements (EXECUTE IMMEDIATE and OPEN FOR) to run dynamically
constructed SQL statements at runtime. NDS is dramatically easier to use than DBMS_SQL and, in most cases,
is noticeably faster. But NDS wasn't the only new feature for improving SQL performance and flexibility.
Oracle also introduced "bulk binding" for DML and queries.

And never the twain could meet. In other words, we couldn't use bulk binding to run a dynamic SQL
statement. In Oracle 9i, all that's changed. We can now take advantage of these two great features together.
This section refreshes your knowledge of bulk binding and then shows how to use it with dynamic SQL,
finishing up with an explanation of improved error handling for bulk-bind operations.

Suppose we need to write a program to populate elements of a PL/SQL collection from a SELECT query. Our
first pre-Oracle8i inclination would be to write what you see in Example 5.

Example 5. Using a cursor FOR loop to populate a collection.

DECLARE
TYPE employee_ids_t IS
TABLE OF employees.employee_id%TYPE
INDEX BY BINARY_INTEGER;

employee_ids employee_ids_t;
n INTEGER := 0;
BEGIN
FOR j IN (SELECT employee_id
FROM employees
WHERE salary < 3000)
LOOP
n := n + 1;
employee_ids (n) := j.employee_id;
END LOOP;
END;

Using this approach, each explicit row-by-row assignment of the collection element to the cursor component
causes a context switch between the PL/SQL engine and the SQL engine resulting in performance overhead.
With Oracle8i and above, we can move away from these context switches (and all that code) by performing
a BULK COLLECTION, as shown here:

BEGIN
SELECT employee_id
BULK COLLECT INTO employee_ids
FROM employees
WHERE salary < 3000;
...
END;

Wow! That's quite a change in code volume. And you'll see substantial improvements in performance, as
well, by minimizing the number of context switches required to execute the block.

But what if you want to execute this same query (and then process the rows) for one of any number of
different employee tables, segregated by location?

In Oracle 9i, we can take the preceding query and transform it into a dynamic SQL query that will handle
this additional complexity. There are many application implementation situations that require dynamic SQL.
44
Native dynamic SQL (execute immediate and related constructs) is usually preferred over Dbms_Sql
because it's easier to write and proofread and executes faster. However, pre-Oracle9i, only Dbms_Sql could
be used for dynamic bulk binding. Oracle 9i introduces the following syntax for bulk binding in native
dynamic SQL:

CREATE OR REPLACE PROCEDURE


process_employees (loc_in IN VARCHAR2)
IS
BEGIN
EXECUTE IMMEDIATE
'SELECT employee_id FROM '
|| loc_in || '_employees'
BULK COLLECT INTO employee_ids;
...
END;

It's so nice to see the fairly arbitrary restrictions in the PL/SQL language falling away with each new
release! Let's take a look at how you implement "in-binding" (binding variables into the dynamic SQL),
"out-binding" (extracting values from the dynamic SQL), and error handling when using bulk operations on
dynamic SQL statements.

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
45
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, with Oracle 9i only, also
employ a RETURNING BULK COLLECT clause:

CREATE OR REPLACE PROCEDURE


46
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

In Oracle8i, FORALL was wonderful for rapid processing of bulk DML statements. One problem with it,
however, is that you lose some of the granularity in exception handling that you help with row-by-row
processing. Suppose, for example, that we want to load a whole bunch of words into a vocabulary table. We
can do it very efficiently as follows:

BEGIN
FORALL indx IN
words.FIRST .. words.LAST
INSERT INTO vocabulary
(text)
VALUES (words (indx));
END;

If, however, an error occurred in any single INSERT, the entire FORALL statement would fail. This is the sort
of scenario that was easily handled with row-level processing, such as with a loop like this:

FOR indx IN words.FIRST .. words.LAST


LOOP
BEGIN
INSERT INTO t text)
VALUES (words (indx));
EXCEPTION
WHEN OTHERS
THEN
error_codes (indx) := SQLERRM;
END;
END LOOP;

With this kind of code, we'd insert all rows that didn't cause an error to occur. With Oracle 9i, you can now
do this with both static and dynamic FORALL SQL statements, by taking advantage of the SAVE EXCEPTIONS
clause.

FORALL indx IN
words.first..words.last
SAVE EXCEPTIONS

47
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 Example 6.

Example 6. Using the SQL%BULK_COLLECTIONS pseudo-collection.

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 j IN 1 .. SQL%BULK_EXCEPTIONS.COUNT
LOOP
log_error (
SQL%BULK_EXCEPTIONS(indx).ERROR_INDEX,
SQLERRM(-1 * SQL%BULK_EXCEPTIONS(indx).ERROR_CODE)
);
END LOOP;
END;

Each row of this pseudo-collection is a record consisting of two fields: ERROR_INDEX and ERROR_CODE. The
former field 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).

The March of Progress

The new Oracle 9i features covered in this article should provide a comfortable feeling about Oracle's
commitment to the PL/SQL language. Native compilation offers a path to formidable and transparent
improvements in our application performance (even, and perhaps especially, existing "legacy" code). The
CASE statement and expression, as well as support for dynamic SQL in bulk binding, help round out the
language semantics.

• We presented two examples on p.3...

CREATE OR REPLACE FUNCTION


grade_translator (grade_in IN VARCHAR2)
RETURN VARCHAR2
IS
retval VARCHAR2(100);
BEGIN
CASE
WHEN grade_in = 'A'
48
THEN retval := 'Excellent';
WHEN grade_in = 'B'
THEN retval := 'Very Good';
WHEN grade_in = 'C'
THEN retval := 'Good';
WHEN grade_in = 'D'
THEN retval := 'Fair';
WHEN grade_in = 'F'
THEN retval := 'Poor';
ELSE retval := 'No such grade';
END CASE;
RETURN retval;
END;

...and...

CREATE OR REPLACE FUNCTION


grade_translator (grade_in IN VARCHAR2)
RETURN VARCHAR2
IS
BEGIN
RETURN
CASE
WHEN grade_in = 'A'
THEN 'Excellent'
WHEN grade_in = 'B'
THEN 'Very Good'
WHEN grade_in = 'C'
THEN 'Good'
WHEN grade_in = 'D'
THEN 'Fair'
WHEN grade_in = 'F'
THEN 'Poor'
ELSE 'No such grade'
END;
END;

While they both do compile and run OK, chrisrimmer is right - they're not the best examples of good
style.

In the case that the WHEN at every leg is an equality test on the same expression, then you should
pull it out to the top of the CASE as you show.

So we should have used an example like this...

FUNCTION
grade_translator (grade_in IN VARCHAR2)
RETURN VARCHAR2
IS
BEGIN
RETURN
CASE
49
WHEN grade_in = 'A'
THEN 'Excellent'
WHEN grade_in = 'B'
THEN 'Very Good'
WHEN grade_in = 'C'
THEN 'Good'
WHEN grade_in IN ('D', 'E', 'F' )
THEN 'Could do better'
ELSE 'No such grade'
END;
END;

Thanks! Bryn.

• Problem with the examples? 2003-01-09 05:43:50 chrisrimmer [Reply]

Perhaps I have misunderstood, but the 2 examples of the grade_translator function on p3 of the
article do not seem to have a selector. They appear to be examples of the "searched" case statement
described lower down p3. I think they should read:

.....
CASE grade_in
WHEN 'A'
THEN retval := 'Excellent';
WHEN 'B'
THEN retval := 'Very Good';
.....

and

.....
CASE grade_in
WHEN 'A' THEN 'Excellent';
WHEN 'B' THEN 'Very Good';

50
Multi-Level Collections in Oracle 9i
Collections

New to Oracle 9i, you can now nest collections within collections, also referred to as support for "multi-
level collections." A collection is a data structure (actually, three different, but similar data structures:
index-by tables, nested tables, and varying arrays) that acts like a list or array. Collections are, in fact, the
closest you can get to traditional arrays in the PL/SQL language, though there are a number of differences.
Developers use collections to manage lists of information in their programs—or even within columns in a
database table.

Use of collections can both simplify the code we need to write and optimize performance of that code. A
collection can, for example, be used as the target of a bulk-bind query to improve the performance of data
transfer between the database and the PL/SQL processing. Collections can serve as a "local cache," avoiding
repetitive queries against the database.

Prior to Oracle 9i, collections could only be used to represent a single dimension of information (a list of
names or salaries). With Oracle 9i and support for multi-level collections, PL/SQL developers can now
model multi-dimensional phenomena, which greatly enlarges the set of real-world problems that we can
address.

This article will demonstrate how to use multi-level collections, first with a simple example and then a more
complex application of this feature.

An Introduction to Multi-Level Collections

Suppose we want to build a system to maintain information about pets. Besides their standard information,
such as species, name, and so on, we'd like to keep track of their visits to the veterinarian. So we create a
vet-visit object type:

create type vet_visit_t is object (


visit_date date,
reason varchar2 (100)
);
/

Notice that objects instantiated from this type aren't associated with a pet (that is, a foreign key to a pet table
or object). You'll soon see why we don't need to do that. Now we create a nested table of vet visits (we are,
after all, supposed to visit the vet at least once a year):

create type vet_visits_t is table of vet_visit_t


/

With these data structures defined, we'll now declare our object type to maintain information about the pets:

create type pet_t is object (


tag_no integer,
name varchar2 (60),
petcare vet_visits_t,
member function set_tag_no (
new_tag_no in integer) return pet_t)
not final;
/

51
This object type has three attributes and one member method. Any object instantiated from this type will
have associated with it a tag number, name, and a list of visits to the vet. You can also modify the tag
number for that pet by calling the set_tag_no program. Finally, we've declared this object type to be NOT
FINAL so that we can extend this generic pet object type, taking advantage of Oracle9i's support for object
type inheritance.

So we've now declared an object type that contains an attribute and a nested table. We don't need a separate
database table to keep track of these veterinarian visits; they're a part of our object.

Now let's take advantage of the new multi-level collections features of Oracle 9i. In lines 2-3 of the
anonymous block shown in Example 1, we declare a local associative table TYPE, in which each row
contains a single pet object. We then declare a collection to keep track of this "bunch of pets."

Example 1. Defining and accessing a multi-level collection.


/* file multilevel_collections.sql */
1 declare
2 type bunch_of_pets_t is table of pet_t index by binary_integer;
3 my_pets bunch_of_pets_t;
4 begin
5 my_pets (1) :=
6 pet_t (100, 'Mercury',
7 vet_visits_t (
8 vet_visit_t ('01-Jan-2001', 'Clip wings'),
9 vet_visit_t ('01-Apr-2002', 'Check cholesterol'))
10 );
11 dbms_output.put_line (my_pets (1).name);
12 dbms_output.put_line (my_pets (1).petcare (2).reason);
13 dbms_output.put_line (my_pets.count);
14 dbms_output.put_line (my_pets(1).petcare.last);
15 end;

Lines 5-10 assign an object of type pet_t to the first row in this associative table. As you can see, the
syntax required when working with nested, complex objects of this sort can be quite intimidating. So let's
"parse" the various steps required.

To instantiate an object of type pet_t, we must provide a tag number, name, and list of vet visits, which is a
nested table. To provide a nested table of type vet_visits_t, we must call the associated constructor (of
the same name). We can either provide a null or empty list, or initialize the nested table with some values.
We do this in lines 8 and 9. Each row in the vet_visits_t collection is an object of type vet_visit_t, so
again we must use the object constructor and pass in a value for each attribute (date and reason for visit).

Once the collection has been populated, we can access its data. We do this in lines 11-14. In line 11, we
display the value of the name attribute of the pet object in row 1 of the my_pets associative table. In line
12, we display the value of the reason attribute of the vet visit object in row 2 of the nested table, which in
turn resides in the first row of the my_pets associative table. Mmm. That's a mouthful, and it's a "line-full"
of code:

dbms_output.put_line (my_pets(1).petcare(2).reason);

On lines 13 and 14, we demonstrate how you can use the collection methods (in this case, COUNT and LAST)
on both outer and nested collections.

The output from running this script is:

52
Mercury
Check cholesterol
1
2

Let's now take a look at a more complex example of applying multi-level collections.

The "Runner's Training Logs" Scenario

Consider implementing a system to allow a running coach to maintain training logs for each of the runners
under his guidance. Each runner is identified by first name and runs several times per week. A run is
characterized by the distance and the average pace. The coach will want to monitor week-by-week
variations and progress. Of course, many designs for the logical data model will work, but we consider just
two here.

Single flat relational table:

create table reln_training_logs (


first_name varchar2(20) not null,
week number not null,
run number not null,
distance number not null,
pace number not null );

alter table reln_training_logs


add constraint reln_training_logs_pk
primary key (first_name,week,run)
using index;

Relational table with multi-level collection column:

create type run_t as object (


distance number, pace number );

create type weeks_running_t is


varray(20) of run_t not null;

create type training_log_t is


varray(255) of weeks_running_t not null;

create table nested_training_logs (


first_name varchar2(20) primary key,
training_log training_log_t );

The reln_training_logs approach would be suitable if the typical access was for ad hoc queries across
runners, and the nested_training_logs approach would be suitable if the typical access was to report all
the information for each of a number of selected runners.

We'll look at code to populate and to report on the nested_training_logs table. In our previous article we
described the powerful table function feature, new in Oracle 9i. We'll see an interesting application of table
functions in this example that allows us to "view" nested_training_logs as reln_training_logs and to
"view" reln_training_logs as nested_training_logs. By writing each with a ref cursor input
parameter, we can conveniently test that the result of two successive transformations is identical to the
starting data.

Populating the Nested Table


53
Before we can work with data in our collections, we need to fill them up. The following statements rely on
data structures for which the code is shown in Example 2. The full code for this population step is shown in
Example 3. The code has this general shape:

-- Initialize the collections


v_training_log :=
training_log_t (
weeks_running_t ( run_t ( 0, 0 ) ) );

v_training_log(1) :=
weeks_running_t (
run_t ( 1, 6 ),
run_t ( 7, 7 ),
...
run_t ( 18, 10 ));

-- Extend and populate in the database table.


v_training_log.extend;

...

insert into nested_training_logs (


first_name, training_log )
values
( 'fred', v_training_log );

Example 2. Define the data structures.


create type run_t as object (
distance number, pace number );
create type weeks_running_t is
varray(20) of run_t not null;
create type training_log_t is
varray(255) of weeks_running_t not null;

create or replace package my_types is


type reln_training_log_row_t is record (
first_name varchar2(20),
week number,
run number,
distance number,
pace number );

type cur_t is ref cursor


/*
strong cursor type for table
function partitioning
*/
return reln_training_log_row_t;

type reln_training_logs_tab_t is
table of reln_training_log_row_t;

type nested_training_log_row_t is record (


first_name varchar2(20),
training_log training_log_t );
type nested_training_logs_tab_t is
table of nested_training_log_row_t;
end my_types;

create table nested_training_logs (


first_name varchar2(20) primary key,
54
training_log training_log_t );

create table nested_training_logs_2 (


first_name varchar2(20) primary key,
training_log training_log_t );

create table reln_training_logs (


first_name varchar2(20) not null,
week number not null,
run number not null,
distance number not null,
pace number not null );
alter table reln_training_logs
add constraint reln_training_logs_pk primary key (
first_name,week,run)
using index;

create table reln_training_logs_2 (


first_name varchar2(20) not null,
week number not null,
run number not null,
distance number not null,
pace number not null );
alter table reln_training_logs_2
add constraint reln_training_logs_2_pk primary key (
first_name,week,run)
using index;

Example 3. Procedure to populate the nested table.


create or replace procedure populate_nested_training_logs is
v_training_log training_log_t;
begin
v_training_log :=
training_log_t ( weeks_running_t ( run_t ( 0, 0 ) ) );

v_training_log(1) :=
weeks_running_t
(
run_t ( 1, 6 ),
run_t ( 7, 7 ),
run_t ( 3, 6 ),
run_t ( 9, 9 ),
run_t ( 3, 6 ),
run_t ( 18, 10 )
);

v_training_log.extend;
v_training_log(2) :=
weeks_running_t
(
run_t ( 5, 7 ),
run_t ( 9, 8 ),
run_t ( 3, 7 ),
run_t ( 9, 9 ),
run_t ( 3, 7 )
);

v_training_log.extend;
v_training_log(3) :=
weeks_running_t
(
run_t ( 5, 7 ),
run_t ( 9, 8 ),
55
run_t ( 3, 7 ),
run_t ( 9, 9 ),
run_t ( 3, 7 )
) ;

insert into nested_training_logs (


first_name, training_log ) values
( 'fred', v_training_log );

v_training_log :=
training_log_t ( weeks_running_t ( run_t ( 0, 0 ) ) );

v_training_log(1) :=
weeks_running_t
(
run_t ( 2, 10 ),
run_t ( 3, 11 ),
run_t ( 3, 11 ),
run_t ( 4, 12 )
);

v_training_log.extend;
v_training_log(2) :=
weeks_running_t
(
run_t ( 1, 10 ),
run_t ( 2, 11 ),
run_t ( 3, 12 ),
run_t ( 2, 10 ),
run_t ( 1, 9 ),
run_t ( 4, 12 )
);

insert into nested_training_logs (


first_name, training_log ) values
( 'sid', v_training_log );
end populate_nested_training_logs;

Reporting on the Nested Table

We'll also need some code to extract the information from the nested table and show to a user of this
application. The following cursor for loop retrieves the name of the person and his training log and then
iterates through that log, displaying its contents. See Example 4 for the complete code.

begin
for v_row in (select first_name, training_log
from nested_training_logs)
loop
show (v_row.first_name);

for week in
v_row.training_log.first ..
v_row.training_log.last
loop
show (week);

for run in
v_row.training_log (week).first ..
v_row.training_log (week).last
loop
show (run);
show (v_row.training_log (week) (run).distance);
56
show (v_row.training_log (week) (run).pace);
end loop;
end loop;
end loop;
end;

Notice that in the preceding code we see that appending each successive subscript to the variable
representing the multi-level collection instance drills down into each successive layer in its structure.

If appropriate, this could be rewritten using bulk collect into local multi-level collection (with one extra
level), thus:

declare
type first_name_tab_t is table of
nested_training_logs.first_name%type
index by binary_integer;
v_first_name_tab first_name_tab_t;

type training_logs_tab_t is table of training_log_t


index by binary_integer;
v_training_logs_tab training_logs_tab_t;
begin
select first_name, training_log
bulk collect into
v_first_name_tab, v_training_logs_tab
from nested_training_logs;

for j in
v_first_name_tab.first..
v_first_name_tab.last
loop
Show ( v_first_name_tab(j) );
for week in v_training_logs_tab(j).first..
v_training_logs_tab(j).last
loop
Show ( week );
for run in v_training_logs_tab(j)(week).first..
v_training_logs_tab(j)(week).last
loop
Show ( run );
Show ( v_training_logs_tab(j)(week)(run).distance );
Show ( v_training_logs_tab(j)(week)(run).pace );
end loop;
end loop;
end loop;
end;

As you can see, we simply add another row specifier since we have an additional level of nesting:

v_training_logs_tab(j)(week)(run).distance

Example 4. Report on the contents of the nested table.


begin
for v_row in
( select first_name, training_log from nested_training_logs )
loop
dbms_output.put_line ( v_row.first_name );
for week in v_row.training_log.first..
v_row.training_log.last
loop

57
dbms_output.put_line ( '. week #' || to_char(week) );
for run in v_row.training_log(week).first..
v_row.training_log(week).last
loop
dbms_output.put_line
(
'. run #' || to_char(run) || ': '
|| lpad ( v_row.training_log(week)(run).distance, 3, ' ' )
|| ' /'
|| lpad ( v_row.training_log(week)(run).pace, 3, ' ' )
);
end loop;
end loop;
end loop;
end;

Deriving a table function from the reporting logic to output a relational "view" At the heart of the innermost
loop in the preceding section, we have the required information to populate a record corresponding to one
row of the relational representation. (Writing a program to generate a report is a convenient way to test the
logic before converting the code to a table function.) The conversion is relatively routine:

1. Surround the block with a create function statement and declare a ref cursor input parameter.
2. Define types for a record, and table of such records, according to the requirement and add a return
declaration for the record type.
3. Add the pipelined keyword.
4. Declare local variables v_in_row and v_out_row as records of the appropriate types.
5. Reformulate the cursor loop (if it's not already coded this way) to use fetch p_in_cursor into
v_in_row with the corresponding exit condition (don't open it—this is done by the system when the
table function is invoked).
6. Replace the Show invocations with assignments for the elements of the target record.
7. Deliver the record as the actual parameter to pipe row().
8. Add close p_in_cursor and return as the last executable statements.

With these changes in place (see Example 5), we can now conveniently perform ad hoc queries, such as that
shown here:

select first_name, avg ( distance ) d, avg ( pace ) p


from
(
select first_name, distance, pace
from table
(
reln_training_logs_fn
(
cursor
(
select first_name, training_log
from nested_training_logs
)
)
)
)
group by first_name;

Example 5. Table function to "view" the contents of the nested table as a relational
table.
create or replace function reln_training_logs_fn

58
( p_nested_training_logs in sys_refcursor )
return my_types.reln_training_logs_tab_t
/*
The algorithm handles each row in isolation and thus
is amenable to the simplest form of parallelism
*/
parallel_enable (
partition p_nested_training_logs by any ) pipelined
is
v_in_row my_types.nested_training_log_row_t;
v_out_row my_types.reln_training_log_row_t;
begin
loop
fetch p_nested_training_logs into v_in_row;
exit when p_nested_training_logs%notfound;

for week in v_in_row.training_log.first..


v_in_row.training_log.last
loop
for run in v_in_row.training_log(week).first..
v_in_row.training_log(week).last
loop
v_out_row.first_name :=
v_in_row.first_name;
v_out_row.week :=
week;
v_out_row.run :=
run;
v_out_row.distance :=
v_in_row.training_log(week)(run).distance;
v_out_row.pace :=
v_in_row.training_log(week)(run).pace;
pipe row ( v_out_row );
end loop;
end loop;
end loop;
close p_nested_training_logs;
return;
end reln_training_logs_fn;

Writing a Table Function to Output a Nested "View" from the


Relational Representation

Suppose it had been decided to implement the persistent storage of the collection as a relational
representation. It's still possible to view it as if it were the nested table representation by using a table
function. The simplest design would use nested PL/SQL cursor loops, thus:

for each distinct runner… ;


for each distinct week for that runner… ;
for each run for that week for that runner
add the object to represent the run
to the column in the collection for that week;
when done with that week add the column
for the whole week to the "plane"
of the collection for that runner's log;
when done with that runner,
pipe the record representing the name
and the training log collection.

59
To make the table function more general, it needs to have a ref cursor input parameter to be invoked with a
SELECT having two levels of nested CURSOR subqueries corresponding to the preceding nested PL/SQL
loops. An alternative is to design the function to accept a "flat" SELECT. The latter approach requires slightly
more elaborate coding of the function logic (to explicitly detect the next week and the next runner) but
makes the resulting function substantially more user-friendly, and so it was selected for implementation in
this illustration. To make the example richer with respect to understanding table functions, parallelization
declarations are added to ensure that all the rows for a particular runner go consecutively to the same slave,
and that for that runner the input rows are ordered by week and then run. (The algorithm depends on these
assumptions.)

This logic is shown in Example 6.

Example 6. Table function to "view" the contents of the relational table as a nested
table.
create or replace function nested_training_logs_fn
( p_reln_training_logs my_types.cur_t )
return my_types.nested_training_logs_tab_t
/*
The algorithm depends on assuming that it receives
rows ordered by first_name, week, then run, and that
all the rows for a particular runner go consecutively
to the same slave. These declarations ensure this and
remove the need for an ORDER BY clause in the SELECT
that's used to invoke this function.
*/
order p_reln_training_logs by ( first_name, week, run )
parallel_enable (
partition p_reln_training_logs by range ( first_name ) )
pipelined
is
g_in_row my_types.reln_training_log_row_t;
g_out_row my_types.nested_training_log_row_t;
g_weeks_running weeks_running_t;
g_training_log training_log_t;
g_first_time boolean := true;
g_got_a_row boolean;
g_new_week boolean;
g_new_runner boolean;
g_current_first_name reln_training_logs.first_name%type;
g_prev_first_name reln_training_logs.first_name%type;
g_current_week reln_training_logs.week%type;
g_prev_week reln_training_logs.week%type;

procedure get_next_row is begin


fetch p_reln_training_logs into g_in_row;
g_got_a_row := not p_reln_training_logs%notfound;
if g_got_a_row
then
case g_first_time
when true then
g_first_time := false;
g_new_runner := false;
g_new_week := false;
else
g_new_runner :=
g_prev_first_name <> g_in_row.first_name;
g_new_week := case g_new_runner
when true then true
else
g_prev_week <> g_in_row.week
60
end;
end case;
g_prev_first_name := g_in_row.first_name;
g_prev_week := g_in_row.week;
end if;
return;
end get_next_row;

function got_next_runner return boolean is begin


g_current_first_name := g_in_row.first_name;
g_new_runner := false;
return g_got_a_row;
end got_next_runner;

function got_next_week return boolean is begin


g_current_week := g_in_row.week;
g_new_week := false;
return ( not g_new_runner ) and g_got_a_row;
end got_next_week;

function got_next_run return boolean is begin


return ( not g_new_week ) and g_got_a_row;
end got_next_run;

procedure new_training_log is begin


g_training_log := null;
end new_training_log;

procedure new_weeks_running is begin


g_weeks_running := null;
end new_weeks_running;

procedure store_this_run is begin


if g_weeks_running is null
then
g_weeks_running :=
weeks_running_t ( run_t ( 0, 0 ) );
else
g_weeks_running.extend;
end if;
g_weeks_running ( g_in_row.run ):=
run_t ( g_in_row.distance, g_in_row.pace );
end store_this_run;

procedure store_this_weeks_running is begin


if g_training_log is null
then
g_training_log :=
training_log_t (
weeks_running_t ( run_t ( 0, 0 ) ) );
else
g_training_log.extend;
end if;
g_training_log ( g_current_week ):= g_weeks_running;
end store_this_weeks_running;

procedure output_this_runner is begin


g_out_row.first_name := g_current_first_name;
g_out_row.training_log := g_training_log;
end output_this_runner;

begin
get_next_row();

61
while got_next_runner()
loop
new_training_log;
while got_next_week()
loop
new_weeks_running;
while got_next_run()
loop
store_this_run;
get_next_row();
end loop;
store_this_weeks_running;
end loop;
output_this_runner; pipe row ( g_out_row );
end loop;
close p_reln_training_logs;
return;
end nested_training_logs_fn;

In Summary

You'll find in Example 7 an "end-to-end" test that populates two relational tables used in the relational and
nested representations and then validates that in fact they're identical in content. This test demonstrates the
range of options developers now have to find solutions to their problems.

Multi-level collections certainly introduce an additional level of complexity for PL/SQL developers. They
also, however, widen the range of real-world scenarios that can be modeled accurately and efficiently within
PL/SQL programs.

Example 7. An "end-to-end" test.


truncate table nested_training_logs;
execute populate_nested_training_logs

truncate table reln_training_logs;


insert into reln_training_logs
(
select *
from table
(
reln_training_logs_fn
(
cursor
(
select first_name, training_log
from nested_training_logs
)
)
)
);

truncate table nested_training_logs_2;


insert into nested_training_logs_2
(
select *
from table
(
nested_training_logs_fn
(
cursor ( select * from reln_training_logs )
)

62
)
);

truncate table reln_training_logs_2;


insert into reln_training_logs_2
(
select *
from table
(
reln_training_logs_fn
(
cursor
(
select first_name, training_log
from nested_training_logs_2
)
)
)
);

select *
from reln_training_logs_2
minus
select *
from reln_training_logs;

select *
from reln_training_logs
minus
select *
from reln_training_logs_2;

63
Oracle 9i: Table Functions and Cursor Expressions
Introduction

Cursor expressions (sometimes known as cursor subqueries) are an element of the SQL language. In pre-
Oracle 9i they were supported in SQL and by certain programming environments but not by PL/SQL.
Oracle 9i introduces PL/SQL support for cursor expressions. For example, a cursor expression can now be
used in the SELECT statement used to open a PL/SQL cursor, and manipulated appropriately thereafter. It
can also be used as an actual parameter to a PL/SQL procedure or function, which has great significance in
connection with table functions.

Table functions were also supported (in rudimentary form) in pre-Oracle 9i, but a number of major
enhancements have been made at Oracle 9i. A table function can now be written to deliver rows in pipeline
fashion as soon as they're computed, dramatically improving response time in a "first-rows" scenario. A
function can now be written to accept a SELECT statement as input, allowing an indefinite number of
transformations to be daisy-chained, avoiding the need for storage of intermediate results. And it can now be
written so that its computation can be parallelized to leverage Oracle's parallel query mechanism.

The enabling of parallel execution of a table function means that it's now practical to implement the ETL
(Extraction, Transformation, Load) phase of data warehouse applications in PL/SQL. (A pre-Oracle 9i table
function caused serialization.)

Let's take a look first at cursor expressions.

Manipulating Cursor Expressions in PL/SQL

Consider the following task: List the department names, and, for each department, list the names of the
employees in that department. It can be simply implemented by a classical sequential programming
approach, as shown in Example 1.

Example 1. Use of nested FOR loops.

begin
for department in (select department_id, department_name
from departments
order by department_name)
loop
show (department.department_name);

for employee in (select employee_id, last_name


from employees
where department_id = department.department_id
order by last_name)
loop
show (employee.last_name);
end loop;
end loop;
end;

A note on the code: Whenever two authors collaborate, they determine how best to share the work and
present their contributions. We have two different formatting styles: Steven prefers to capitalize key and
reserved words, while lowercasing everything else; Bryn likes to present everything in lowercase. As Bryn
wrote the vast majority of the code for this article, we'll use his preferred style.
64
The CURSOR expression, introduced in Oracle 8i but not available within PL/SQL, allows you to more
concisely express the desired result set in a single query, such as:

select
department_name,
cursor (
select last_name
from employees e
where e.department_id = d.department_id
order by last_name
) the_employees
from departments d
order by department_name;

Now with Oracle 9i, you can take advantage of the CURSOR expression in your PL/SQL; Example 2 shows a
rewrite of the original nested FOR loop using these expressions. Though this approach requires more lines of
code, and is arguably less easy to proofread than the sequentially programmed implementation, it has this
advantage: There's only one SQL statement, and so it can be optimized more effectively than (what the SQL
engine sees as) two unconnected SQL statements. Table 1 below presents an explanation of the more
interesting code lines in Example 2.

Example 2. Use of CURSOR expression to retrieve all data in a single query.

1 declare
2 cursor the_departments is
3 select
4 department_name,
5 cursor (
6 select last_name
7 from employees e
8 where e.department_id = d.department_id
9 order by last_name
10 )
11 from departments d
12 where department_name in ( 'executive', 'marketing' )
13 order by department_name;
14
15 v_department_name departments.department_name%type;
16 the_employees sys_refcursor;
17
18 type employee_last_names_t is table of employees.last_name%type
19 index by binary_integer;
20
21 v_employee_last_names employee_last_names_t;
22 begin
23 open the_departments;
24
25 loop
26 fetch the_departments into v_department_name, the_employees;
27 exit when the_departments%notfound;
28
29 show (v_department_name);
30
31 fetch the_employees bulk collect into v_employee_last_names;
32
33 for indx in v_employee_last_names.first .. V_employee_last_names.last
34 loop
35 show (v_employee_last_names (indx));
36 end loop;
37 end loop;

65
38
39 close the_departments;
40 end;

Table 1. Explanation of code in Example 2.

Line Description
CURSOR expression inside the query means that the second element in the SELECT list for the cursor is
5
actually a cursor or result set in its own right.
Use of the Oracle 9i, pre-defined weak REF CURSOR type, SYS_REFCURSOR. This way, we don't have to
16 declare our own TYPE xxx IS REF CURSOR. These types are used to declare cursor variables
(the_employees is a cursor variable).
Our fetch statement fetches into the cursor variable. This means that it now points to cursor, which in
26
turn identifies a result set of its own.
This cursor (the_employees) is opened implicitly by Oracle, so we can immediately perform a fetch
31 (in this case, a bulk fetch) to extract all the employee last names into our local collection (defined on
lines 18-19).
33-
Use a numeric FOR loop to scan through the bulk-collected names and process them.
36

Using a Cursor Expression as an Actual Parameter to a PL/SQL


Function

A cursor variable (that is, a variable of type ref cursor) points to an actual cursor, and may be used as a
formal parameter to a PL/SQL procedure or function. A cursor expression defines an actual cursor. (Both
these statements are true pre-Oracle 9i.) So we'd expect that it would be possible to invoke a PL/SQL
procedure or function that has a formal parameter of type ref cursor with a cursor expression as its actual
parameter, thus:

My_Function ( cursor ( select my_col from my_tab ) )

In fact, this wasn't allowed under any circumstances pre-Oracle 9i (attempts would result in the ORA-22902
exception). New in Oracle 9i, it is now allowed under certain circumstances--namely, when the function (it
can't be a procedure) is invoked in a top- level SQL statement.

Given a function that can be invoked as follows:

declare
the_cursor sys_refcursor;
n number;
begin
open the_cursor for
select my_col from my_tab;
n := My_function (the_cursor);
close the_cursor;
end;

it can now be called directly within the query, like this:

66
select 'my_function' my_function
from dual
where
my_function (
cursor (
select my_col
from my_tab ) )
= 1;

or:

select 'my_function' my_function


from dual
order by
my_function (
cursor (
select my_col
from my_tab ) );

Most significantly, this syntax is now allowed in the invocation of a table function in the FROM list of a
SELECT statement, which we'll explore later in the article.

The "Young Managers" Scenario

Consider the requirement to find those managers in the employees table, the majority of whose direct
reports were hired before the manager. The algorithm depends on finding the direct reports for each
manager and comparing the number who were hired before him with the number who were hired after him.
This can be programmed straightforwardly in PL/SQL using classical techniques. (Note that, seeking to use
enhanced Oracle 9i functionality, this is implemented using a single SQL SELECT that has a cursor subquery
for the reports of a given manager.) This approach allows the production of a report, or as is illustrated,
populating a table with the results.

But suppose the requirement is more subtle: to create a VIEW to represent managers as specified, so that it
can be leveraged in ad hoc queries representing the current state of the underlying data. In fact, the
requirement in this scenario can be implemented in pure SQL using only SQL functions such as SUM and
DECODE. There are some rules that are too complex to implement by DECODE, in which case the user could
write his own function.

But this approach, though it works, feels back-to-front! Unlike the classical approach described previously,
it doesn't model the simple statement of the algorithm, and is therefore hard to write and to proofread. A
more comfortable approach is to define a view as follows:

create view young_managers as


select ...
from employees managers
where most_reports_before_manager(
< stuff for this manager > ) = 1;

We can do this classically like so:

create view young_managers as


select ...
from employees managers
where most_reports_before_manager
(
managers.employee_id, managers.hire_date

67
) = 1;

or by passing a cursor expression as the actual parameter to a function whose formal parameter is of type
REF CURSOR (see Example 3), giving us something more like this:

create view young_managers as


select ...
from employees managers
where most_reports_before_manager
(
cursor ( < select statement > ),
managers.hire_date
) = 1;

Example 3. Using a function with a ref cursor parameter in a WHERE clause.

create or replace function Most_Reports_Before_Manager (


report_hire_dates_cur in sys_refcursor,
manager_hire_date in date )
return number
is
type report_hire_date_t is table of employees.hire_date%type
index by binary_integer;
report_hire_dates report_hire_date_t;
before integer:=0; after integer:=0;
begin
fetch report_hire_dates_cur bulk collect into report_hire_dates;
if report_hire_dates.count > 0
then
for j in report_hire_dates.first..report_hire_dates.last
loop
case report_hire_dates(j) < manager_hire_date
when true then before:=before+1;
else after:=after+1;
end case;
end loop;
end if;
case before > after
when true then return 1;
else return 0;
end case;
end Most_Reports_Before_Manager;

create or replace view young_managers as


select managers.employee_id manager_employee_id
from employees managers
where Most_Reports_Before_Manager
(
cursor ( select reports.hire_date from employees reports
where reports.manager_id = managers.employee_id
),
managers.hire_date
) = 1;

The Example 3 approach isn't possible before Oracle 9i. Its advantage over the approach of using a classical
function in a WHERE clause is marginal rather than dramatic: It offers greater potential for reuse in that its
logic is expressed in terms of, and depends only on, the select list for an arbitrary SELECT, whereas the
classical approach hard-codes the SELECT. And, since there's only one SQL statement, this can be optimized
more effectively than (what the SQL engine sees as) two unconnected SQL statements (as discussed earlier).

68
The dramatic benefit of the new Oracle 9i feature allowing a cursor expression as an actual parameter to a
PL/SQL function comes in connection with table functions, which we'll now explore.

Table Functions: Recap

Suppose we have two schema-level types, a tuple analogous to a table row and a table of these, defined as
follows:

create type lookup_row as


object ( idx number, text varchar2(20) );
create type lookups_tab as
table of lookup_row;

We can then write a PL/SQL function that returns an instance of the table as shown in Example 4. While the
example doesn't reflect a "real-world" scenario, it's intended to emphasize the fact that you can model
arbitrarily complex logic within the function, something that can be quite the challenge in pure SQL.

Example 4. A function that returns a collection of information.

create or replace function lookups_fn return lookups_tab


is
v_table lookups_tab;
begin
/*
to extend a nested table, you must use the built-in
procedure extend, but to extend an index-by table,
you just specify larger subscripts.
*/
v_table := lookups_tab (lookup_row (1, 'one'));

for j in 2 .. 9
loop
v_table.extend;

if j = 2
then
v_table (j) := lookup_row (2, 'two');
elsif j = 3
then
v_table (j) := lookup_row (3, 'three');
elsif j = 4
then
v_table (j) := lookup_row (4, 'four');
elsif j = 5
then
v_table (j) := lookup_row (5, 'five');
elsif j = 6
then
v_table (j) := lookup_row (6, 'six');
elsif j = 7
then
v_table (j) := lookup_row (7, 'seven');
else
v_table (j) := lookup_row (j, 'other');
end if;
end loop;

return v_table;
end lookups_fn;

69
We can then invoke it in the FROM list of a SELECT statement thus:

select *
from table (
cast ( lookups_fn()
as lookups_tab ) );

This allows a table to be synthesized by computation. For example, the function might call Utl_File
procedures (to parse data that can't be handled by the SQL*Loader utility or by the external table feature), or
it might call C routines (via the callout framework) that access arbitrary external data sources. Or it might
access database tables and perform transformations that can't be expressed with pure SQL and SQL
functions. The SELECT statement can be used to define a view, and/or combined with other tables in the
FROM list in an arbitrarily complex SQL statement.

A table function, then, is a PL/SQL function that can be invoked in the FROM clause of a SQL SELECT clause.
We'll see later that a table function that exploits new Oracle 9i functionality, which we expect all table
functions to do, can only be invoked in the FROM clause of a SQL SELECT clause.

Pipelined Table Functions: New in Oracle 9i

The preceding functionality is available pre-Oracle 9i. However, it has the limitation that the function must
run to completion, storing all the rows it computes in the PL/SQL table before even the first row can be
delivered. (There are other limitations, discussed shortly.) Oracle 9i introduces the pipelined construct that
allows the procedure to be rewritten as shown in Example 5.

Example 5. A pipelined function.

create or replace function lookups_fn return lookups_tab


pipelined
is
v_row lookup_row;
begin
for j in 1..10
loop
v_row :=
case j
when 1 then lookup_row ( 1, 'one' )
...
when 7 then lookup_row ( 7, 'seven' )
else lookup_row ( j, 'other' )
end;
pipe row ( v_row );
end loop;
return;
end lookups_fn;

Thus each row is delivered as soon as it's ready, so that the response time characteristics of a table function
are symmetrical with those of a row source based on a table scan or an index scan. (For performance, the
PL/SQL runtime system delivers the rows from a pipelined table function in batches.)

Note: The procedure body now mentions only rows (that is, not the table), and the table is just implied by
the return type. (For elegance, the IF construct has been replaced with the new CASE formulation.) The same
syntax as shown earlier can be used to select from the table function, but it can now be simplified thus:

select * from table ( Lookups_Fn );

70
The invocation will be written Lookups_Fn() in the following to emphasize its status as a function.

Oracle 9i also introduces the possibility to create a table function that returns a PL/SQL type (that is, one
that's defined in a PL/SQL block rather than at schema level), as shown in Example 6.

Example 6. A table function that returns a PL/SQL type.

create or replace package My_Types is


type lookup_row is record ( idx number, text varchar2(20) );
type lookups_tab is table of lookup_row;
end My_Types;

create or replace function Lookups_Fn return My_Types.lookups_tab


pipelined
is
v_row My_Types.lookup_row;
begin
for j in 1..10
loop
case j
when 1 then v_row.idx := 1; v_row.text := 'one';
...
when 7 then v_row.idx := 7; v_row.text := 'seven';
else v_row.idx := j; v_row.text := 'other';
end case;
pipe row ( v_row );
end loop;
return;
end Lookups_Fn;

In the limit, a PL/SQL type may be defined in the declare section of an anonymous block and hence have no
persistence. However, to be useful in connection with table functions, the PL/SQL types must be declared in
a package, and so when discussing table functions, they're usually referred to as package-level types (in
contrast to schema-level types).

Note: A table function that returns a package-level type must be pipelined. Moreover, the simpler SELECT
syntax (without the CAST) must be used.

Piping Data from One Table Function to the Next

Also new to Oracle 9i, a table function may now be defined with an input parameter of type ref cursor and
invoked with a cursor expression as the actual parameter. Consider the code shown in Example 7.

Example 7. A transformative pipelined function.

create or replace function Mappings_Fn ( p_input_rows in sys_refcursor )


return My_Types.lookups_tab
pipelined
is
v_in_row My_Types.lookup_row;
v_out_row My_Types.lookup_row;
begin
/*
The following causes...
PLS-00361: IN cursor 'P_INPUT_ROWS' cannot be OPEN'ed
(The system opens the cursor on invoking the function.)
*/
--open p_input_rows;
71
loop
fetch p_input_rows into v_in_row;
exit when p_input_rows%notfound;

case v_in_row.idx
when 1 then v_out_row.idx := 1*2; v_out_row.text := 'was one';
when 2 then v_out_row.idx := 2*3; v_out_row.text := 'was TWO';
when 3 then v_out_row.idx := 3*4; v_out_row.text := 'was three';
when 4 then v_out_row.idx := 4*5; v_out_row.text := 'was FOUR';
when 5 then v_out_row.idx := 5*6; v_out_row.text := 'was five';
when 6 then v_out_row.idx := 6*7; v_out_row.text := 'was SIX';
when 7 then v_out_row.idx := 7*8; v_out_row.text := 'was seven';
else v_out_row.idx :=
v_in_row.idx*10; v_out_row.text := 'was other';
end case;
pipe row ( v_out_row );
end loop;
close p_input_rows;
return;
end Mappings_Fn;

Suppose t is a table that supports a select list compatible with My_Types.lookup_row. We can now invoke
the table function thus:

select *
from table (
mappings_fn (
cursor (
select idx, text
from t ) ) );

Of course, t might have been a view defined thus:

create or replace view t


as
select * from table ( lookups_fn() );

which implies the more compact syntax shown in Example 8.

Example 8. Passing a query as an argument.

create or replace view v as


select *
from table ( Mappings_Fn ( cursor ( select * from table ( Lookups_Fn() ) ) ) );

Data can be piped from one to the next of an arbitrary number of table functions daisy- chained in
succession. And, due to the pipelining feature, storage of intermediate results is avoided. Table functions
can thus be used to implement the Extraction, Transformation, and Load (a.k.a. ETL) operation for building
a data warehouse from OLTP data. In the limit, the extraction table function would access a foreign data
source as discussed earlier.

The "Young Managers" Scenario Revisited: Table Function Approach

We can now use yet another approach! The complete solution can be implemented in a table function. This
has the usability advantage of keeping all the logic in one place, and the performance advantage of invoking
the function only once rather than once per row in the table (see Example 9). This was derived
"mechanically" simply by creating an appropriate PL/SQL table type and by creating the block as a

72
pipelined function to return that type, substituting pipe row ( manager_employee_id ) for insert into
young_managers values ( manager_employee_id ).

Example 9. Using a table function.

create or replace package My_Types is


type employee_ids_tab is table of employees.employee_id%type;
end My_Types;

create or replace function Young_Managers_Fn


return My_Types.employee_ids_tab
pipelined
is
cursor managers is
select
employee_id, hire_date,
cursor (
select hire_date
from employees reports
where reports.manager_id = managers.employee_id
)
from employees managers;

manager_employee_id employees.employee_id%type;
manager_hire_date employees.hire_date%type;
reports sys_refcursor;
type report_hire_date_t is table of employees.hire_date%type
index by binary_integer;
report_hire_dates report_hire_date_t;
before integer; after integer;
begin
open managers;
loop
before:=0; after:=0;
fetch managers into manager_employee_id, manager_hire_date, reports;
exit when managers%notfound;
fetch reports bulk collect into report_hire_dates;
if report_hire_dates.count > 0
then
for j in report_hire_dates.first..report_hire_dates.last
loop
case report_hire_dates(j) < manager_hire_date
when true then before:=before+1;
else after:=after+1;
end case;
end loop;
end if;
if before > after then
pipe row ( manager_employee_id ); end if;
end loop;
close managers;
return;
end Young_Managers_Fn;

create or replace view young_managers as


select column_value manager_employee_id from table ( Young_Managers_Fn() );

The function can be made more general by giving it a ref cursor input parameter and by passing in the
cursor expression as the actual parameter (see Example 10). This would allow it to be "pointed at" any table
that expressed a hierarchy where both parent and child have a date.

73
Example 10. Using a table function with a ref cursor Input parameter.

create or replace function Young_Managers_Fn ( managers in sys_refcursor )


return My_Types.employee_ids_tab
pipelined
is
manager_employee_id employees.employee_id%type;
manager_hire_date employees.hire_date%type;
reports sys_refcursor;
type report_hire_date_t is table of employees.hire_date%type
index by binary_integer;
report_hire_dates report_hire_date_t;
before integer; after integer;
begin
loop
before:=0; after:=0;
fetch managers into manager_employee_id, manager_hire_date, reports;
exit when managers%notfound;
fetch reports bulk collect into report_hire_dates;
if report_hire_dates.count > 0
then
for j in report_hire_dates.first..report_hire_dates.last
loop
case report_hire_dates(j) < manager_hire_date
when true then before:=before+1;
else after:=after+1;
end case;
end loop;
end if;
if before > after then
pipe row ( manager_employee_id ); end if;
end loop;
close managers;
return;
end Young_Managers_Fn;

select column_value manager_employee_id from table


(
Young_Managers_Fn
(
cursor
(
select
employee_id, hire_date,
cursor (
select hire_date
from employees reports
where reports.manager_id =
managers.employee_id
)
from employees managers
)
)
);

Note: An attempt to create a view as this SELECT statement currently fails with "ORA-22902: CURSOR
expression not allowed," where the exception is raised because the SELECT statement that's the argument of
the CURSOR formal parameter to the table function itself has a cursor expression (a.k.a. cursor subquery). A
view can be created when the SELECT statement doesn't have a cursor subquery (see the earlier
Mappings_Fn example).

74
Fanout: Using Table Functions with Side Effects

Sometimes the specification for the transformation to be implemented as a table function explicitly excludes
source data with certain characteristics. In such cases, it's useful to report on the excluded source data and
often most convenient to direct the report to the database for further analysis. A table function may do
DML, provided that this is done within an autonomous transaction, as shown in Example 11.

Example 11. Using autonomous transactions to allow for intended "side effects."

create or replace function lookups_fn_with_side_effect


return my_types.lookups_tab
pipelined
/*
uses...
create table exclusions ( n number );
*/
is
pragma autonomous_transaction;
v_row My_Types.lookup_row;
begin
for j in 1..15
loop
case
when j < 11 then
case j
when 1 then v_row.idx := 1; v_row.text := 'one';
when 2 then v_row.idx := 2; v_row.text := 'TWO';
when 3 then v_row.idx := 3; v_row.text := 'three';
when 4 then v_row.idx := 4; v_row.text := 'FOUR';
when 5 then v_row.idx := 5; v_row.text := 'five';
when 6 then v_row.idx := 6; v_row.text := 'SIX';
when 7 then v_row.idx := 7; v_row.text := 'seven';
else v_row.idx := j; v_row.text := 'other';
end case;
pipe row ( v_row );
else
insert into exclusions values ( j );
end case;
end loop;
commit;
return;
end Lookups_Fn_With_Side_Effect;

Parallelizing Table Function Execution: New in Oracle 9i

It's beyond the scope of this article to describe the details of Oracle's parallel query feature. Suffice it to say
that when certain environment conditions are met (especially a hardware environment that supports multiple
concurrently executing processes making concurrent disk accesses, and a user environment close to single-
user) and when the objects referenced in a query have appropriate parallel attributes, then the elapsed time
for long-running queries can be cut in direct proportion to the number of available CPUs. This is especially
significant in Decision Support Systems (a.k.a. DSS) both at query time and in the Extraction,
Transformation, and Load (a.k.a. ETL) operations to populate them.

Oracle 9i introduces table function features to allow their execution to be parallelized.

75
These features require (with one small exception, discussed shortly) that the table function has exactly one
strongly typed ref cursor input parameter. Let's take a look at the different ways in which table functions
can be designed for parallel execution.

Special Case: Function Behavior is Independent of Input Data


Partitioning

Consider a function that processes each row from its input cursor individually. (Such a transformation,
which generates two or more output rows from each input row- generically referred to as piviotting-benefits
particularly from a table function implementation.) The syntax to parallelize this is straightforward and is
shown in Example 12.

Example 12. Parallel execution for arbitrary partitioning of data.

create or replace function Rowwise_Xform_Fn (


p_input_rows in SYS_REFCURSOR )
return My_Types.xforms_tab
pipelined
parallel_enable ( partition p_input_rows by any )
is
v_in_row My_Types.input_row;
v_out_row My_Types.xform_row;
begin
loop
fetch p_input_rows into v_in_row;
exit when p_input_rows%notfound;
v_out_row.n := v_in_row.n*2;
v_out_row.typ := 'a';
pipe row ( v_out_row );

v_out_row.n := v_in_row.n*3;
v_out_row.typ := 'b';
pipe row ( v_out_row );
end loop;
close p_input_rows;
return;
end Rowwise_Xform_Fn;

See Example 13 for the complete working example. They keyword any expresses the programmer's
assertion that the results are independent of the order in which the function gets the input rows. When this
keyword is used, the runtime system randomly partitions the data among the query slaves. This keyword is
appropriate for use with functions that take in one row, manipulate its columns, and generate output row(s)
based on the columns of this row only. (Of course, if this assertion doesn't hold, the output won't be
predictable.) This is the small exception referred to earlier: The input ref cursor need not be strongly
typed to be partitioned by any.

Example 13. Algorithm is independent of the ordering of the source rows.

create or replace package My_Types is


type input_row is record ( n number );
type cur_t is ref cursor return input_row;

type xform_row is record ( n number, typ char(1) );


type xforms_tab is table of xform_row;
end My_Types;

create table t ( n number );

76
begin
for j in 1..1000
loop insert into t ( n ) values ( j ); end loop;
commit;
end;

create or replace function Rowwise_Xform_Fn (


p_input_rows in My_Types.cur_t )
return My_Types.xforms_tab
pipelined
parallel_enable ( partition p_input_rows by any )
is
v_in_row My_Types.input_row;
v_out_row My_Types.xform_row;
begin
loop
fetch p_input_rows into v_in_row;
exit when p_input_rows%notfound;
v_out_row.n := v_in_row.n * 2; v_out_row.typ := 'a';
pipe row ( v_out_row );
v_out_row.n := v_in_row.n * 3; v_out_row.typ := 'b';
pipe row ( v_out_row );
end loop;
close p_input_rows;
return;
end Rowwise_Xform_Fn;

select * from table (


Rowwise_Xform_Fn ( cursor ( select n from t ) ) )
where rownum < 11;

The ability to exploit the parallel potential of a table function depends on whether the source can be
parallelized.

General Case: Function Behavior Depends on Input Data Partitioning

Consider a transformation along the lines of:

select avg ( salary ), department_id


from employees
group by department_id;

where the aggregation operation to be performed on the set of salaries for a given department is arbitrarily
complex such that a classical SQL implementation is impossible, slow by virtue of a function invocation for
each row of the source table, or prohibitively challenging to write and debug. For example, it might be that
the cost to the employer of paying a given salary depends on the hire date because of changes in benefits
packages that affect only employees hired after the date of change. This is illustrated in Example 14, but to
avoid obscuring it with a complicated algorithm, the aggregation is simply the sum for the salary for each
distinct department. This has the general form shown in Example 15.

Example 14. Algorithm requires only that the source rows are clustered.

Note: In order to avoid having to make the algorithm distractingly complex, the following DELETE should be
issued:

delete from employees where department_id is null;

before continuing thus:


77
create or replace package My_Types is
type dept_sal_row is record
( sal number(8,2), dept number(4) );
type cur_t is ref cursor return dept_sal_row;

type dept_sals_tab is table of dept_sal_row;


end My_Types;

create or replace function Aggregate_Xform


( p_input_rows in My_Types.cur_t )
return My_Types.dept_sals_tab
pipelined
--[ cluster / order ] p_input_rows by (dept)
--parallel_enable
-- ( partition p_input_rows by [ hash / range] (dept) )
is
g_in_row My_Types.dept_sal_row;
g_out_row My_Types.dept_sal_row;
g_first_time boolean := true;
g_last_dept number := null;
g_got_a_row boolean;
g_new_dept boolean;
g_current_dept employees.department_id%type;
g_prev_dept employees.department_id%type;
v_total_sal number;

procedure Get_Next_Row is begin


fetch p_input_rows into g_in_row;
g_got_a_row := not p_input_rows%notfound;
if g_got_a_row
then
case g_first_time
when true then
g_first_time := false;
g_new_dept := false;
else
g_new_dept := g_prev_dept <> g_in_row.dept;
end case;
g_prev_dept := g_in_row.dept;
end if;
return;
end Get_Next_Row;

function Got_Next_Dept return boolean is begin


g_current_dept := g_in_row.dept;
g_new_dept := false;
return g_got_a_row;
end Got_Next_Dept;

function Got_Next_Row_In_Dept return boolean is begin


return ( not g_new_dept ) and g_got_a_row;
end Got_Next_Row_In_Dept;

begin
Get_Next_Row();
while Got_Next_Dept()
loop
v_total_sal := 0;
while Got_Next_Row_In_Dept()
loop
v_total_sal := v_total_sal + g_in_row.sal;
Get_Next_Row();
end loop;

78
g_out_row.sal := v_total_sal;
g_out_row.dept := g_current_dept;
pipe row ( g_out_row );
end loop;
close p_input_rows;
return;
end Aggregate_Xform;

Example 15. General form of a function performing an aggregate transformation.

create or replace function Aggregate_Xform (


p_input_rows in My_Types.cur_t )
return My_Types.dept_sals_tab
pipelined
is
...
begin
Get_Next_Row();
while Got_Next_Dept() /* relies on assumption that
all rows for given dept are
delivered consecutively */
loop
v_total_sal := 0;
while Got_Next_Row_In_Dept()
loop
v_total_sal := v_total_sal + g_in_row.sal;
Get_Next_Row();
end loop;
g_out_row.sal := v_total_sal;
g_out_row.dept := g_current_dept;
pipe row ( g_out_row );
end loop;
close p_input_rows;
return;
end Aggregate_Xform;

Given that the input rows will be partitioned between different slaves, the integrity of the algorithm requires
that all the rows for a given department go to the same slave, and that all these rows are delivered
consecutively. (Strictly speaking, the requirement for consecutive delivery is negotiable, but the design of
the algorithm to handle this case would need to be much more elaborate. For that reason, Oracle commits to
consecutive delivery.) We use the term clustered to signify this type of delivery, and cluster key for the
column (in this case, "department") on which the aggregation is done. But significantly, the algorithm does
not care in what order of cluster key it receives each successive cluster, and Oracle doesn't guarantee any
particular order here.

This allows the possibility of a quicker algorithm than if rows were required to be clustered and delivered in
order of the cluster key. It scales as order N rather than order N.log(N), where N is the number of rows. The
syntax to accomplish this is shown here:

create or replace function aggregate_xform (


p_input_rows in my_types.cur_t )
return my_types.dept_sals_tab
pipelined
cluster p_input_rows by (dept)
parallel_enable
( partition p_input_rows by hash (dept) )
is ...

79
We can choose between hash (dept) and range (dept) depending on what we know about the distribution of
the values. (Hash will be quicker than range and is the natural choice to be used with cluster... by.) Here, to
be partitioned by a specified column, the input ref cursor must be strongly typed. Cluster... by isn't allowed
without parallel_enable (partition... by).

Note: At version 9.0.1, it's necessary to include ORDER BY on the cluster key in the SELECT used to invoke
the table function as follows, in order to preserve correctness of behavior, but this restriction will be
removed when the order N clustering algorithm is productized:

select * from table (


Aggregate_Xform (
cursor (
select salary, department_id
from employees
where department_id is not null
order by department_id ) ) );

Order By Versus Cluster By

This alternative syntax is also allowed:

create or replace function my_fn (


p_input_rows in my_types.cur_t )
return my_types.items_tab
pipelined
order p_input_rows by (c1)
parallel_enable
( partition p_input_rows by range (c1) )
is...

This means that those rows that are delivered to a particular slave as directed by partition... by will be
locally sorted by that slave, thus parallelizing the sort. Therefore, there should be no ORDER BY in the
SELECT used to invoke the table function. (To have one would subvert the attempt to parallelize the sort.)
Thus it's natural to use the range option together with the order by option. This will be slower than cluster
by, and so should be used only when the algorithm depends on it.

Note: The cluster... by construct can't be used together with the order... by in the declaration of a table
function. This means that an algorithm that depends on clustering on one key, c1, and then on ordering
within the set row for a given value of c1 by, say, c2 would have to be parallelized by using the order... by
in the declaration in the table function. Here, we'd use:

create or replace function median (


p_input_rows in my_types.cur_t )
return my_types.items_tab
pipelined
order p_input_rows by (c1,c2)
parallel_enable
( partition p_input_rows by range (c1) )
is...

The current restriction preventing using cluster... by together with order... by implies no loss of
functionality, but only a missed opportunity to leverage the order N sort.

Caution: It's possible to design an algorithm for a table function that would deliver a different number of
rows according to the degree of parallelism. The simplest example is a function that returns a table of

80
NUMBER representing the count of the rows its input cursor delivered. A non-parallelized version would
deliver just one row giving count(*) for the input table. A parallelized version would deliver N rows (where
N is the degree of parallelism), the sum of whose values would give count(*) for the input table. However,
this breaks the parallel query abstraction. Oracle recommends against programming this way.

Syntax for Table Function Based on Schema-Level Type

When a table function is written to return a schema-level type, the syntax required to invoke it is somewhat
verbose. For completeness, it's illustrated here.

The Lookups_Fn and Mappings_Fn Example

(This example has been rewritten to return schema-level types)

Since the query syntax for an object table is rather verbose, we recap it here using a table.

create table t of lookup_row;


insert into t values ( lookup_row ( 1, 'one' ) );
insert into t values ( lookup_row ( 2, 'TWO' ) );
insert into t values ( lookup_row ( 3, 'three' ) );
insert into t values ( lookup_row ( 4, 'FOUR' ) );
insert into t values ( lookup_row ( 5, 'five' ) );
insert into t values ( lookup_row ( 6, 'SIX' ) );
insert into t values ( lookup_row ( 7, 'seven' ) );
insert into t values ( lookup_row ( 8, 'other' ) );
insert into t values ( lookup_row ( 9, 'other' ) );
insert into t values ( lookup_row ( 10, 'other' ) );
commit;

/* this is how an object query should be written */


select VALUE(a) rec from t a;

/* because it's verbose, it's convenient to define a view */


create or replace view v as
select value(a) rec from t a;

/* test the view */


select * from v;

Now the example proper:

create type lookup_row as


object ( idx number, text varchar2(20) );

create type lookups_tab as table of lookup_row;

create or replace function Lookups_Fn


return lookups_tab
pipelined
is
v_row lookup_row;
begin
for j in 1..10
loop
v_row :=
case j
when 1 then lookup_row ( 1, 'one' )
when 2 then lookup_row ( 2, 'TWO' )
when 3 then lookup_row ( 3, 'three' )
81
when 4 then lookup_row ( 4, 'FOUR' )
when 5 then lookup_row ( 5, 'five' )
when 6 then lookup_row ( 6, 'SIX' )
when 7 then lookup_row ( 7, 'seven' )
else lookup_row ( j, 'other' )
end;
pipe row ( v_row );
end loop;
return;
end Lookups_Fn;

Note the syntax of the query. Since the table function returns an object, it follows from the syntax against an
object table above. Again, it's convenient to encapsulate it in a view:

create or replace view lookups as


select value(a) rec
from table
(
cast ( Lookups_Fn() as lookups_tab )
) a;

select * from lookups;

create or replace function Mappings_Fn


( p_input_rows in sys_refcursor )
return lookups_tab
pipelined
is
v_in_row lookup_row;

/* always initialize an object type using a


type constructor or user defined constructor */
v_out_row lookup_row := lookup_row( 1, 'x' );
begin
loop
fetch p_input_rows into v_in_row;
exit when p_input_rows%notfound;
case v_in_row.idx
when 1 then v_out_row.idx := 1*2;
v_out_row.text := 'was one';
when 2 then v_out_row.idx := 2*3;
v_out_row.text := 'was TWO';
when 3 then v_out_row.idx := 3*4;
v_out_row.text := 'was three';
when 4 then v_out_row.idx := 4*5;
v_out_row.text := 'was FOUR';
when 5 then v_out_row.idx := 5*6;
v_out_row.text := 'was five';
when 6 then v_out_row.idx := 6*7;
v_out_row.text := 'was SIX';
when 7 then v_out_row.idx := 7*8;
v_out_row.text := 'was seven';
else v_out_row.idx :=
v_in_row.idx*10;
v_out_row.text := 'was other';
end case;
pipe row ( v_out_row );
end loop;
close p_input_rows;
return;
end Mappings_Fn;

82
Note the syntax of the query. It's most compactly expressed using the views v or lookups defined earlier.

select value(b)
from table
(
cast
(
Mappings_Fn
(
cursor ( select * from lookups )
)
as lookups_tab
)
) b;

For completeness, here's how it looks without the view:

select value(b) from table


(
cast
(
Mappings_Fn
(
cursor
( select value(a) from table
(
cast ( Lookups_Fn() as lookups_tab )
) a
)
)
as lookups_tab
)
) b;

We can create a VIEW mapped_lookups with this SELECT statement, and then access it without restriction
from PL/SQL. For example:

declare
cursor table_fn_cur is
select * from mapped_lookups;
rec lookup_row;
begin
open table_fn_cur;
loop
fetch table_fn_cur into rec;
exit when table_fn_cur%notfound;
Print ( rec.idx, rec.text );
end loop;
close table_fn_cur;
end;

Note, however, that this simpler syntax doesn't work here:

for rec in ( select * from mapped_lookups ) loop

Business Benefits of Table Functions and Cursor Expressions

• Cursor expressions allow encapsulation of logic for reuse in compatible query situations, giving
increased developer productivity and application reliability.
83
• Table functions give increased functionality by allowing sets of tuples from arbitrary external data
sources and sets of tuples synthesized from arbitrary computations to be invoked (as if they were a
table) in the FROM list of a SELECT clause. For convenience, they can be used to define a VIEW, giving
new functionality.
• Table functions can be used to deliver the rows from an arbitrarily complex PL/SQL transformation
sourced from Oracle tables (including, therefore, other table functions) as a " VIEW," without storage
of the calculated rows. This gives increased speed and scalability, and increased developer
productivity and application reliability.
• Taking the "VIEW" metaphor a step further, the input parameters to the table function allow the
"VIEW" to be parameterizable, increasing code reusability and therefore increasing developer
productivity and application reliability.
• A table function with a ref cursor input parameter can be invoked with another table function as the
data source. Thus table functions can be daisy-chained, allowing modular program design and hence
increased ease of programming, reuse, and application robustness.
• Table function execution can be parallelized giving improved speed and scalability. This, combined
with the daisy-chaining feature, makes table functions particularly suitable in data warehouse
applications for implementing Extraction, Transformation, and Load operations.
• Fanout (DML from an autonomous transaction in the table function) adds functionality of
particular interest in data warehouse applications.
• A table function allows data stored in nested tables to be queried as if it were stored relationally, and
data stored relationally to be queried as if it were stored as nested tables. (This will be illustrated in
the code samples for the next article.) This allows genuine independence between the format for the
persistent storage of data and the design of the applications that access it. (A VIEW can be defined on
a table function, and INSTEAD OF triggers can be created on the VIEW to complete the picture.)

Table functions and cursor expressions expand in important new ways our ability as PL/SQL developers
both to improve performance and reuse critical business logic. These features are particularly important if
you rely on parallelization in your application and want to fully exploit PL/SQL-based functionality.

84
Oracle9i: HTTP Communication from Within the Oracle Database
The B2B (business-to-business) component of e-Business (which is still going strong, even if there is much
less IPO hype about it) depends on automated communication between business sites across the public
Internet. In other words, distributed components need to be able to communicate with each other without
any need for manual, human intervention.

HTTP (the Hypertext Transfer Protocol) offers a standard set of rules for exchanging files (such as text and
multimedia files) on the Web. Web servers contain an HTTP daemon, which waits for requests and responds
to them when they arrive at a site served by that Web server. An HTTP client, such as a Web browser, can
submit requests for files or for actions to be taken.

In a B2B implementation, the requestor (sometimes called the consumer) is a mechanical version of the
familiar Web browser. This article explains how to implement the requestor in an Oracle 9i database using
Utl_Http. The other partner in the dialogue, the provider, is implemented using the same technology as any
Web site, and is not our focus in this article. The requestor/provider relationship is just another example of
the familiar remote procedure call (RPC) paradigm. The requestor is the invoker of the remote procedure,
and the provider is its implementor. The exciting thing is that now we're doing remote procedure calls across
the public Internet.

To fully automate this process, however, you need standardized-or at least agreed- upon-semantics for
communicating requests and understanding responses. Though partners in a particular B2B relationship
could define standards for their protocols from scratch, the de facto standard that has emerged is to rely on
XML documents (eXtended Markup Language) for both request and reply.

Oracle offers technology to allow both the requestor and the provider to straightforwardly implement their
services backed by an Oracle 9i database, and using only PL/SQL on top of fully elaborated APIs. The
simplest way to code the provider is to use mod_plsql, either directly via the HTTP listener component of
the Oracle 9i database or via Oracle9iAS, and to write a PL/SQL stored procedure that's exposed as the
URL representing the request. The XML document payload expressing the request is decoded; the database
is accessed to supply the reply information and is updated appropriately; and the reply is encoded and sent
using Htp.Print or a similar mechanism. A detailed discussion of this end of the dialogue is beyond the
scope of this article, and, of course, the provider could be implemented using entirely non-Oracle
technology.

We provide and explain a complete working example at the end of this article. If you're lucky enough to
have Oracle 9i database installations on machines at two distinct locations, then you'll be able to see the
communication between requestor and provider take place across the public Internet. (The code will work
fine with both requestor and provider in the same database, of course, but you'll have to use your
imagination a little to supply the realism!)

The request is typically sent (or more likely queued and then sent later) in the body of a database trigger,
which fires on an event like a stock level falling below the defined threshold for reordering. The XML
document expressing the request is encoded by accessing current database values and sent, typically using
the "POST" method to ensure that an arbitrarily large XML request can be sent piecewise. Authentication
information (for instance, username and password) is likely to be required as part of the request. And
possibly the request header will need to be explicitly set to reflect an agreed-upon protocol. Then the
response is (started to be) fetched, and its status code is checked for errors, and its header is checked for
protocol compliance. Then the arbitrarily large XML document expressing the response itself is fetched
piecewise, decoded, and the information is used to update the database. A robust implementation is likely to
have a component that automatically sends a generated email to a system administrator in the event of an

85
error. Oracle has features for encoding and decoding XML, and for sending email from the database, but,
again, these are beyond the scope of this article.

Depending on the design of the workflow, state may need to be represented. For example, a customer may
request a price and delivery date for a given quantity of items from several vendors. Each vendor would
reply with price and delivery date and with an "offer good to" date. When the customer site sends a request
to the selected vendor to place a definite order, it will need to refer to the specific offer. If such a scheme is
used within a single organization-for example, to communicate between databases at local offices in
different countries-then the communication protocol can be designed from scratch, and most likely an offer
reference number will be exchanged as part of the XML encoding. However, if the partners in the B2B
relationship are completely independent, and especially if the relationship is casual, then the requestor will
have to follow whatever protocol the provider has defined. It may be that the provider has implemented the
state that represents an ongoing dialogue using cookies. In this case, the requestor will need to handle these
programmatically.

The Utl_Http Package

The Utl_Http package pre-Oracle 9i allowed a basic implementation of the requestor site. It allowed an
arbitrarily large response to be handled piecewise in a PL/SQL VARCHAR2. But it supported only the "GET"
method-that is, it didn't support sending arbitrarily large messages in the body of the request. And it didn't
support authentication, setting the header of the request, inspecting the status code and header of the
response, or dealing with cookies. Oracle 9i adds support for all these (including optionally fetching the
response "as is" into a PL/SQL RAW), and beyond that provides full support for the semantics that can be
expressed via HTTP. For example, persistent HTTP connections are now supported. Use of these gives
dramatic speed and scalability improvement for applications that repeatedly and frequently make HTTP
requests to the same site. And users now have full control over the encoding of character data.

HTTP relies on an underlying transport layer. Thus the Utl_Http package (written in PL/SQL) is
implemented on top of the Utl_Tcp package. (The Utl_Smtp package for sending email from the database
is implemented in the same fashion.) Pre-Oracle9i, Utl_Tcp was implemented in Java. At Oracle9i, it has
been re-implemented natively-that is, in C directly on top of the socket layer-to improve its performance.

Later in the article, we'll provide a code sample that shows how to model the requestor at SQL*Plus, and
that can be used to inspect the return status and content of an arbitrary password-protected URL.

Encoding of Character Data

In the classical client/server architecture, the database and the client may use different encoding schemes to
represent character data. For example, in a Japanese application, the database might use (a variety of) the
EUC character set, and the client might use (a variety of) the SJIS character set. Thus character-encoding
conversion is required. The solution is well known and long established: Oracle Net transparently handles
the conversion (as specified by the database character set and the NLS_LANG client environment variable). A
corresponding issue exists for Utl_Http. When a request is sent, it may need to be encoded differently than
the database character set (because the requestor knows that the target URL requires this). And when a
response is received, it may again be encoded differently than the database character set (because that's the
non-negotiable behavior of the target URL).

86
There are two areas of concern when sending a request: the URL and the request body. When sending by the
"GET" method, all request parameterization is via the URL itself, typically after the ? delimiter. Search
terms, for example, are normally handled this way. HTTP defines no convention for specifying different
character sets for the URL and expects that everything is 7-bit ASCII. Other character-encoding schemes
should be represented as the hex codes of their bytes using the %nn notation. (The sender of the request
must know from documentation which character set the URL expects to decode from the hex
representation.) Oracle 9i introduces the Utl_Url package, which has functions to convert from the database
character set to a hex-coded representation of a specified character set, and vice versa. In addition, these
functions handle the conversion of the reserved symbols: percent (%), semi-colon (;), slash (/), question
mark (?), colon (:), at sign (@), ampersand (&), equals sign (=), plus sign (+), dollar sign ($), and comma (,).

When sending by the "PUT" method, the character set of the request body should be set via the charset
attribute of the Content-Type in the request header, using the new Utl_Http.Set_Header procedure. If this
is done, it gives Oracle sufficient information to transform appropriately when sending a character request
body (by using Utl_Http.Write_Text). If the charset attribute isn't set in the request header, then no
character set conversion takes place unless the user has catered for it via the overloaded procedure
Utl_Http.Set_Body_Charset. The variant Set_Body_Charset(charset varchar2)-a.k.a. the global
variant-allows the user to set a fallback character set, to be assumed, if no other information is provided, for
both requests and responses for the session. The variant Set_Body_Charset(r Utl_Http.Req, charset
varchar2)-a.k.a. the request variant-allows the user to insist on a character set for the body for this request.
(A record of PL/SQL type Utl_Http.Req is returned when the HTTP request is begun with
Utl_Http.Begin_Request.) The choice made via the request variant won't only override that made via the
global variant but will also override that made via the charset attribute of the request header. For this reason,
the recommended way to specify the character set conversion for the request body is via the charset attribute
of the header. Only if the user has a special reason for leaving this unspecified in the request header would
he use the request variant of Set_Body_Charset.

There's just one area of concern when receiving the response: the response body. If the implementation of
the URL is well-mannered, then the character set of the response body will be specified correctly in the
charset attribute of the Content-Type in the response header, accessible to the user via the procedure
Utl_Http.Get_Header. Oracle will implicitly perform the appropriate conversion in connection with
calling Utl_Http.Read_Text. However, this is often not set. In this case, the user can use the global variant
of Set_Body_Charset to determine the character set conversion. However, the charset attribute of the
response header is sometimes set wrong. (This is likely when pages in different character sets are served up
as files from the file system seen by the Web server, since the Content-Type header information will often
be set globally for the server with no mechanism to make it file-specific.) For this reason, a third overloaded
variant Set_Body_Charset(r Utl_Http.Resp,charset varchar2) is provided-a.k.a. the response
variant. (A record of PL/SQL type Utl_Http.Resp is returned when the HTTP response is received with
Utl_Http.Get_Response.) The choice made via the response variant will override that made via the global
variant and that expressed via the charset attribute of the response header.

Note: From Oracle 8i v8.1.6 and pre-Oracle 9i, Oracle detected the charset of the response body (if this was
specified) and used the information to do the character set conversion. And if the charset attribute of the
response body wasn't specified, then no conversion took place, and no overriding or fallback mechanism
was provided. Under special circumstances (for example, fetching a SJIS Japanese response where the
charset attribute isn't specified into an EUC database), problems arose pre- Oracle 9i.

Thus the user now has full control over all character set conversion issues. In an extreme case, where the
response body is Content-Type text/HTML and where the HTML <meta> tag is used to specify the
character set, the user can retrieve the response body into a PL/SQL RAW with Utl_Http.Read_Raw and

87
then write custom code to parse the HTML and to convert to the database character set in a PL/SQL
VARCHAR2 once the response character set is discovered.

A Closer Look at Utl_Http Calls

Example 1 shows: how to send an HTTP request-setting the proxy information, setting the method to
"GET," providing username/password authentication information, and setting the request header-and how to
get the response-retrieving the status code, the header information, and the response body. The "GET"
method is suitable for non-parameterized URLs or for URLs with a manageable volume of parameter name-
value pairs. The maximum length of the URL string is limited by the capacity of the PL/SQL VARCHAR2
variable used to pass it. The "POST" method is suitable for parameterizing the request with an arbitrarily
large volume of data, especially, for example, as might be the case when the request is expressed as an XML
document.

Example 1. A simple demonstration of Utl_Http's basic features.


DECLARE
req Utl_Http.req;
resp Utl_Http.resp;
NAME VARCHAR2 (255);
VALUE VARCHAR2 (1023);
v_msg VARCHAR2 (80);
v_url VARCHAR2 (32767) := 'http://otn.oracle.com/';
BEGIN
/* request that exceptions are raised for error Status Codes */
Utl_Http.set_response_error_check (ENABLE => TRUE );

/* allow testing for exceptions like Utl_Http.Http_Server_Error */


Utl_Http.set_detailed_excp_support (ENABLE => TRUE );

Utl_Http.set_proxy (
proxy => 'www-proxy.us.oracle.com',
no_proxy_domains => 'us.oracle.com'
);
req := Utl_Http.begin_request (url => v_url, method => 'GET');

/*
Alternatively use method => 'POST' and Utl_Http.Write_Text to
build an arbitrarily long message
*/
Utl_Http.set_authentication (
r => req,
username => 'SomeUser',
PASSWORD => 'SomePassword',
scheme => 'Basic',
for_proxy => FALSE /* this info is for the target Web server */
);
Utl_Http.set_header (r => req, NAME => 'User-Agent', VALUE => 'Mozilla/4.0');
resp := Utl_Http.get_response (r => req);

DBMS_OUTPUT.put_line ('Status code: ' || resp.status_code);


DBMS_OUTPUT.put_line ('Reason phrase: ' || resp.reason_phrase);

FOR i IN 1 .. Utl_Http.get_header_count (r => resp)


LOOP
Utl_Http.get_header (r => resp, n => i, NAME => NAME, VALUE => VALUE);
DBMS_OUTPUT.put_line (NAME || ': ' || VALUE);
END LOOP;

BEGIN

88
LOOP
Utl_Http.read_text (r => resp, DATA => v_msg);
DBMS_OUTPUT.put_line (v_msg);
END LOOP;
EXCEPTION
WHEN Utl_Http.end_of_body
THEN
NULL;
END;

Utl_Http.end_response (r => resp);


EXCEPTION
/*
The exception handling illustrates the use of "pragma-ed" exceptions
like Utl_Http.Http_Client_Error. In a realistic example, the program
would use these when it coded explicit recovery actions.

Request_Failed is raised for all exceptions after calling


Utl_Http.Set_Detailed_Excp_Support ( ENABLE=>FALSE )
And it is NEVER raised after calling with ENABLE=>TRUE
*/
WHEN Utl_Http.request_failed
THEN
DBMS_OUTPUT.put_line (
'Request_Failed: ' || Utl_Http.get_detailed_sqlerrm
);
/* raised by URL http://xxx.oracle.com/ */
WHEN Utl_Http.http_server_error
THEN
DBMS_OUTPUT.put_line (
'Http_Server_Error: ' || Utl_Http.get_detailed_sqlerrm
);
/* raised by URL http://otn.oracle.com/xxx */
WHEN Utl_Http.http_client_error
THEN
DBMS_OUTPUT.put_line (
'Http_Client_Error: ' || Utl_Http.get_detailed_sqlerrm
);
/* code for all the other defined exceptions you can recover from */
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line (SQLERRM);

A B2B example

Picture a community of customers and vendors. Each customer buys items from one or many vendors, and
each vendor sells items to one or many customers. Customers and vendors maintain inventory data in Oracle
databases. The community has come to appreciate the benefits of e-Business and has agreed to standardize
on a way to express the variety of requests and replies that need to be sent, as shown in Table 1.

Table 1. B2B participants and their requests and replies.


Participant Request Reply
This is customer #1
Our ref #nnnnn
customer #1 (to many Can you supply me with item X?
vendors):
What is your price for N units?
When can you deliver?

89
This is vendor #1
vendor #1 Your ref #nnnnn
We don't stock item X
This is vendor #2
Your ref #nnnnn
N units of item X will cost you Y
vendor #2
We can deliver by D1
This offer good until D2
Our offer ref #mmmm
This is customer #1
Our ref #ppppp
customer #1 to vendor #2 Your offer ref #mmmm
Supply me with N units of item
X
This is vendor #2 Your ref #ppppp
Order for N units of item X confirmed
vendor #2 to customer #1
Our order ref #qqqqq

They agree to adopt XML via HTTP as the communication format and transport mechanisms since this
supports all current messaging requirements and is readily extensible, without requiring changes to the basic
message exchange and parsing mechanisms.

They recognize, furthermore, that occasionally the attempt to send a message to a particular site will fail (for
instance, because that site is down). They require, as a result, that an administrator at the sending site be
notified of the failure by automatically generated email.

Implementation Concept

A customer database event will trigger the sending of a message. The message will be constructed from
current database values. It will be sent to one or several password- protected URLs using the Utl_Http API-
URL and password data to be retrieved from the database.

The vendor URL will be implemented as a PL/SQL procedure via mod_plsql and Htp.Print, etc. This
procedure will parse the message and access vendor database values to compose the reply and will record
data about the reply in the database.

The customer will parse the return message and update the customer database values accordingly. If an error
is detected, then the Utl_Smtp API will be used to alert the administrator by email.

Simplified Scenario

One customer communicates with one vendor. The sample will be more convincing if the customer and
vendor sites are implemented in different databases on different machines, as shown in the following script:

connect system/manager@customer_site
create user customer identified by customer;
grant resource, connect to customer;

connect system/manager@vendor_site
create user vendor identified by vendor;
grant resource, connect to vendor;

90
This approach will, of course, work with both pieces in a single database.

Setting Up the Customer Site

The customer inventory is represented in a single stock_levels table (stock_levels.sql). A trigger on this
table fires when the stock level of an item falls below a threshold.

The trigger computes the number of items to be ordered and inserts a row into the customer_orders tables,
which represents an orders queue (customer_orders.sql). The order number is generated from the
order_ref_seq sequence number (order_ref_seq.sql). The customer orders queue will be consumed
periodically by the scan_customer_orders procedure (scan_customer_orders.sql). This could be
automatically scheduled using the Dbms_Job API, but the code sample requires it to be executed manually.

The procedure calls the submit_order procedure for each item to be ordered (submit_order.sql).
Submit_order assembles the message, getting the appropriate XML tags as package constants from the tags
package. It also retrieves the vendor URL and password data from the vendors table (vendors.sql).

Note: You will need to edit the INSERT statement for the vendors table (shown in Example 2) to correctly
specify the node where you've created the vendor user.

Example 2. Setting up the vendors table.


INSERT INTO vendors
(vendor_id, url, the_user, PASSWORD)
VALUES (1,
--
-- modify this data for your site
--
'http://bllewell-sun.us.oracle.com/pls/vendor/receive_order',
--
-- Uncomment this line to fabricate an error and cause an error
-- email to be sent
-- 'http://bllewell-sun.us.oracle.com/pls/vendor/Nonexistent',
'my_vendor',
'my_password');

Note: A simple version of the submit_order procedure is provided for comparison in


submit_order_simple_version.sql. It uses only Utl.Http.Request, which was available in Oracle 8i. The
full version of the submit_order relies on features introduced in Utl.Http in Oracle 9i. All calls to the
Oracle 9i Utl.Http API are bundled in a grandchild procedure named get_http_request
(get_http_request.sql).

Let's take a look at some of the steps in get_http_request that demonstrate the extended power of
Utl_Http in Oracle 9i.

At the very beginning of the executable section, we set up the HTTP session: First, set the error-check
response level so that if a call to Utl_Http.Get_Response results in a Web server returning a state
indicating failure, Oracle will raise an exception. We also call Utl_Http to set the proxy Web server (you'll
want to change the settings you see in the following code block):

BEGIN /* executable section */


Utl_Http.set_response_error_check (
ENABLE => TRUE );

Utl_Http.set_proxy (

91
proxy
=> 'www-proxy.us.oracle.com',
no_proxy_domains
=> 'us.oracle.com'
);

Now it's time to start a new HTTP request:

v_req := Utl_Http.begin_request (
url => p_url, method => 'GET');

Set authentication for this session based on the provided username and password:

Utl_Http.set_authentication (
r => v_req,
username => p_user,
PASSWORD => p_password,
scheme => 'Basic'
);

Set the HTTP request header and then obtain the response from the Web server. The v_resp variable is a
record of type Utl_Http.resp, with fields named status_code, reason_phrase, and http_version.

Utl_Http.set_header (
r => v_req,
NAME => 'User-Agent',
VALUE => 'Mozilla/4.0'
);
v_resp := Utl_Http.get_response (r => v_req);

At this point, you may prefer to test v_resp.status_code explicitly. The constant Utl.Http.Http_Ok
(which has the value 200) means "OK." You may also want to program explicit action for various other
error values of v_resp.status_code. Since no action is taken here, we simply rely on the fact that if there's
an error, Oracle will raise an exception, ORA-29268, with a message such as "HTTP client error nnn -
<some explanation>."

Assuming that the response came back without error, we then loop through all the different headers in the
response and make sure that they're all of type XML, as shown in Example 3.

Example 3. Confirm response headers for XML document types.


FOR i IN 1 .. Utl_Http.get_header_count (r => v_resp)
LOOP
Utl_Http.get_header (
r => v_resp,
n => i,
NAME => v_name,
VALUE => v_value
);

IF LOWER (v_name) = 'content-type'


THEN
IF INSTR (LOWER (v_value), 'text/xml') < 1
THEN
RAISE_APPLICATION_ERROR (
-20997,
'Get_Http_Request:
unexpected Content-Type: ' || v_value
);
92
END IF;
END IF;
END LOOP;

Once we make sure that the content in the response conforms to what's expected (and what can be handled),
it's time to obtain the XML document itself. We deliberately declared a small buffer variable
VARCHAR2(80) to illustrate piecewise fetch logic. In a more typical implementation, you'd use a large
buffer (maximum size is 32,767) so that you can handle arbitrarily long messages.

BEGIN
LOOP
Utl_Http.read_text (
r => v_resp,
data => v_buffer);
v_msg :=
v_msg || v_buffer;
END LOOP;
EXCEPTION
WHEN Utl_Http.end_of_body
THEN
NULL;
END;

When we're finished retrieving the HTTP response text, we end the response and return the value:

Utl_Http.end_response (r => v_resp);


RETURN v_msg;

The return message is then parsed using the parse_message procedure (parse_message.sql), and the
resulting information is used to update the orders queue.

On error, an email is sent automatically using the send_error_email procedure (see send_error_email.sql).
This procedure relies on the Demo_Mail package code sample, available on the Oracle Technology Network.

Be sure to edit the Customizable Section in send_error_mail.sql for the SMTP host and domain for your
environment.

Vendor Site

The vendor implements the URL in the receive_order procedure (receive_order.sql) via mod_plsql and
Htp.Print.

Make sure that this basic mechanism is properly configured by compiling and testing a simple mod_plsql
URL, such as that shown in Example 4 (hello.sql).

Example 4. Simple "hello" procedure to test mod_plsql mechanism.


CREATE OR REPLACE PROCEDURE hello
IS
-- http://bllewell-sun.us.oracle.com/pls/vendor/hello
BEGIN
Htp.Print (
'<head><title>hello</title></head><body>'
|| 'Hello. This is vendor #1'
|| '</body></html>'
);
END hello;

93
The receive_order procedure parses an incoming message using the parse_message package
(parse_message.sql) and updates the vendor orders table (vendor_orders.sql) accordingly. It composes
a return message, using the appropriate XML tags as directed by the named constants in the tags package.

Note: The customer message is sent in this code sample as the value in a name- value parameter pair using
the "GET" method. This works fine for the concrete data provided. A realistic implementation should cater
to the possibility that the message to be sent is arbitrarily long, and so would use the "POST" method to
send the message in the body of the HTTP request. The Utl_Http API supports this. However, the
programming of the procedure that implements the URL would need to be correspondingly more elaborate.

Test the System

First, test the sending of email from the database, which you can do by calling send_error_email as shown
here:

connect customer/customer@customer_site
BEGIN
Send_Error_Mail (
12345, 'This is a test' );
END;
/

The complete end-to-end test involves the following steps:

1. Update the curr_stock_level for a row in the customer's stock_levels table so that it falls below
threshold_stock_level.
2. Trigger the message exchange.
3. Check the customer_orders and vendor_orders tables.

The script found in b2b_test.sql and shown in Example 5 will run the preceding test.

Example 5. End-to-end test script.


CONNECT customer/customer@customer_site

UPDATE stock_levels
SET curr_stock_level = 50
WHERE scu = 1;
COMMIT ;

UPDATE stock_levels
SET curr_stock_level = 9
WHERE scu = 1;
COMMIT ;

SET Serveroutput On
EXECUTE Scan_Customer_Orders

COLUMN o format 9999999999


COLUMN v format 999
COLUMN scu format 999
COLUMN q format 999
COLUMN d format a24
COLUMN s format a10
COLUMN msg format a60
SET Wrap On
SET LineSize 140

94
SELECT order_ref o, vendor_id v, scu, quantity q,
TO_CHAR (order_date, 'hh:mi:ss::DD-Mon-YYYY')
d, status s, err_msg msg
FROM customer_orders;

COLUMN m format a140


SELECT err_msg m FROM customer_orders;

---------------------------------------------------------

CONNECT vendor/vendor@vendor_site
SELECT * FROM vendor_orders;

If you execute b2b_test.sql immediately after running customer_install.sql and vendor_install.sql, you
should see something like the content shown in Example 6 and Example 7 in the customer_orders and
vendor_orders table, respectively.

Example 6. Content in the customer_orders table after script execution.


O V SCU Q D S MSG
------- ---- ---- ---- ------------------------ ---------- --------
1234567 1 1 41 12:51:23::08-Nov-2001 submitted

Example 7. Content in the vendor_orders table after script execution.


ORDER_REF CUSTOMER_ID SCU QUANTITY ORDER_DAT STATUS
--------- ----------- ---------- ---------- --------- ------
1234567 1 1 41 08-NOV-01 new

To test the exception reporting and the automatic sending of email, fabricate an error condition. A simple
way to do this is to update the vendors table using a URL such as that shown in Example 8.

Example 8. A URL that will fabricate an error condition.


http://bllewell-sun.us.oracle.com:7777/pls/vendor/Nonexistent

Once you've finished, run customer_install.sql, vendor_install.sql, and finally b2b_test.sql. You should then
see data such as that shown in Example 9 in the customer_orders table. You should also receive a
corresponding email.

Example 9. Resulting data in the customer_orders table.


D S MSG
------------------------ ------ --------------------------------------------
01:45:11::08-Nov-2001 failed ORA-29268: HTTP client error 404 - Not Found

PL/SQL at the Heart of Internet Applications

The Utl_Http package further enhances the ability of a PL/SQL-based application to integrate with and
operate at the very heart of an Internet-centric application. With Utl_Http, you can implement the requestor
site in a B2B transaction.

Pre Oracle 9i, the Utl_Http package supported enough functionality to implement the sending of a basic
B2B request and the receipt of the response. Oracle 9i adds:

• The "POST" method to handle arbitrarily long requests


• Authentication
• Access to return status code
95
• RAW reply
• Cookie support

In other words, it provides full support for the semantics that can be expressed via HTTP. It also adds full
functionality for character-set conversion for request and reply.

With the addition of these new features, Utl_Http can now support arbitrarily complex requirements for the
requestor site.

96
Oracle 9i Release 2 Developments for PL/SQL Collections

Introduction

For the first several years after Steven published the first edition of Oracle PL/SQL Programming in 1995,
he evangelized the use of packages as a fundamental building block of PL/SQL-based applications. This
seemed to be a critical message for a number of years, as relatively few developers knew about and used
packages. Lately, it appears that the "package story" has caught on; most developers do deploy the majority
of their functionality from within packages. In the course of querying students and presentation attendees
about their programming habits, however, Steven has discovered another very helpful aspect of PL/SQL
that's being drastically under-utilized: the collection.

A PL/SQL collection is, in its essence, very similar to a single-dimensioned array. A collection allows you
to maintain lists of information, and it can be used to improve query performance and also simplify the code
you write to manage data within a PL/SQL program. These collection data structures come in three flavors:
nested table, varying array (a.k.a. VARRAY), and associative array. We can't, within the scope of this article,
offer a complete introduction to collections. You can obtain such coverage from any number of PL/SQL
texts and the Oracle documentation. Our intention in this article is, instead, to let you know about some very
interesting developments in Oracle 9i Release 2 regarding PL/SQL collections.

We will, in fact, focus on one particular type of collection: the associative array. For those of you who have
worked with collections over the years, this will be an unfamiliar term. Some of you will remember back in
Oracle 7 when collections were first introduced; at that point, they were called "PL/SQL tables," since they
were similar to very simple relational tables (consisting of a single column) but were declared and
manipulated only within PL/SQL programs. Then in Oracle 8, Oracle introduced two other kinds of single-
dimensioned lists (VARRAYS and nested tables). In the process, they changed the name of the original
collection type from "PL/SQL table" to "index-by table" (reflecting its mandatory INDEX BY
BINARY_INTEGER clause).

VARRAYS and nested tables can be used both in schema-level declarations (especially, for example, for the
type of a column of a relational table) and in PL/SQL declarations. Index-by tables can be used only in
PL/SQL declarations. Now, in Oracle 9i Release 2, Oracle has once again renamed this collection type, this
time from "index-by table" to "associative array." Why another change? The term associative array is the
name commonly used in other programming languages (including Perl, C++, JavaScript, and Cymbal, to
name a few) to refer to a data structure that stores pairs of keys and values. By making this change, PL/SQL
becomes more consistent with the nomenclature of much of the programming world, and therefore makes
PL/SQL a bit more accessible to developers who are new to PL/SQL, but have experience in other
languages.

Associative arrays can still be used only in PL/SQL declarations. The wonderful development regarding
collections in Oracle 9i Release 2 is, however, not in the name change, but in some significant new
functionality. Let's take a look.

Declaring Associative Arrays

In the bad old days, there was just one way to declare an associative array:

DECLARE
TYPE names_list_t IS
TABLE OF employee.last_name%TYPE
INDEX BY BINARY_INTEGER;
97
The "INDEX BY BINARY_INTEGER" clause was fixed and unchangeable. This meant that the only index
allowed on an associative array was the row number, and the row number had to have been declared as
BINARY_INTEGER.

There were several drawbacks to this scheme. First, it required reliance on an outmoded datatype, since
BINARY_INTEGER has since been superseded by PLS_INTEGER as a more efficient integer datatype. Second,
it meant that if the list being manipulated had a non-integer key, the developer had to write some very
complex and/or compute-intensive logic (namely, to perform full collection scans or create alternative
indexes via hashing) to take advantage of collections.

These restrictions have now been lifted. You can now declare associative arrays to be indexed by
BINARY_INTEGER, PLS_INTEGER, VARCHAR2 and even anchored declarations of those types using %TYPE. All
of the following statements are valid declarations of associative array types with integer indexes:

DECLARE
TYPE array_t1 IS
TABLE OF NUMBER
INDEX BY BINARY_INTEGER;

TYPE array_t2 IS
TABLE OF NUMBER
INDEX BY PLS_INTEGER;

TYPE array_t3 IS
TABLE OF NUMBER
INDEX BY POSITIVE;

TYPE array_t4 IS
TABLE OF NUMBER
INDEX BY NATURAL;
BEGIN
...
END;

And, very interestingly, if you do declare a type based on a constrained BINARY_INTEGER subtype, such as
POSITIVE, then, if you try to reference a negative row number, you'll get an error, as shown here:

SQL> DECLARE
2 TYPE pos_only_t IS
3 TABLE OF NUMBER
4 INDEX BY POSITIVE;
5
6 pos_only pos_only_t;
7 BEGIN
8 pos_only (-9) := 1;
9 END;
10 /
DECLARE
*
ERROR at line 1:
ORA-06502: PL/SQL: numeric or value error
ORA-06512: at line 8

Note: Even at Oracle 8i Version 8.1.7, the syntax to use subtypes of BINARY_INTEGER for an index-by table
was allowed, but the implied constraint wasn't enforced.

You can even use a user-defined subtype, thus:

98
DECLARE
SUBTYPE my_integer IS PLS_INTEGER NOT NULL;
TYPE array_t4 IS
TABLE OF NUMBER
INDEX BY my_integer;
BEGIN
...
END;

So PL/SQL is now much more flexible than it used to be when it comes to declaring integer-indexed
collections. (Note: There are still restrictions. See the section titled "Invalid declarations" for reminders of
which syntax is still not deemed acceptable.)

Much more exciting, however, is the fact that you can now declare associative arrays to have VARCHAR2 or
string index values! Here are some examples of such declarations:

DECLARE
TYPE array_t1 IS
TABLE OF NUMBER
INDEX BY VARCHAR2(64);

TYPE array_t3 IS
TABLE OF NUMBER
INDEX BY VARCHAR2(32767);

TYPE array_t4 IS
TABLE OF NUMBER
INDEX BY employee.last_name%TYPE;
BEGIN
...
END;
/

It's especially impressive that Oracle now lets us use %TYPE to declare an associative array with a VARCHAR2
index. This allows us to avoid hard-coding a VARCHAR2 maximum length in the TYPE statement.

Invalid Declarations

There are still many INDEX BY clauses that aren't valid, even if the datatype is, ostensibly, consistent or can
be converted to something consistent with VARCHAR2 or BINARY_INTEGER. You won't be able to declare an
associative array type based on any of the following clauses:

INDEX BY NUMBER
INDEX BY INTEGER
INDEX BY DATE
INDEX BY VARCHAR2
INDEX BY CHAR(n)
INDEX BY <some table>.<some column>%TYPE

where <some column> isn't of a type that can by used explicitly as the target of INDEX BY.

Working with VARCHAR2-Indexed Collections

We'll finish up this article by examining a scenario in which VARCHAR2-indexed collections are put to use.
First, however, let's walk through the example of declaring and using such a collection shown in Example 1.

99
Line(s) Description
Associative array type declaration. Each row of a collection based on this type contains a string of
2
up to 64 characters.
Declarations of two collections based on the population_type. The first list contains the populations
4-5
of countries. The second list contains the populations of continents.
Populate individual rows in both the country and continent population lists. Notice that the row
"numbers" aren't numbers, but are instead the names of countries and continents. Notice that we
10-18
assign a value in line 15 to the "Antarctica" row in the continents collection and then we override
that value on line 17.
Obtain and display the number of rows in the collection. All the traditional collection methods may
20-21 be used with VARCHAR2-indexed collections. COUNT still returns the number of rows in the
collection
Obtain and display information about the first and last defined rows in the continent collection. This
takes some getting used to. The FIRST and LAST methods don't return integer values; instead, they
23-31
return the string value that is the lowest or highest in the sort order defined for the current character
set in the database.
33-38 Iterate through all the defined rows in the collection, using the NEXT method.

Example 1. Example of VARCHAR2-indexed collection.


1 DECLARE
2 idx VARCHAR2(64);
3 TYPE population_type IS TABLE OF NUMBER INDEX BY idx%TYPE;
4
5 country_population population_type;
6 continent_population population_type;
7
8 howmany PLS_INTEGER;
9 BEGIN
10 country_population('Norway') := 4000000;
11 country_population('Greenland') := 100000;
12 country_population('Iceland') := 750000;
13
14 continent_population('Australia') := 30000000;
15
16 continent_population('Antarctica') := 1000;
17
18 continent_population('Antarctica') := 1001;
19
20 howmany := country_population.COUNT;
21 DBMS_OUTPUT.PUT_LINE ('COUNT = ' || howmany);
22
23 idx := continent_population.FIRST;
24 DBMS_OUTPUT.PUT_LINE ('FIRST row = ' || idx);
25 DBMS_OUTPUT.PUT_LINE (
26 'FIRST value = ' || continent_population(idx));
27
28 idx := continent_population.LAST;
29 DBMS_OUTPUT.PUT_LINE ('LAST row = ' || idx);
30 DBMS_OUTPUT.PUT_LINE (
31 'LAST value = ' || continent_population(idx));
32
33 idx := country_population.FIRST;
34 WHILE idx IS NOT NULL
100
35 LOOP
36 DBMS_OUTPUT.PUT_LINE ( idx || ' = ' || country_population(idx) );
37 idx := country_population.NEXT(idx);
38 END LOOP;
39 END;

Here's the output one would see in SQL*Plus when running this script with SERVEROUTPUT turned ON:

COUNT = 3
FIRST row = Antarctica
FIRST value = 1001
LAST row = Australia
LAST value = 30000000
Greenland = 100000
Iceland = 750000
Norway = 4000000

Using VARCHAR2-Indexed Collections

So why would a developer care about the fact that you can now index by VARCHAR2 in addition to
PLS_INTEGER? First of all, in general, you'll want to take advantage of associative arrays when you need to
maintain any sort of lists of data in your PL/SQL programs. Sure, you can use relational tables to manage
lists, but they involve much more programming and CPU overhead. The code you write for associative
arrays is lean and mean.

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.

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. Of course, the Oracle
9i Database provides a solution with SQL and a B*-tree index. But there's a set of scenarios where
considerable performance improvement can be obtained by instead using an explicit PL/SQL
implementation. This was true even before the new features discussed in this article were available. These
scenarios are characterized by very frequent lookups in a relatively small set of values, usually in
connection with flattening a relational representation for reporting or for UI presentation.

For this article, we'll work with an English-French vocabulary and translation mechanism. Suppose we have
a set of English-French vocabulary pairs stored persistently in the most obvious way in a schema-level table:

-- translations.sql
CREATE TABLE translations (
english varchar2(200),
french varchar2(200));

and we have data in the table as follows (populated by the translations.sql file):

SELECT * FROM translations;


101
ENGLISH FRENCH
-------------------- ----------
computer ordinateur
tree arbre
book livre
cabbage chou
country pays
vehicle voiture
garlic ail
apple pomme
desk ‚scritoire
furniture meubles

Our task is to allow lookup from French to English, and to allow efficient addition of new vocabulary pairs.
We'll immediately turn to the package construct to provide a clean interface to this functionality, as shown
in Example 2.

Example 2. The vocab package interface to the French-English translation engine.


CREATE OR REPLACE PACKAGE vocab
IS
FUNCTION lookup (p_english IN VARCHAR2)
RETURN VARCHAR2;

PROCEDURE new_pair (
p_english IN VARCHAR2, p_french IN VARCHAR2);
END vocab1;
/

The vocab.new_pair procedure performs a straightforward insert into the table:

PROCEDURE new_pair (
p_english IN VARCHAR2,
p_french IN VARCHAR2)
IS
BEGIN
INSERT INTO translations
(english, french)
VALUES (p_english, p_french);
END new_pair;

This vocabulary is, furthermore, static during the user's session (that is, during the time when the user
application accesses the translation table, no changes are being made to this table). Our challenge then
becomes: 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.
• 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.

102
In the following sections, we'll examine each of these approaches, implemented in distinct packages
(vocab1 through vocab4). Then we'll execute a test script that compares the performance of these
implementations.

The Pure SQL Approach

The vocab1 package offers the traditional, pure SQL approach to solving this problem:

CREATE OR REPLACE PACKAGE BODY vocab1


IS
FUNCTION lookup (
p_english IN VARCHAR2)
RETURN VARCHAR2
IS
v_french
translations.french%TYPE;
BEGIN
SELECT french
INTO v_french
FROM translations
WHERE english = p_english;

RETURN v_french;
END lookup;
...
END;

This is a wonderfully simple implementation and demonstrates the elegance of the SQL language. Each time
Lookup is invoked, we make a roundtrip between PL/SQL and SQL, involving a "context switch." Oracle
has been working hard to reduce the overhead of this switch, but you still pay a price. It would also be very
convenient if this most straightforward of approaches is also the most efficient. We'll soon find out!

Linear Search in index-by-binary_integer Table

If we identify the context switch between PL/SQL and SQL as a potential performance problem, then we
should seek ways to avoid that switch. One way to do this is to cache all the data in program memory,
thereby removing the need for repeated SQL access. To do this, we must:

1. Retrieve all the data from our static vocabulary table.


2. Store the data in a session-persistent, PL/SQL data structure (the most appropriate in this case being
the index-by table).
3. Scan through that collection to find the French word and then return the associated English word.

This technique has been available ever since Oracle 7.3.4. The implementation is contained in the
vocab2.sql script file and is shown in Example 3. Here are high-level explanations of the major areas of
functionality:

Line(s) Description
3-10 Declare the index-by table type, the collection itself, and a query used to initialize the collection.
Use a FOR loop to scan through the collection linearly. As soon as a match is found, RETURN that
12-23 value. For those of you who prefer a more structured approach using a WHILE loop, you'll find such
an implementation in the vocab2.sql script file. It's slightly less efficient than the FOR loop.

103
Initialize the collection by transferring the contents of each row to the collection. This happens only
26-35 the first time the lookup function (or any other program in the package) is called. Then the data
persists in the collection.

Example 3. Using a linear search algorithm.


1 CREATE OR REPLACE PACKAGE BODY vocab2
2 IS
3 TYPE word_list IS TABLE OF translations%ROWTYPE
4 INDEX BY BINARY_INTEGER /* can't use pls_integer pre-9.2 */;
5
6 g_english_french word_list;
7
8 CURSOR trans_cur
9 IS
10 SELECT english, french FROM translations;
11
12 FUNCTION lookup (p_english IN VARCHAR2)
13 RETURN VARCHAR2
14 IS
15 BEGIN
16 FOR indx IN 1 .. g_english_french.LAST ()
17 LOOP
18 IF g_english_french (indx).english = p_english
19 THEN
20 RETURN g_english_french (indx).french;
21 END IF;
22 END LOOP;
23 END lookup;
24
25 BEGIN /* package initialization */
26 DECLARE
27 indx PLS_INTEGER := 0;
28 BEGIN
29 FOR rec IN trans_cur
30 LOOP
31 indx := indx + 1;
32 g_english_french (indx).english := rec.english;
33 g_english_french (indx).french := rec.french;
34 END LOOP;
35 END;
36 END vocab2;

As you can see, the algorithm is trivial, but does require a fair amount of coding. In Oracle 9i Release 2, we
can simplify and improve the performance of the initialization section, by the way, by using the BULK
SELECT to directly populate the collection, as is shown here:

BEGIN
OPEN trans_cur;
FETCH trans_cur BULK COLLECT INTO g_english_french;
CLOSE trans_cur;

This depends on the exciting new functionality in Oracle 9i that allows much wider use of RECORD binds in
SQL statements than previously was possible. (Here, g_english_french is a table of RECORDs of the same
shape as the translations table.) This will be the subject of our next article.

Hash-Based Lookup in index-by Table

104
The linear search algorithm suffers from the well-known disadvantage that on average we'll examine half
the elements in the index-by table before we find a match. A possible improvement is to maintain the
elements in lexical sort order and to use a binary chop algorithm: Compare the search target to the half-way
element to determine which half it's in; repeat this test recursively on the relevant half. This requires much
more elaborate coding-and testing-especially if you also need to write update and insert procedures that
modify the database table and the cached collection. This is the sort of code that one writes as an exercise in
a computer science class. One shouldn't have to go through such efforts when writing production code with
a language as robust and mature as PL/SQL.

And, in fact, we don't-even prior to Oracle 9i Release 2. An alternative path is available to us by taking
advantage of the Oracle hashing algorithm provided by the DBMS_UTILITY.GET_HASH_VALUE. Without
going into all the details, a hash algorithm transforms a string into a number. If you use the algorithm
correctly (give it enough distinct integer values to choose from), there's a very good chance (but no
guarantee; see the end of this section for details on "conflict resolution") that every distinct string will
convert to a distinct integer value. It's possible, therefore, to use hashing to construct our own string-based
index, which can then be used to look up the French word from the English.

You'll find the hash-based implementation in the vocab3.sql script, the key elements of which are shown in
Example 4. While it's beyond the scope of this article to offer a thorough explanation of the technique, the
following table provides an overview of those key elements.

Line(s) Description
Declarations of variables and constants used for consistent hashing, as well as the index-by table
3-8 holding the French translations. In this case, the row number of the collection will be the hashed
value of the English word.
The lookup function takes the English word, hashes it to an integer, and then returns the French
12-18
word found in that row. This makes more sense when you look at the initialization section.
The initialization section. In this case, for each row retrieved from the table, we hash the English
21-26
word into an integer and then deposit the corresponding French word into that row in the collection.

Example 4. Key elements of the hash-based lookup technique.


1 CREATE OR REPLACE PACKAGE BODY vocab3
2 IS
3 hash BINARY_INTEGER;
4 g_hash_base CONSTANT NUMBER := 1;
5 g_hash_size CONSTANT NUMBER := 1000000 ;
6
7 TYPE word_list IS TABLE OF translations.french%TYPE
8 INDEX BY BINARY_INTEGER;
9
10 g_english_french word_list;
11
12 FUNCTION lookup (p_english IN VARCHAR2) RETURN VARCHAR2
13 IS
14 BEGIN
15 hash := DBMS_UTILITY.get_hash_value (
16 p_english, g_hash_base, g_hash_size);
17 RETURN g_english_french (hash);
18 END lookup;
19
20 BEGIN /* package initialization */
21 FOR rec IN (SELECT english, french FROM translations)

105
22 LOOP
23 hash := DBMS_UTILITY.get_hash_value (
24 rec.english, g_hash_base, g_hash_size);
25 g_english_french (hash) := rec.french;
26 END LOOP;
27 END vocab3;

What a neat and clean algorithm! Unfortunately, it's a bit naive and definitely not suited for real-world use.
The sad fact of the matter is that no one has yet come up with a hashing algorithm that can guarantee that
two distinct values for the name IN parameter to get_hash_value will always produce distinct hash or
integer values. For hashing to be "ready for prime time," one has to implement a "conflict resolution"
algorithm (in other words, if "TABLE" and "CHAIR" both hash to the same number, you have to detect this
and store the information in separate, traceable rows).

Oracle doesn't provide built-in conflict resolution. On the other hand, it's not all that difficult to implement
this logic; one of the simplest techniques is called "linear probe." The altind.pkg script contains an
implementation of such conflict resolution logic; you'll only need to make minimal changes to adapt it to
your own circumstances.

Direct Lookup in index-by-varchar2 Table

Of course, a yet more elaborate ambition pre-Oracle 9i Database Release 2 would be- after studying the
relevant computer science textbooks-to implement a B*-tree structure in PL/SQL, horror of wheel re-
invention notwithstanding!

The datastructure might look like this:

type Node_t is record (


value varchar2(20),
left_child binary_integer /* refer to the array... */,
right_child binary_integer /* ...index of the... */,
parent binary_integer /* ...relevant element. */ );
type Btree_t is table of Node_t index by binary_integer;
the_tree Btree_t;

However, the implementation would be very far from trivial, and is certainly too long and complex for
inclusion in this article.

A far better approach-newly possible in Oracle 9i Database Release 2-is to use precisely the B*-tree
organization of the values but to do so implicitly via a language feature, the index-by-varchar2 table.

The index-by-varchar2 table is optimized for efficiency of lookup on a non-numeric key, where the
notion of sparseness isn't really applicable. The index-by-*_integer table (where now *_integer can be
either pls_integer or binary_integer), in contrast, is optimized for compactness of storage on the
assumption that the data is dense.

This implies that there might even be cases where, even though the key is inherently numeric, it's better to
represent it as an index-by-varchar2 table via a To_Char conversion.

You can think of the index-by-varchar2 table as the in-memory PL/SQL version of the schema-level
index organized table. Using this approach, we can produce a simple, intuitive package for both populating
and looking up the French translations of English words. See Example 5 and the following table for an
explanation of this virtually transparent code.

106
Line(s) Description
Declare a VARCHAR2-indexed collection type, and an instance of that type. In this case, each row of
3-5
the collection contains a word in French, and the index into that row is the word in English.
7-12 The lookup function simply returns the value found in the row for that English word.
The initialization section of the package deposits the French word in to the row indicated by the
15-18
English word.

Example 5. Using the VARCHAR2-indexed collection approach.


1 CREATE OR REPLACE PACKAGE BODY vocab4
2 IS
3 TYPE word_list IS TABLE OF translations.french%TYPE
4 INDEX BY translations.english%type;
5 g_english_french word_list;
6
7 FUNCTION lookup (p_english IN VARCHAR2)
8 RETURN VARCHAR2
9 IS
10 BEGIN
11 RETURN g_english_french (p_english);
12 END lookup;
13
14 BEGIN /* package initialization */
15 FOR j IN (SELECT english, french FROM translations)
16 LOOP
17 g_english_french (j.english) := j.french;
18 END LOOP;
19* END vocab4;

You can't really get much more straightforward than that! In fact, the body of function Lookup is now so
trivial that you may decide to dispense with it altogether (suppressing a mild qualm about violating some
rule of best practice, since you're exposing the collection in the package specification) and use this stripped-
down implementation (see vocab5.sql):

CREATE OR REPLACE PACKAGE vocab5


IS
TYPE word_list IS
TABLE OF translations.french%TYPE
INDEX BY translations.english%type;
lookup word_list;
END vocab5;
/

CREATE OR REPLACE PACKAGE BODY vocab5


IS
BEGIN /* package initialization */
FOR indx IN (
SELECT english, french
FROM translations)
LOOP
lookup (indx.english) :=
indx.french;
END LOOP;
END vocab5;
/

107
Note that it's not yet possible to use an index-by-varchar2 table as the target or source of a bulk-binding
construct. We can't, in other words, take advantage of the BULK COLLECT syntax to deposit all the rows of
translations directly into the lookup collection in a single roundtrip.

And the Winner Is...

Whew. OK. So we have now a total of five different implementations we can test for optimal performance:

1. Database lookup (vocab1.sql)


2. Linear search using FOR loop (vocab2.sql)
3. Hash index (vocab3.sql)
4. Associative array (VARCHAR2 index) via a function call (vocab4.sql)
5. Associative array (VARCHAR2 index) via direct access (vocab5.sql)

To compare performance, we take advantage of the DBMS_UTILITY.GET_TIME function, which helps us


calculate elapsed time down to the hundredth of a second. We've encapsulated this function inside a "timer
object," which is created in the tmr.ot script. This object type allows us to easily declare, start, and stop
multiple virtual timers inside our PL/SQL code, as shown here (a fragment of the vocab.tst timing script):

DECLARE
v translations.french%TYPE;
db_tmr tmr_t :=
tmr_t.make ('DB lookup', 10000);
...
BEGIN
db_tmr.go;
FOR indx IN 1 .. 10000
LOOP
v := vocab1.lookup ('computer');
END LOOP;
db_tmr.STOP;

When we run the vocab.tst script for 5,000, 7,500, and 10,000 iterations of the FOR loops (as shown earlier),
we get results that are typified by Table 1. Here are some observations on these results:

• Linear searches of collections should always be avoided, especially since, as of Oracle 9i Release 2,
the alternatives are so easy to write. Not only is the linear search very much slower than any of the
alternatives for a given number of iterations, but also (and even more to its detriment) it doesn't scale
linearly as the number of iterations increases.

Note: In the test as constructed here, the time for the linear lookup scales as "sum of 1-to-N" for N
iterations. It's easy to show that our reported numbers follow this pattern, either by doing some
algebra or by writing a trivial program to sum 1-to- 5000, 1-to-7500, and 1-to-10000. This is left as
an exercise for the reader!

• Database lookups supported by an appropriate index are very efficient (10,000 executions of the
query-based lookup function took less than two seconds)-they just aren't the most efficient path to
take.
• The associative array and hash-based lookups are all dramatically faster (an order of magnitude or
higher) than the database lookup. Their improvement over linear search is far greater.
• Removal of the function interface for the associative array (VARCHAR2 index) didn't appreciably
improve performance. You should avoid direct access to data structures and instead hide them
behind functions to improve overall maintainability of your code.

108
• Perhaps most impressive, the elapsed time for the index-supported database lookup and both the
hash-based and associative array lookups scaled linearly as the number of iterations increased by an
order of magnitude. These are all very stable algorithms.

Table 1. Performance comparison of five lookup approaches.

These measurements were made on a stand-alone, high-end laptop running Windows 2000. The times are in
seconds, and are the raw results of a single run of the test. Of course, they vary from run to run. Forgive us
for not taking the next step and quoting our figures with appropriate precision and standard deviations!

Method 5000 Iterations 7500 Iterations 10000 Iterations


DB lookup 0.80 1.13 1.52
Linear search 18.91 43.13 78.45
Hash Index 0.09 0.13 0.17
Assoc Array 0.05 0.09 0.11
Assoc Array (without function) 0.04 0.06 0.08

The bottom line for PL/SQL developers: By extending the flexibility of the collection syntax, storage, and
access, we're able to write much simpler code that is much more efficient than implementations possible in
earlier versions. It's incumbent upon all of us to become aware of these impressive new features, and then
figure out how to best integrate them into existing and new application development projects.

109
Oracle9i: Using PL/SQL Records in SQL Statements

The PL/SQL RECORD Datatype

A PL/SQL RECORD is a composite datatype. In contrast to a scalar datatype like NUMBER, a record 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. Records are very handy constructs for PL/SQL developers.

The easiest way to define a record is by using the %ROWTYPE syntax in your declaration. For example, the
following statement:

DECLARE
bestseller books%ROWTYPE;

creates a record that has a structure corresponding to the books table; for every column in the table, there's 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 preceding code and bestseller will take on the new structure
of that table.

Asecond way to declare a record is to define your own RECORD TYPE. One advantage of a user-defined TYPE
is that you can take advantage of native PL/SQL datatypes as well as derived values in the field list, as
shown here:

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 preceding user-defined record datatype includes a field ("title") that's 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 Oracle PL/SQL Programming, 3rd Edition.

Next, we can declare a record based on this type (you don't use %ROWTYPE in this case, because you're
already referencing a type to perform the declaration). Once you've declared a record, you can then
manipulate the data in these fields (or the record as a whole) as you can see here:

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 :=

110
bestseller;
END;

Note that in the preceding code we've used the structure of the books table to define our PL/SQL records,
but the assignment to the title field didn't in any way affect data inside that table. You should also be
aware that while you can assign one record to another, you couldn't perform comparisons or computations
on records. Neither of these statements will compile:

BEGIN
IF bestseller =
required_reading
THEN ...

BEGIN
left_to_read :=
bestseller -
required_reading;

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). And if you're using %ROWTYPE to declare the argument, the "shape" of the record (numbers and
types of fields) will adjust automatically with changes to the underlying cursor or table. Here's 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 ...

Prior to Oracle 9i Release 2, it was only possible to use a record in conjunction with a SQL statement in one
way: on the receiving end of a SELECT INTO or FETCH INTO statement. For example:

DECLARE
bestseller books%ROWTYPE;
BEGIN
SELECT *
INTO bestseller
FROM books
WHERE title =
'ORACLE PL/SQL PROGRAMMING';
END;

This is very convenient syntax, but it unfortunately just leaves us all hungry for the full range of record-
smart SQL, most importantly the ability to perform INSERT and UPDATE operations with a record (as
opposed to having to "break out" all the individual fields of that record). In summary, before Oracle 9i
Release 2, records offered significant advantages for developers, but also left us frustrated because of the
limitations on their usage. Oracle 9i Release 2 goes a long way in relieving (but not completely curing us of)
our frustrations.

Oracle 9i Release 2 Record Improvements

In response to developer requests, Oracle has now made it possible for us to do any of the following with
static SQL (such as, SQL statements that are fully specified at the time your code is compiled):

111
• Use collections of records as the target in a BULK COLLECT INTO statement. You no longer need to
fetch into a series of individual, scalar-type collections.
• Insert a row into a table using a record. You no longer need to list the individual fields in the record
separately, matching them up with the columns in the table.
• Update a row in a table using a record. You can now take advantage of the special SET ROW syntax to
update the entire row with the contents of a record with a minimum of typing.
• Use a record to retrieve information from the RETURNING clause of an UPDATE, DELETE, or INSERT.

Some restrictions do remain at Version 9.2.0 for records in SQL, including:

• You can't use the EXECUTE IMMEDIATE statement (Native Dynamic SQL) in connection with record-
based INSERT, UPDATE, or DELETE statements. (It's supported
for SELECT, as stated earlier.) Also In This Series
• With DELETE and UPDATE...RETURNING, the column-list
must be written explicitly in the SQL statement. Oracle 9i Release 2 Developments for
• In the bulk syntax case, you can't reference fields of the in- PL/SQL Collections
bind table of records elsewhere in the SQL statement
HTTP Communication from Within the
(especially in the where clause). Oracle Database

But why dwell on the negative? Let's explore this great new Multi-Level Collections in Oracle 9i
functionality with a series of examples, all of which will rely on the
employees table, defined in the hr schema that's installed in the Table Functions and Cursor
seed database. The script to create this schema is Expressions

demo/schema/human_resources/hr_cre.sql under the Oracle Home


Native Compilation, CASE, and
directory. Dynamic Bulk Binding

The samples also rely on common features such as an index-by-*_integer table, records of employees
%rowtype and a procedure to show the rows of such a table. These are implemented in the Emp_Utl
package.

SELECT with RECORD Bind

As we noted earlier, while it was possible before 9.2.0 to SELECT INTO a record, you couldn't BULK SELECT
INTO a collection of records. The resulting code was often very tedious to write and not as efficient as would
be desired. Suppose, for example, that we'd like to retrieve all employees hired before June 25, 1997, and
then give them all big, fat raises. A very straightforward way to write the logic for this is shown in Example
1.

Example 1. Give raises to employees using single row fetches.


DECLARE
v_emprec employees%ROWTYPE;
v_emprecs emp_util.emprec_tab_t;

CURSOR cur
IS
SELECT *
FROM employees
WHERE hire_date < TO_DATE(
'25-JUN-1997', 'DD-MON-YYYY');

i BINARY_INTEGER := 0;
BEGIN
OPEN cur;

112
LOOP
FETCH cur INTO v_emprec;
EXIT WHEN cur%NOTFOUND OR cur%ROWCOUNT > 10;
i := i + 1;
v_emprecs (i) := v_emprec;
END LOOP;

emp_util.give_raise (v_emprecs);
END;

There's no problem understanding this logic, but depending on the quantity of data involved, this could be a
very inefficient implementation. We'd really love to take advantage of the recent (Oracle 8i) addition of the
BULK COLLECT syntax (allowing us to fetch multiple rows with a single pass to the database); we might see
an order of magnitude improvement.

To use BULK COLLECT with records prior to Oracle 9i Release 2, however, we'd need to select each element
in the select list into its own collection; this technique is shown in Example 2. The complete code for this
block may be seen in bulkcollect8i.sql and is more than 80 lines long! It's approaching what is feasible to
maintain, and feels especially uncomfortable because of the artificial requirement to compromise the natural
modeling approach by slicing the desired table of records vertically into N tables of scalars.

Example 2. BULK COLLECT into separate collections.


DECLARE
TYPE employee_ids_t IS
TABLE OF employees.employee_id%TYPE
INDEX BY BINARY_INTEGER;
...
v_employee_ids employee_ids_t;
...
v_emprecs emp_util.emprec_tab_t;

CURSOR cur
IS
SELECT employee_id,
...
FROM employees
WHERE hire_date >= TO_DATE(
'25-JUN-1997', 'DD-MON-YYYY');

BEGIN
OPEN cur;
FETCH cur BULK COLLECT
INTO v_employee_ids,
...
LIMIT 10;
CLOSE cur;

FOR j IN 1 .. v_employee_ids.LAST
LOOP
v_emprecs (j).employee_id :=
v_employee_ids (j);
...
END LOOP;

emp_util.give_raise (v_emprecs);
END;

Note: The clause limit 10 is equivalent to where rownum <= 10.

113
With Oracle 9i Release 2, our program becomes much shorter, intuitive, and maintainable. What you see
here 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: Once again, 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's 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 that was added to the PL/SQL language in Oracle
9i Release 1.

INSERT with RECORD Bind

PL/SQL developers are demanding, no doubt about that. Even though Oracle can add all sorts of cool, new
functionality into PL/SQL, we'll still find something missing, something else we so dearly need. For years,
one of our favorite "wish-we-had's" was the ability to insert a row into a table using a record. Prior to Oracle
9i Release 2, if we had put our data into a record, it would then be necessary to "explode" the record into its
individual fields when performing the insert, as in:
114
DECLARE
v_emprec employees%ROWTYPE
:= emp_util.get_one_row;
BEGIN
INSERT INTO employees_retired (
employee_id,
last_name,
...)
VALUES (
v_emprec.employee_id,
v_emprec.last_name,
...);
END;

This is very cumbersome coding; it certainly is something we would have liked to avoid. In Oracle 9i
Release 2, we can now take advantage of simple, intuitive, and compact syntax to bind an entire record to a
row in an insert. This is shown here:

DECLARE
v_emprec employees%rowtype
:= Emp_Util.Get_One_Row;
BEGIN
INSERT INTO employees_retired
VALUES v_emprec;
END;

Notice that we don't 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're inserting a large number of rows.

Take a look at the example in Example 3. Table 1 explains the interesting parts of the retire_them_now
procedure (written and run at a low-tech company that never went public nor saw its value crash, enabling
them to now offer early, paid retirement to everyone over 40 years of age!).

Example 3. Bulk INSERTing with a record.


1 CREATE OR REPLACE PROCEDURE retire_them_now
2 IS
3 bulk_errors EXCEPTION;
4 PRAGMA EXCEPTION_INIT (bulk_errors, -24381);
5 TYPE employees_t IS TABLE OF employees%ROWTYPE
6 INDEX BY PLS_INTEGER;
7 retirees employees_t;
8 BEGIN
9 FOR rec IN (SELECT *
10 FROM employees
11 WHERE hire_date < ADD_MONTHS (SYSDATE, -1 * 18 * 40))
12 LOOP
13 retirees (SQL%ROWCOUNT) := rec;
14 END LOOP;
15 FORALL indx IN retirees.FIRST .. retirees.LAST
16 SAVE EXCEPTIONS
17 INSERT INTO employees
18 VALUES retirees (indx);
19 EXCEPTION
20 WHEN bulk_errors
21 THEN
22 FOR j IN 1 .. SQL%BULK_EXCEPTIONS.COUNT
23 LOOP
24 DBMS_OUTPUT.PUT_LINE ( 'Error from element #' ||

115
25 TO_CHAR(SQL%BULK_EXCEPTIONS(j).error_index) || ': ' ||
26 SQLERRM(SQL%BULK_EXCEPTIONS(j).error_code));
27 END LOOP;
28* END;

Table 1. Description of the retire_them_now procedure.

Line(s) Description
3-4 Declare an exception, enabling us to trap by name an error that occurs during the bulk insert.

5-7
Declare an associative array, each row of which contains a record having the same structure as the
employees table.
9-14 Load up the array with the information for all employees who are over 40 years of age.

15-18
The turbo-charged insert mechanism, FORALL, that includes a clause to allow FORALL to continue
past errors and references a record (the specified row in the array).

20-26
Typical code you'd write to trap any error that was raised during the bulk insert and display or deal
with each error individually.

Prior to Oracle 9i Release 2, you could use the FORALL syntax, but it would have been necessary to create
and populate a separate collection for each column, and then reference individual columns and collections in
the INSERT statement.

UPDATE SET ROW with RECORD Bind

Oracle 9i 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's 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's 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;

Prior to Oracle 9i Release 2, this same functionality would require listing the columns explicitly, as shown
in Example 4.

Example 4. Pre-Oracle9i Release 2 update of entire row.


DECLARE
v_emprec employees%ROWTYPE := emp_util.get_one_row;
116
BEGIN
v_emprec.salary := v_emprec.salary * 1.2;

UPDATE employees
SET first_name = v_emprec.first_name,
last_name = v_emprec.last_name,
email = v_emprec.email,
phone_number = v_emprec.phone_number,
hire_date = v_emprec.hire_date,
job_id = v_emprec.job_id,
salary = v_emprec.salary,
commission_pct = v_emprec.commission_pct,
manager_id = v_emprec.manager_id,
department_id = v_emprec.department_id
WHERE employee_id = v_emprec.employee_id;
END;

Now, it would certainly be nice to be able to use the SET ROW syntax in a FORALL statement, as follows:

DECLARE
v_emprecs emp_util.emprec_tab_t
:= emp_util.get_many_rows;
BEGIN
-- This will not work, due to:
-- PLS-00436:
-- implementation restriction:
-- cannot reference fields of
-- BULK In-BIND table of records.
FORALL j IN
v_emprecs.FIRST .. v_emprecs.LAST
UPDATE employees
SET ROW = v_emprecs (j)
WHERE employee_id =
v_emprecs (j).employee_id;
END;

Sadly, this code fails to compile with the error: "PLS-00436: implementation restriction: cannot reference
fields of BULK In-BIND table of records." Instead, we must write:

DECLARE
v_emprecs emp_util.emprec_tab_t
:= emp_util.get_many_rows;

TYPE employee_id_tab_t IS
TABLE OF employees.employee_id%TYPE
INDEX BY PLS_INTEGER;

v_employee_ids employee_id_tab_t;
BEGIN
-- Transfer just the IDs into their own
-- collection for use in the WHERE clause
-- of the UPDATE statement.
FOR j IN v_emprecs.FIRST .. v_emprecs.LAST
LOOP
v_employee_ids (j) :=
v_emprecs (j).employee_id;
END LOOP;

FORALL j IN
v_emprecs.FIRST .. v_emprecs.LAST
UPDATE employees
SET ROW = v_emprecs (j)
117
WHERE employee_id = v_employee_ids (j);
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's 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. An example of this
feature for DELETEs is shown in Example 5.

Example 5. RETURNING into a record from a DELETE statement.


DECLARE
v_emprec employees%ROWTYPE;
BEGIN
DELETE FROM employees
WHERE employee_id = 100
RETURNING employee_id, first_name, last_name, email, phone_number,
hire_date, job_id, salary, commission_pct, manager_id,
department_id
INTO v_emprec;

emp_util.show_one (v_emprec);
END;

You can also retrieve less than a full row of information by relying on programmer-defined record types, as
this next example shows:

DECLARE
TYPE key_info_rt IS RECORD (
id NUMBER,
nm VARCHAR2 (100)
);

v_emprec key_info_rt;
BEGIN
DELETE FROM employees
WHERE employee_id = 100
RETURNING employee_id, first_name
INTO v_emprec;
...
END;

You must still list the individual columns or derived values in the RETURNING clause, making the integration
a bit less than ideal (for example, Oracle could and perhaps will some day allow us to write RETURNING ROW
INTO v_emprec). Nevertheless, this is a significant improvement over Version 9.0.1, where a RECORD could
not be used as the target for INTO, requiring us to provide a long list of individual variables to hold the
values returned from the DML statement.

Next, suppose that we execute a DELETE or UPDATE that modifies more than one row. In this case, we can
use the RETURNING clause to obtain information from each of the individual rows modified by using BULK
COLLECT to populate a collection of records! This technique is shown in Example 6.

Example 6. RETURNING multiple rows of information from an UPDATE statement.


DECLARE
v_emprecs emp_util.emprec_tab_t;

118
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;

Again, this is a significant improvement over Version 9.0.1, in which you would have to declare a separate
collection for each value specified in the RETURNING clause, and then populate each separately. A fragment
of this approach is shown in Example 7:

Example 7. RETURNING multiple rows of data from an UPDATE statement in Version 9.0.1.
DECLARE
TYPE employee_ids_t IS TABLE OF employees.employee_id%TYPE
INDEX BY BINARY_INTEGER;
...
v_employee_ids employee_ids_t;
...
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_employee_ids, v_first_names, v_last_names, v_emails,
v_phone_numbers, v_hire_dates, v_job_ids, v_salarys,
v_commission_pcts, v_manager_ids, v_department_ids;

FOR j IN 1 .. v_employee_ids.LAST
LOOP
v_emprecs (j).employee_id := v_employee_ids (j);
...
END LOOP;

emp_util.show_all (v_emprecs);
END;

Performance Impact of Record Binding

There's no doubt that using records in DML statements results in greatly reduced code volume and therefore
increased productivity. Is there, however, a penalty to be paid in runtime execution of this leaner code? Our
tests (see Table 2) show for the most part that there's no measurable difference between field and record-
based operations.

Table 2. Scripts to examine performance impact of record binding.

Script name What is tested?


insrec1.tst Insert with record for table with sequence-generated primary key.
insrec2.tst Insert with record on table with non-sequence primary key.
119
insrec3.tst Insert with record on table with many columns with non-sequence primary key.
insrec4.tst Bulk insert with record on table with non-sequence primary key.

Depending on extenuating circumstances, however, you can see more of a differential.

For example, any one of the following situations could impact negatively on record-based DML processing
time:

• Use of a sequence to generate a primary key. You can't include <sequence>.NEXTVAL in your record
specification, so you must execute an "external" query (usually against the Oracle "dual" table) prior
to the INSERT itself, to obtain the primary key value and assign it to the appropriate field in the
record. See the insrec1.tst script for a demonstration of the impact of this step. One must conclude
that a record-based INSERT is simply not a good fit for this scenario.
• Update triggers on individual columns of the table. An update with a record updates all columns of
the table. To avoid this problem, make sure that you include a WHEN clause on your triggers to avoid
extraneous execution (when NEW and OLD values are the same). See the genwhen.sql script for a
utility that will generate the appropriate WHEN clause for each column of a table.
• If, on the other hand, you take advantage of Oracle 9i Release 2's ability to perform bulk collect
operations with records (see insrec4.tst), you'll find that record-based operations are consistently and
noticeably faster than those relying on individual fields (requiring a separate collection for each
field).

Record-based DML was added to the PL/SQL language primarily as a "usability" feature, rather than one
related to performance. Part of the challenge of integrating new features into your "box of tricks" is that you
need to know when not to use them. In general, if you're already working with and populating records
(particularly if you're transferring data from one table to another using records), you'll find this feature to be
a wonderful enhancement.

Records: The Way to Go

Records have always been a very powerful programming construct for PL/SQL developers. Use of records
reduces code volume and also increases the resiliency of one's code, since a record defined using %ROWTYPE
automatically (upon recompilation of the program) adapts to the current structure of the base cursor or table.

The inability to utilize records within SQL DML statements in a PL/SQL program has long been a
frustration to developers. With Oracle 9i Release2, another barrier between SQL and PL/SQL has been
removed, allowing for ever-smoother programming efforts, higher productivity, and more easily maintained
applications.

120

Vous aimerez peut-être aussi