You are on page 1of 16

Oracle

Solutions for High-End


Oracle® DBAs and Developers Professional

A Modified
Perspective on Error
Handling in PL/SQL
Steven Feuerstein
You can’t write a high-quality application without building a robust error handling
and reporting architecture. Steven Feuerstein has been thinking about this
challenge for years and has come to some surprising conclusions: Hard-coded
error names are good, and RAISE_APPLICATION_ERROR is a waste of time. In this
article, he explains his thinking and offers some new ideas for error handling in
PL/SQL applications.

September 2004

I
’VE long preached (and, yes, I can be preachy) about the importance of
Volume 11, Number 9
consistent error handling, based on a common infrastructure package. I
suggest a variety of best practices to go with this high-level suggestion,
1 A Modified Perspective on
including but not limited to: Error Handling in PL/SQL
• Don’t call RAISE_APPLICATION_ERROR directly. Steven Feuerstein
• Don’t hard-code error numbers, particularly those in the -20,NNN error
range, in your code. 8 Accessing java.util.zip from
PL/SQL: The GZIP Classes
• Provide simple ways for developers to handle errors that hide the Gary Menchen
implementation of your error log and enforce consistency.
12 Attribute Denormalization
Check out my Oracle PL/SQL Best Practices book (www.oreilly.com/ Peter Hamilton
catalog/orbestprac) for a more complete treatment of my exception handling 16 September 2004 Downloads
best practices.
I also implemented a package that satisfies these best practices in
PL/Vision, a library of reusable code now available as freeware from
Quest Software (http://quest-pipelines.com/pipelines/dba/PLVision/
plvision.htm). For example, suppose I had to write a trigger to ensure that Indicates accompanying files are available online at
www.pinnaclepublishing.com.
employees are “not too young.” This is, of course, an application-specific
error, so many of us are likely to write code that looks like think. They were right; both approaches have hard-
this in the trigger: coding. A change in the name (to make it more accurate,
to reflect changed meaning, and so on) would require a
IF :NEW.birthdate > ADD_MONTHS (SYSDATE, -1 * 18 * 12)
THEN
change in code with either approach.
RAISE_APPLICATION_ERROR After more discussion, we ended up agreeing on the
(-20070, 'Employee "' || :NEW.last_name
|| '" must be 18.'); following points regarding these two techniques for
END IF; avoiding the hard-coding of the error number itself:
• Use of a predefined, named constant has the
Following the PL/Vision approach, I would predefine advantage of offering compile-time verification of my
my error number and give it a name in an error numbers choice of error. In other words, if I typed “errnums_
package (preferably generated) and then call the pkg.en_emp_too_yng”, my code wouldn’t compile,
appropriate raise program (record the error, and then since that identifier isn’t defined in errnums_pkg. If,
stop)... and while I’m at it, I’ll also move that hard-coded on the other hand, I typed ‘EMPLOYEE-TOO-YNG’
business rule logic to a separate package: for my error name, the code would compile but I
wouldn’t identify the right error. Compile-time
IF employee_rules.emp_too_young (:NEW.birthdate)
THEN verification is always preferable over runtime errors.
PLVexc.recNstop (
errnums_pkg.en_emp_too_young,
• Predefined, named constants require more advanced
,'Employee "' || :NEW.last_name || '"'); planning or cause you to write code in a “stop and
END IF;
start” manner. It’s hard to know in advance of writing
one’s application the various types of errors that may
This approach is certainly an improvement over
be encountered or that you may want to raise and
calling RAISE_APPLICATION_ERROR directly. Over the
handle. If you rely on predefined constants for error
past year, however, I’ve changed my views about how
numbers, each time you encounter the need for a new
best to organize, raise, and handle errors. I’d like to share
error you’ll have to stop writing your program to
those new views with you in this article. First, I’ll explain
upgrade the error numbers package. You’ll then need
what brought me to rethink my recommendations on
to recompile that package—and all the programs now
this topic.
marked invalid by the change to that package. With
literals, I can use “top down design”: As I write the
Learning from my students
program, I specify new errors. My program will
In November 2003, Quest Software sponsored a series of
compile, since these are literals. I can maintain my
seminars by yours truly in Germany and Scandinavia.
focus on the program at hand. When done, I can
While in Munich, I presented the aforementioned
“batch define” all of my errors at once.
approach for error handling. Two students approached
me over a break with a concern about this line:
So, there are pluses and minuses for each approach,
PLVexc.recNstop ( as is pretty much always the case with any set of
errnums_pkg.en_emp_too_young, programming alternatives.
,'Employee "' || :NEW.last_name || '"');

They were trying to come up with a general error What Qnxo needs
handling architecture as well, but they had a problem In addition to learning from my students, I’ve reshaped
with my named exceptions. “Each time you define a my views on error handling through the real-world
new exception,” they reminded me, “the errnums_pkg experience of building Qnxo, the world’s first active
specification would be changed and recompiled. This mentoring software product (www.qnxo.com). Qnxo
would invalidate all of the programs that reference the (Delphi front end and Oracle-PL/SQL back end) needs a
package. In our system, that simply won’t do.” solid error handling infrastructure, in which back-end
These students suggested instead using literal strings errors are recorded and also communicated in a robust
to identify the error, as in: manner to its users. That’s nothing particularly unique.
But my goal with Qnxo is to help you build applications
PLVexc.recNstop ( the right way, faster than you can do them the “quick and
'EMPLOYEE-TOO-YOUNG',
,'Employee "' || :NEW.last_name || '"'); dirty” way. Since error handling is a key aspect of doing
things “the right way,” it’s my intention to expose the error
My first reaction was visceral: “You are putting hard- handling infrastructure I use in Qnxo, so that users of
coded literals in your programs. That’s bad.” They then Qnxo can apply it to their own applications. So I’d better
pointed out that my approach (errnums_pkg.en_emp_ get it right!
too_young) also “hard-coded” the name of the error. It As my error handling strategy for Qnxo evolved, I
simply wasn’t a literal. Now, that made me sit back and discovered, among other things, that I definitely preferred

2 Oracle Professional September 2004 www.pinnaclepublishing.com


the use of literal strings over named constants to identify
Listing 1. The application table.
errors. In the remainder of this article, I’ll show you my
overall architecture for error handling and reporting in CREATE TABLE SA_APPLICATION
Qnxo, along with the DDL and some of the code that (
ID INTEGER,
implements the architecture. NAME VARCHAR2(500),
Most, but not all, of my error handling requirements DESCRIPTION VARCHAR2(4000),
DEPLOY_DIR VARCHAR2(4000),
for Qnxo reflect common needs: USE_QDA VARCHAR2(1),
• When an error occurs, grab as much as information as RAE_ERROR_CODE INTEGER DEFAULT -20000,
);
possible about the error, for both the user and the
support personnel.
When you decide to build your application according
• Communicate the circumstances of the error to users
to the Qnxo architecture (using the encapsulation
in a way that they can understand, and give them the
packages), and generate components of your application
information they need to resolve their error (in the
from Qnxo, you’ll then need to take certain steps to
case of user error).
ensure that you properly deploy your application.
I decided to define errors for and within a given
There is, however, one fairly unique aspect to Qnxo
application, and furthermore to allow users to group
error handling: Qnxo is providing the error handling code
these errors into categories defined by a range of error
for others to use in their own applications. Thus, I need to
code values, much as Oracle does. Listing 2 shows the
carefully separate out the phase in which you define errors
tables that store this information. Thus, we have error
in your application from the code that will actually be
categories for a particular application, and errors for a
deployed with the application when it runs in production.
given error category.
You very likely don’t have this same issue, but I think that
Here’s a description of the most interesting (and
you might still find useful the idea of keeping these
perhaps not entirely self-explanatory) error columns:
elements distinct.
• help_text—Advice given to users to help them resolve
the problem. In many cases, the only possible help
Defining errors for an application
is to contact application support. But in other cases,
One of the problems with PL/SQL and, more broadly, the
it will be possible to give the user instructions on
Oracle data dictionary is that it contains virtually no way
how to better use the application to avoid this error
to define “metadata” about one’s application. We store
in the future.
information in the database about our programs, their
• substitute_string—Generic messages are of limited
parameters, dependencies, and so forth—all very low-
assistance. Errors don’t occur in a vacuum. Rather,
level stuff. As to how these elements combine into an
they’re in response to specific circumstances,
application, Oracle leaves us high and dry. For years,
usually either system problems or data problems.
CASE tools helped in this area, but most developers aren’t
It sure would be nice to be able to provide a non-
going to take the necessary up-front time to design with
generic message to users that reflects the specific
most CASE-style tools.
circumstances and makes it easier for them to resolve
I decided that with Qnxo, I would ask for the
the problem. This column allows the developer to
absolute minimum of metadata information about one’s
provide a string with placeholders that are replaced
application, so I have, for example, a table to keep track
at runtime with useful information.
of applications, as shown in Listing 1. Most columns are
self-explanatory. The deploy_dir column indicates the
directory in which the deployment code will be placed. Listing 2. Error definition tables.
The use_qda column indicates whether or not this
CREATE TABLE SA_ERROR_CATEGORY
application will be based on the Qnxo Development (
Architecture, or QDA. If the column value is “Y”, then ID INTEGER NOT NULL,
APPLICATION_ID INTEGER NOT NULL,
Qnxo will generate lots of the code needed to build the NAME VARCHAR2(500) NOT NULL,
application, and it will use the Qnxo error handling LOW_VALUE INTEGER NOT NULL,
HIGH_VALUE INTEGER NOT NULL,
infrastructure. The rae_error_code column contains the DESCRIPTION VARCHAR2(4000)
default system error code used to raise errors. );
(Note: You’ll notice in this article that several of the CREATE TABLE SA_ERROR
Qnxo tables start with the SA prefix, and others the QD (
ID INTEGER NOT NULL,
prefix. The SA prefix reflects the previous name of Qnxo, ERROR_CATEGORY_ID INTEGER NOT NULL,
namely Swyg. “QD” stands for Qnxo Deployment. I’ll CODE INTEGER NOT NULL,
NAME VARCHAR2(500) NOT NULL,
probably upgrade the S* names at some point, but I’m DESCRIPTION VARCHAR2(4000),
sure many of you know what a hassle that will be!) HELP_TEXT VARCHAR2(4000),

www.pinnaclepublishing.com Oracle Professional September 2004 3


DEFAULT_TEXT VARCHAR2(4000),
SEVERITY VARCHAR2(1),
SUBSTITUTE_STRING VARCHAR2(4000)
);

One example of an error defined in Qnxo is shown


in Figure 1. Notice the text in the Error Message field:

“We could not find any columns for the object named
$object owned by $schema. Perhaps this is not a table or
you do not have sufficient privileges to see this table.”

The $object and $schema placeholders are replaced


with the name of the object and its owner if the Qnxo
generation engine encounters this error. The user can
then easily see which object is causing the problem.
This very simple set of tables allows us to define
enough information about errors to provide excellent
feedback to users. Let’s see how I deploy this information Figure 1. An error definition inside Qnxo.
into a new set of tables. Then we’ll look at the code ID INTEGER,
you’d write in the Qnxo architecture to leverage this ERROR_ID INTEGER,
ERROR_STACK VARCHAR2(4000),
information. CALL_STACK VARCHAR2(4000),
MESSAGE VARCHAR2(4000),
SYSTEM_ERROR_CODE INTEGER,
Deploying error information SYSTEM_ERROR_MESSAGE VARCHAR2(4000),
ENVIRONMENT_INFO VARCHAR2(4000),
As mentioned earlier, I need to separate the definition of );
errors from the deployment to or use of those errors in the
production application code. My objective with Qnxo is QD_ERROR
that you can use it to help you, in many ways, construct QD_ERROR is the domain of all known errors you set up
your application, but when the time comes to run that prior to deployment. This is a copy and consolidation
application, you can’t be expected to drag along a whole (denormalization, really) of SA_APPLICATION,
lot of Qnxo packages and tables. Furthermore, you SA_ERROR_CATEGORY, and SA_ERROR. This table is
shouldn’t have to deal with lots of different packages in mostly “read-only” at runtime. Should an exception occur
order to use the error handling features. In other words, that wasn’t previously defined in the error table, however,
it should be straightforward and lean. a default application-level error entry is made in the table.
Let’s see what I’ve done; you can help me judge You can then analyze the table to determine whether there
whether I’ve met my objectives. The “QD” (Qnxo are other errors you need to define in SA_ERROR (and
Deployment) tables are described in the following then redeploy out to the application).
sections; Listing 3 shows the DDL for the tables.
QD_ERR_INSTANCE
This is a particular occurrence of a known error. For
Listing 3. The DDL for the Qnxo Deployment tables.
example, NO_DATA_FOUND is a predefined system
CREATE TABLE QD_CONTEXT exception in PL/SQL. An actual raising of that exception
( in my program, however, such as through a SELECT
ID INTEGER,
ERR_INSTANCE_ID INTEGER, INTO that returns no rows, is a particular instance of that
NAME VARCHAR2(500), exception. By using this error instance table, I can
VALUE VARCHAR2(4000),
); associate all sorts of runtime information about the
CREATE TABLE QD_ERROR exception with a handle, and then pass back that handle
( to the calling program. In this way, I can treat exceptions
ID INTEGER,
ERROR_CATEGORY_ID INTEGER, in PL/SQL as variable data structures, rather than as
ERROR_CATEGORY_NAME VARCHAR2(500), special case data structures with many limitations.
APPLICATION_ID INTEGER,
APPLICATION_NAME VARCHAR2(500),
CODE INTEGER , QD_CONTEXT
NAME VARCHAR2(500),
DESCRIPTION VARCHAR2(4000), QD_CONTEXT contains the additional data elements
DEFAULT_TEXT VARCHAR2(4000),
DEFAULT_ERROR_CODE INTEGER, (in the form of name-value pairs) associated with a
SUBSTITUTE_STRING VARCHAR2(4000), particular error instance. This table allows us to store
RECOMMENDATION VARCHAR2(4000)
); additional, error-specific data in a flexible and highly
CREATE TABLE QD_ERR_INSTANCE
structured format. We can then use this data in our
( substituted strings.

4 Oracle Professional September 2004 www.pinnaclepublishing.com


Raising errors with the Qnxo architecture ,value4_in
,name5_in
=>
=>
cl
'CONTEXT'
So my tables are packed full of useful information. How ,value5_in => context_in
,grab_settings_in => TRUE
do I leverage them in my code? First let’s look at raising );
errors, and then we’ll move on to handling errors. I offer a END assign_set_stmt;
single package named qd_runtime to handle all runtime
operations. It includes overloading of these programs: The names I give these contexts need to match up
• qd_runtime.raise_error—Raises the specified error with the substitution string in the SA_ERROR table for
by number or name. this error, which is:
• qd_runtime.register_error—Registers the fact that
an error has occurred, but doesn’t raise the error. “The expression $EXPRESSION could not be executed
Useful for non-critical errors, when you want to successfully. The original string was $FULL-LINE,
record it but also want to allow processing in the the open and close characters were $OPEN-TAG-
application to continue. CHARACTER and $CLOSE-TAG-CHARACTER,
• qd_runtime.add_context—Adds a single context and the optional context value was $CONTEXT.”
name-value pair to an error instance.
Thus, when the error is raised, users receive a detailed
I’ll focus my attention on qd_runtime.raise_error; it’s report that will hopefully provide lots of guidance and
the program I use most often and it actually incorporates help for them to resolve the problem quickly.
the other two. Consider the overloading of raise_error I don’t know about you, but I have mixed feelings
that accepts an error name, shown in Listing 4. about the header for raise_error. I really don’t like the way
those hard-coded five name-value pairs look. What if I
want to pass in six or eight values? In my initial design
Listing 4. Raising an error with the qd_runtime package. of this package, I didn’t offer the ability to add name-
value pairs in the call to raise_error. Instead, I’d first call
PROCEDURE qd_runtime.raise_error (
error_name_in IN qd_error_tp.name_t qd_runtime.register_error, and then call add_context for
,text_in IN qd_err_instance_tp.message_t := NULL
,name1_in IN VARCHAR2 DEFAULT NULL
each name-value pair, as in:
,value1_in IN VARCHAR2 DEFAULT NULL
,name2_in IN VARCHAR2 DEFAULT NULL EXCEPTION
,value2_in IN VARCHAR2 DEFAULT NULL WHEN OTHERS
,name3_in IN VARCHAR2 DEFAULT NULL THEN
,value3_in IN VARCHAR2 DEFAULT NULL DECLARE
,name4_in IN VARCHAR2 DEFAULT NULL l_err_instance_id PLS_INTEGER;
,value4_in IN VARCHAR2 DEFAULT NULL BEGIN
,name5_in IN VARCHAR2 DEFAULT NULL qd_runtime.register_error (
,value5_in IN VARCHAR2 DEFAULT NULL error_name_in
); => 'DYNAMIC-ASSIGNMENT-FAILURE'
,err_instance_id_out
=> l_err_instance_id
It allows me to specify the name of the error, an );
optional text message, and up to five name-value pairs qd_runtime.add_context (
err_instance_id_in => l_err_instance_id
for contexts. One example of a call to this program is ,NAME_IN => 'EXPRESSION'
shown in Listing 5. In this case, I specify the error with a ,value_in => expr
);
hard-coded literal string ‘DYNAMIC-ASSIGNMENT- ...
END;
FAILURE’. So even if I haven’t yet defined this error, I END;
can immediately compile and use this code. I then look
back through my code to see what variables are present I found this to be an awful lot of code to have to
that I’d want to record with the error. I pass these in as write, which means that I probably wouldn’t do it. And
name-value pairs. if I wouldn’t do it, then why would anyone else? So I
added the five name-value pairs to the parameter list of
Listing 5. Raising an error with raise_error. raise_error. Then I could hide all that complexity and
code behind the scenes and make it very easy for a
EXCEPTION programmer to use the error handling infrastructure.
WHEN OTHERS
THEN
Listing 6 shows the three steps taken by the
qd_runtime.raise_error raise_error program. First, I “grab” all of the current
(error_name_in => 'DYNAMIC-ASSIGNMENT-FAILURE'
,name1_in => 'EXPRESSION' system settings from Oracle; some of these values will
,value1_in => expr change depending on what I do in the rest of my error
,name2_in => 'FULL-LINE'
,value2_in => NAME_IN handling code, so I want to “freeze” them at the very start
,name3_in => 'OPEN-TAG-CHARACTER' of my error handling procedure. Next, I call register_error
,value3_in => op
,name4_in => 'CLOSE-TAG-CHARACTER' to save all the information to my various tables. Finally, I

www.pinnaclepublishing.com Oracle Professional September 2004 5


raise the error instance back to the calling program. THEN
RAISE_APPLICATION_ERROR
( l_error.code
, err_instance_in.MESSAGE);
Listing 6. The implementation of raise_error.
-- 2. Undefined error information.
ELSIF l_error.code = 1
PROCEDURE raise_error ( THEN
error_code_in IN qd_error_tp.code_t RAISE_APPLICATION_ERROR
,text_in IN qd_err_instance_tp.message_t (qd_error_xp.default_error_code,
:= NULL NVL (err_instance_in.MESSAGE
,name1_in IN VARCHAR2 DEFAULT NULL , 'User-defined error'));
,value1_in IN VARCHAR2 DEFAULT NULL
,name2_in IN VARCHAR2 DEFAULT NULL -- 3. Using positive error numbers or we have an
,value2_in IN VARCHAR2 DEFAULT NULL -- error instance with no error code
,name3_in IN VARCHAR2 DEFAULT NULL -- (runtime-defined error).
,value3_in IN VARCHAR2 DEFAULT NULL ELSIF l_error.code > 0
,name4_in IN VARCHAR2 DEFAULT NULL OR (l_error.code IS NULL AND
,value4_in IN VARCHAR2 DEFAULT NULL err_instance_in.ID IS NOT NULL)
THEN
,name5_in IN VARCHAR2 DEFAULT NULL
RAISE_APPLICATION_ERROR (
,value5_in IN VARCHAR2 DEFAULT NULL
-20000
,grab_settings_in IN BOOLEAN DEFAULT TRUE
,c_error_prefix
)
|| LPAD (err_instance_in.ID
IS ,c_error_code_len
l_err_instance_id qd_err_instance_tp.id_t; ,'0')
BEGIN );
IF grab_settings_in
THEN -- 4. We have a message to pass back, at least.
g_grabbed_settings.system_error_code := SQLCODE; ELSIF l_error.code IS NULL AND
g_grabbed_settings.system_error_message := err_instance_in.MESSAGE IS NOT NULL
DBMS_UTILITY.format_error_stack; THEN
g_grabbed_settings.error_stack := RAISE_APPLICATION_ERROR
DBMS_UTILITY.format_error_stack; (qd_error_xp.default_error_code
g_grabbed_settings.call_stack := ,err_instance_in.MESSAGE);
DBMS_UTILITY.format_call_stack;
END IF; END IF;
END issue_oracle_raise;
register_error
(error_code_in => error_code_in
,err_instance_id_out => l_err_instance_id The issue_oracle_raise program is called from
,text_in => text_in
,name1_in => name1_in qd_runtime.raise_error. It calls RAISE_APPLICATION_
,value1_in => value1_in ERROR to propagate the error specified by the error
,name2_in => name2_in
,value2_in => value2_in instance ID out to the host environment. The most
,name3_in => name3_in important section in the program for our purposes is
,value3_in => value3_in
,name4_in => name4_in the clause labeled “3”, which contains this call:
,value4_in => value4_in
,name5_in => name5_in
,value5_in => value5_in RAISE_APPLICATION_ERROR (
,grab_settings_in => FALSE -20000
); ,c_error_prefix
|| LPAD (err_instance_in.ID
raise_error_instance ,c_error_code_len
(err_instance_id_in => l_err_instance_id); ,'0')
END raise_error; );

The most interesting part of what Qnxo is doing What I’m doing here is bundling up the error
behind the scenes is creating an error instance row, instance handle and passing it back as the error message
associating lots of information with it, and then passing in this format:
back the primary key of that error instance row (the
ORA-20000: QNXO000000001345
handle) in the RAISE_APPLICATION_ERROR error
message. Let’s take a closer look at the heart of this logic, I use this format so that when the host environment
shown in Listing 7. calls qd_runtime.get_error_info (discussed in the next
section), I can be sure that the value 1345 is an error
Listing 7. Passing back the error instance handle. instance handle and not some random number string
that has nothing to do with Qnxo.
PROCEDURE issue_oracle_raise ( So I’ve come up with a way to pass a unique handle
err_instance_in IN
qd_err_instance_tp.qd_err_instance_rt to extensive error information back to the calling
) program. Now we need to see how I can get all of that
IS
l_error qd_error_tp.qd_error_rt; error information given the error message.
BEGIN
l_error := qd_error_qp.onerow
(id_in => err_instance_in.error_id); Handling errors with qd_runtime.get_error_info
-- 1. Ready to raise the application error.
Raising the exception is just the first step. Now we have to
IF l_error.code BETWEEN -20999 AND -20000 extract the information recorded with that error so it can

6 Oracle Professional September 2004 www.pinnaclepublishing.com


be displayed or otherwise processed by the application. former isn’t restricted to 255 characters) and then call the
The qd_runtime package offers the overloaded get_error_ parse_message program to extract from that string the
info procedure to make it very easy to do this. I’ll focus on error instance primary key. From there, I get the generic
the one we’re using in Qnxo, shown in Listing 8. error information and then return all required values,
from the error instance row, the error row, and the
grabbed settings.
Listing 8. The get_error_info procedure.
Listing 9 shows a PL/SQL-only script that includes
PROCEDURE qd_runtime.get_error_info ( the raise_error and get_error_info programs. Figure 2
code_out OUT qd_error_tp.code_t shows the interface we’ve built in Qnxo to display
,name_out OUT qd_error_tp.name_t
,text_out OUT qd_err_instance_tp.message_t the error information that’s returned by qd_runtime
,system_error_code_out .get_error_info.
OUT qd_err_instance_tp.system_error_code_t
,system_error_message_out
OUT qd_err_instance_tp.system_error_message_t
,recommendation_out Listing 9. A PL/SQL block demonstrating the raising and handling
OUT qd_error_tp.recommendation_t
,error_stack_out of an error.
OUT qd_err_instance_tp.error_stack_t
,call_stack_out BEGIN
OUT qd_err_instance_tp.call_stack_t qd_runtime.raise_error
) (error_name_in => 'NO-COLUMNS-FOUND'
IS ,name1_in => 'SCHEMA'
l_err_instance qd_err_instance_tp.qd_err_instance_rt; ,value1_in => USER
l_error qd_error_tp.qd_error_rt; ,name2_in => 'OBJECT'
BEGIN
,value2_in => l_object_name
parse_message (DBMS_UTILITY.format_error_stack
);
, l_err_instance.id);
EXCEPTION
l_err_instance := qd_err_instance_qp.onerow
WHEN OTHERS
(l_err_instance.id);
THEN
l_error := qd_error_qp.onerow
(l_err_instance.error_id); DECLARE
code_out := l_error.code; err_rec qd_runtime.error_info_rt;
NAME_out := l_error.NAME; BEGIN
text_out := substituted_string qd_runtime.get_error_info
(l_err_instance (code_out => err_rec.code
,l_error.substitute_string ,name_out => err_rec.NAME
); ,text_out => err_rec.text
recommendation_out := l_error.recommendation; ,system_error_code_out
system_error_code_out := => err_rec.system_error_code
g_grabbed_settings.system_error_code; ,system_error_message_out
system_error_message_out := => err_rec.system_error_message
g_grabbed_settings.system_error_message; ,recommendation_out
error_stack_out := g_grabbed_settings.error_stack; => err_rec.recommendation
call_stack_out := g_grabbed_settings.call_stack; ,error_stack_out
END get_error_info; => err_rec.error_stack
,call_stack_out => err_rec.call_stack
);
Recall that Qnxo is a Delphi application accessing END;
an Oracle database on the back end. Since I’m working END;
/
with a non-PL/SQL host environment (as opposed to
Continues on page 15
100 percent back-end stored code), I need to be careful
about the datatypes I use. For example, object and
record types in Oracle aren’t universally recognized by
non-PL/SQL front ends. As a result, my qd_runtime
.get_error_info program returns a series of scalar
values, with which just about any front end can work
(other overloadings return records and even collections
of values).
Notice also that qd_runtime.get_error_info doesn’t
have any IN parameters. This program is designed to be
called in Qnxo (Delphi) whenever the back end passes
back an exception. In this situation, the Oracle error
message is currently set to whatever was passed back by
a RAISE or RAISE_APPLICATION_ERROR.
Now, let’s look at the implementation of the
get_error_info procedure. I grab the error message from
DBMS_UTILITY.FORMAT_ERROR_STACK (this is
generally recommended over SQLERRM, since the Figure 2. The Qnxo form to display errors.

www.pinnaclepublishing.com Oracle Professional September 2004 7


Oracle
Professional

Accessing java.util.zip from


PL/SQL: The GZIP Classes
Gary Menchen
The java.util.zip classes can be used to compress LOBs in getBinaryOutputStream(), which return binary streams
much the same way that desktop utilities such as WinZip that are used to read from and write to a BLOB. For
compress files. Oracle provides a data compression package— CLOBs, I use the oracle.sql.CLOB getAsciiStream() and
UTL_COMPRESS—in 10g, but those of us using earlier versions getAsciiOutputStream() methods—these return single
of the database can provide similar functionality using Java bytes and thus are functionally like the binary stream
Stored Procedures. In this article, Gary Menchen discusses classes used with BLOBs.
how to use the GZIP classes. You’ll learn a new approach to Oracle also provides getCharacterStream() and
compression that’s different from what Oracle is providing, getCharacterOutputStream() methods that return
and see an illustration of writing routines to process LOBs descendents of Java Reader and Writer classes.
using Java’s input and output stream classes. Gary also Characters, in Java, are stored in two-byte Unicode
reviews how to avoid a potentially serious bug when using character sets—I elaborate on those later in the article.
this compression technique.
GZIP compression

A
few years ago, I needed a base64 encoding There are two flavors of compression in the java.util.zip
function to allow sending binary e-mail classes: ZIP, which compresses a stream of bytes and
attachments within PL/SQL. The first version I stores it in an archive wrapped with information such as
wrote was in PL/SQL; performance wasn’t particularly file name, date, size, and comment; and GZIP, which
good, so I tried doing the same thing in Java. I began compresses a single stream, but doesn’t wrap the results
this more as a learning exercise than anything else, since in any metadata. GZIP is an appropriate choice when you
my Java skills are quite rudimentary, but I found the want to compress a single “thing” and you don’t need to
performance was dramatically better, and the Java store the identification of what you’re compressing in the
InputStream and OutputStream classes were easy to compressed data. Because the only operations available
work with. in GZIP are compression and decompression, the user
A good rule for programming is to use the simplest interface to access the classes from PL/SQL is very
tool available for any task, and while Oracle uses Java simple. These consist of the following:
extensively for its own purposes, as a developer I’ve had • Java procedures that compress CLOBs and
little reason to—why use Java when I can use PL/SQL just BLOBs, storing the result in a BLOB passed as an
as well? Some things, however, aren’t readily available in OUT parameter.
the version of Oracle (9.2) that I, like many others, have • Java procedures that decompress a BLOB containing
access to, and data compression is one of them. data compressed with GZIP, returning the
uncompressed data as either a CLOB or a BLOB.
Accessing LOBs from Java • PL/SQL procedures that publish the Java procedures.
The InputStream and OutputStream classes in Java • PL/SQL procedures that provide the direct API
provide simple and efficient access to input and for developers. These aren’t required to make use
output. Oracle provides the class oracle.sql.CLOB for of the Java routines, but they provide a more
processing CLOBs and oracle.sql.BLOB for processing convenient syntax.
BLOBs. These map to Oracle’s CLOB and BLOB
datatypes, and provide methods that return InputStream Compressing a BLOB using GZIPOutputStream
and OutputStream descendents for reading from and I’ve placed all of the Java code for the GZIP procedures
writing to their corresponding datatypes. These classes in a single class that I’ve named GZIPFORPLSQL. The
are documented in the Oracle manual Application complete source code is available in the Download file.
Developer’s Guide—Large Objects (LOBs). For BLOBs, I use The Java method for compressing a BLOB is displayed in
the oracle.sql.BLOB methods getBinaryStream() and Listing 1, followed by a description.

8 Oracle Professional September 2004 www.pinnaclepublishing.com


Uncompressing a GZIPed BLOB with
Listing 1. The gZipBlob function.
GZIPInputStream
static public void gZipBlob( oracle.sql.BLOB inblob, The code to uncompress the BLOB works in the same
oracle.sql.BLOB[] outblob ) basic manner and is displayed in Listing 3. inBlob is a
throws java.io.IOException,
java.util.zip.ZipException, BLOB containing (presumably) data compressed with
java.sql.SQLException GZIP. I use the method getBinaryStream() again to
{
try get an InputStream and attach that to a GZIPInputStream,
{
byte [] byte_array = new byte[BUFFER]; which will handle the work of decompression. The
int bytes_read; BufferedInputStream just inserts buffering between
GZIPOutputStream gos = new
GZIPOutputStream( the original InputStream and the GZIPInputStream.
outblob[0].getBinaryOutputStream()); The OutputStream returned by oracle.sql.BLOB
InputStream is = inblob.getBinaryStream();
bytes_read = is.read(byte_array); .getBinaryOutputStream (see the details in Listing 1) is
while (bytes_read > 0) {
gos.write(byte_array,0,bytes_read);
attached to another buffered stream.
bytes_read = is.read(byte_array);
}
gos.flush(); Listing 3. Uncompressing a BLOB using GZIPInputStream.
gos.close();
is.close();
} // try static public void UnGZipBlob(oracle.sql.BLOB inblob,
catch (Exception e) { oracle.sql.BLOB[] outblob )
e.printStackTrace(); throws java.io.IOException,
} // catch java.util.zip.ZipException,
} // gZipBlob
java.sql.SQLException
{
The parameters are inblob, the BLOB to be try
{
compressed, and outblob, the OUT parameter, passed as int count;
an array. getBinaryStream() and getBinaryOutputStream() InputStream is = inblob.getBinaryStream() ;
GZIPInputStream gis =
are methods of the oracle.sql.BLOB class, and inherit from new GZIPInputStream(
the Java InputStream and OutputStream classes, so that new BufferedInputStream(is));
byte [] byte_array = new byte[BUFFER];
they can be used anywhere in Java that an InputStream or int bytes_read;
OutputStream may be used. BufferedOutputStream bos = new
BufferedOutputStream(
I declare a buffer, byte_array, of size BUFFER outblob[0].getBinaryOutputStream(), BUFFER );
(a static int declared at the beginning of the class while ((count = gis.read(byte_array,
0, BUFFER)) != -1) {
declaration), attach the OUT BLOB’s binary stream to bos.write(byte_array, 0, count);
a GZIPOutputStream variable, and set up a simple }
bos.flush();
loop, reading from the input stream and passing the bos.close();
results to the OutputStream until there’s nothing left to gis.close();
} // try
read. The OutputStream is flushed, and both streams catch (Exception e)
are then closed. {
e.printStackTrace();
The function is published to PL/SQL by: } // catch
} // UnGZipBlob
PROCEDURE gZipBlobInterface (blobin in blob,
blobout in out blob)
as language JAVA Compressing a CLOB with GZIPOutputStream
name 'GZIPFORPLSQL.gZipBlob(oracle.sql.BLOB, CLOBs are processed in a similar manner to BLOBs,
oracle.sql.BLOB[])';
except that there’s no getBinaryStream method—instead,
Oracle provides two alternate types of streams.
I believe a function provides a more natural interface
getCharacterStream and getCharacterOutputStream
here, so I also wrote a small PL/SQL function, shown in
are used to read and write the contents of a CLOB
Listing 2, that creates the temporary BLOB to hold the
as characters, preserving the two-byte Unicode
compressed data and passes it along to gZipBlobInterface.
character set of the CLOB. Java treats characters as
two-byte values.
Listing 2. Creating a PL/SQL function to compress a BLOB. getAsciiStream and getAsciiOutputStream read and
write the contents of a CLOB as single-byte characters,
function gZipBlob(pBlobIn in blob) return blob
is which is fine as long as you’re working with one-byte
vBlob blob; character sets. This can be used in the place of the
begin
dbms_lob.createTemporary(vBlob,true); oracle.sql.BLOB.getBinaryStream method I used in
gZipBlobInterface(pBlobIn, vBlob); Listing 3, and is the method I use in Listing 4 to read
return vBlob;
end gZipBlob; from the CLOB.

www.pinnaclepublishing.com Oracle Professional September 2004 9


vGZip := GZIP.gZipRaw(vRaw);
Listing 4. Compressing a CLOB with GZIPOutputStream and vUnGzip := GZIP.unGZipRaw(vGZip);
if utl_raw.cast_to_Varchar2(vUnGZip) <>
getAsciiStream. rec.object_name then
raise_application_error(-20001,
static public void gZipClob( oracle.sql.CLOB inclob, 'GZIP mismatch with ' || rec.object_name);
oracle.sql.BLOB[] outblob ) end if;
throws java.io.IOException, end loop;
java.util.zip.ZipException, end;
java.sql.SQLException
{
try I discovered that as I increased the number of rows
{ processed (rownum < 2000, 3000, and so on), performance
byte [] byte_array = new byte[BUFFER];
int bytes_read; declined dramatically. This happened both on my single-
GZIPOutputStream gos = new user database installed on my laptop and in a test
GZIPOutputStream(
outblob[0].getBinaryOutputStream()); database running on a Sun box under Unix. On the
InputStream is = inclob.getAsciiStream(); laptop, running Windows, I could see from the Task
bytes_read = is.read(byte_array);
while (bytes_read >= 0) Manager that Page File usage went up to 2.2GB, and
{
gos.write(byte_array,0,bytes_read);
finally, when trying to process 4000 rows, an ORA-04030
bytes_read = is.read(byte_array); out of process memory exception was raised.
}
gos.flush(); I created a view to collect session statistics, and a
gos.close(); table to hold the results, as shown here:
is.close();
} // try
catch (Exception e) { CREATE OR REPLACE VIEW MYSTATISTICS
e.printStackTrace(); AS
} // catch select m.sid, s.statistic#,s.value,n.name,n.class
} // gZipClob from (select sid from v$mystat where rownum = 1) m
join v$sesstat s
on m.sid = s.sid
In both cases, the GZIPed output is stored in a BLOB, join v$statname n
on s.statistic# = n.statistic#
so the output is handled in exactly the same way when /
compressing a CLOB. The getAsciiStream method returns CREATE TABLE STATS
(WHEN DATE,
a descendent of OutputStream, so I don’t need any extra WHAT VARCHAR2(20),
steps before passing it along to GZIPOutputStream. SID NUMBER,
STATISTIC# NUMBER,
If I needed to retain a two-byte character set, I VALUE NUMBER,
NAME VARCHAR2(64),
would read from the CLOB using oracle.sql.CLOB CLASS NUMBER)
.getCharacterStream, and interpose another class in front /
of GZIPOutputStream to convert the characters to bytes
before GZIPOutputStream compresses them. The I seeded the table with:
OutputStreamWriter class is designed for that purpose.
insert into stats
There are examples of these in the accompanying select sysdate,'initial,m.* from mystatistics m
Download file, in the gZipClob2 and gUnGZipClob2
methods—but since I work only with single-byte I then ran the Listing 5 script multiple times with
character sets, I haven’t been able to test them properly. 1000 rows, then 2000 rows, and so on until the ORA-04030
was at 4000 rows. Statistics 20 and 21, Session PGA
A warning about Java and PGA memory Memory and Session PGA Memory Max, illustrate the
After finishing the methods to compress and decompress situation quite clearly.
BLOBs and CLOBs, I added methods for compressing
RAW and decompressing RAW values: gZipRaw and initial 20 session pga memory 556260
1000-1 20 session pga memory 621796
unGZipRaw. I then set up a little script, as shown in 1000-2 20 session pga memory 621796
Listing 5, to test this by compressing and then 1000-2 20 session pga memory 425188
1000-2 20 session pga memory 425188
decompressing the object_name column from the ALL_ 1000-5 20 session pga memory 621796
2000-1 20 session pga memory 687332
OBJECTS view. 2000-2 20 session pga memory 687332
2000-3 20 session pga memory 687332
2000-4 20 session pga memory 687332
Listing 5. Testing GZIP compression/decompression with RAWs. 3000-1 20 session pga memory 621796
4000-1 20 session pga memory 621796

declare
vRaw raw(2000); As you can see, very little appears to happen to
vGZip raw (2000); Session PGA Memory. This is of interest because of what
vUnGZip raw(2000);
begin happens to Session PGA Memory Max—remember that
for rec in (select object_name from all_objects the statistics are collected at the point that the mystatistics
where rownum < 1000) loop
vRaw := utl_Raw.cast_to_Raw(rec.object_name); view is queried.

10 Oracle Professional September 2004 www.pinnaclepublishing.com


initial
1000-1
21
21
session
session
pga
pga
memory
memory
max
max
818404
531725540
An example of breaking a job into small,
1000-2 21 session pga memory max 531725540 chained sections
1000-2 21 session pga memory max 531725540
1000-2 21 session pga memory max 531725540
The procedure in Listing 6 will “process” (compress,
1000-5 21 session pga memory max 531725540 decompress, and compare the results) the object_name
2000-1 21 session pga memory max 1054964964
2000-2 21 session pga memory max 1054964964 column of the ALL_OBJECTS view. It processes 500 rows
2000-3 21 session pga memory max 1054964964 beginning with <pInitial>, and uses rownum to control
2000-4 21 session pga memory max 1054964964
3000-1 21 session pga memory max 1579187428 the selection of rows.
4000-1 21 session pga memory max 1855093988

I believe that these two tables combined show pretty Listing 6. Breaking up a job into sections.
clearly that:
procedure compressJob( pInitial in number)
1. Session PGA Memory grows linearly in relation to is
the number of calls to the Java procedure in the vRaw raw(2000);
vGZip raw (2000);
processing loop. vUnGZip raw(2000);
2. The memory is released on the next process—Session vCtr number := 1;
vJob number;
PGA Memory was always “normal,” because the cursor testCursor(cpFrom number) is
query that retrieved the statistic called the PGA to select object_name from
(select rownum prownum, object_name
be released. from all_objects
order by owner, object_Name) where prownum
>= cpFrom and prownum < cpFrom + 500;
This looked to me like a problem with the JVM. I tried begin
various permutations in the Java code, including writing for rec in testCursor( pInitial) loop
vCtr := vCtr + 1;
them both as procedures and as functions, setting the vRaw := utl_Raw.cast_to_Raw(rec.object_name);
instance variables to null, calling System.gc() to try to vGZip := GZIP.gZipRaw(vRaw);
vUnGzip := GZIP.unGZipRaw(vGZip);
force garbage collection, and so on—none of that made if utl_raw.cast_to_Varchar2(vUnGZip) <>
any difference. rec.object_name then
raise_application_error(-20001,
I found two different bugs on Metalink that should be 'GZIP mismatch with ' || rec.object_name);
studied by anyone interested in using the code described Continues on page 16
in this article:
• Bug 2791857 (“ORA-4030 OUT OF PROCESS
MEMORY RUNNING JAVA STORED PROCEDURE”)
describes exactly the problem I encountered in this
article, apparently even with the same Java classes.
This is described as a bug in the Deflator classes
(from which GZIP inherits) causing a memory leak.
However, I see no evidence that the memory is
leaking in my example; it’s just not being released
until the next process is encountered after the loop.
• Bug 2875058 (“UNLIMITED PGA MEMORY
D:
GROWTH WHEN REPEATEDLY CALLING JSP”)
seems closer to the mark. In this bug, the problem GE
A
isn’t described as a memory leak, and it’s not specific PA
to the Deflator classes. In the Metalink document, the
RTER OFT
QU REALS
memory is reported as reused when the loop is next
run—still not exactly what I’m seeing here, since the
A
tables show that the Session PGA Memory returns to
normal as soon as the mystatistics view is queried.

Both bugs are recorded as being fixed in version 10g.


Until I move to 10g, I’ll be careful about how I use this
Java code—I have no hesitations about using it to
compress and decompress individual LOBs, which will be
my primary use. If I had to process many thousands of
items, I’d break the job up into smaller sessions and
submit them to the job queue, arranging for each job to
call the next when completed.

www.pinnaclepublishing.com Oracle Professional September 2004 11


Oracle
Professional

Attribute Denormalization
Peter Hamilton
In this article, Peter Hamilton examines alternative methods the same place? I’m well on the way to deciding to
of merging logical entities representing few attributes into denormalize the attributes of the event queue.
a single physical database table, a frequent decision in the
database design process. Traditional approach
Traditionally, attribute denormalization is achieved by

T
HE process of physical database design invariably determining an upper threshold for the number of
requires us to make decisions about the adoption parameters that the event queue can have. Once decided,
of denormalized data structures. There can be the parameters are listed param_1, param_2, and so on.
many instances where controlled denormalization is So, for an event queue with 10 possible parameters, we’d
deemed appropriate. have this:
A typical example is an address, which, in its most
normalized state, yields an address entity and an address SQL> desc eq_denorm
Name
line entity. If the normalized structure were implemented, ------------------
ID
the result would be two physical tables (address and EVENT_TYPE
address line) that contain a single attribute, the address EQ_NAME
PARAM_VAL_1
line data. There’s clearly a case for denormalizing this PARAM_VAL_2
structure into a single physical table (address). This article ..
PARAM_VAL_10
focuses on this type of design approach, which I’ve
termed “attribute denormalization,” and examines some
I encounter numerous instances of this type of design.
methods by which it can be achieved.
It has the advantage that the event queue and associated
parameters are held together in a single record, benefiting
Problem domain the performance of both selection and manipulation. It
Figure 1 illustrates a normalized event queue, which
has several disadvantages, however:
I’ve used throughout this article. The idea is that
• The designer must enforce an arbitrary upper limit
events are stored in the queue for later processing by
on the number of parameters that an event queue
an event actioning process. Each event can have a
can have.
number of parameters.
• The data can be quite sparse if a typical event queue
As I look at the entity structure shown in Figure 1,
has a few parameters.
I get the feeling that the parameter table adds little
• Any SQL that manipulates the table must make the
value to the design. It merely stores the parameter
mapping between the list of parameters and the
values, which are always associated with a single event
actual data items.
queue element. As the parameters are always retrieved
along with the event queue item, why not store them in
The consequence of the last point is that the chosen
limit of the number of parameters per queue, in our
example, often manifests itself within the application
source code. For example, on selection of an event
queue record in a PL/SQL program, each parameter
may be received into an element of a collection. The
program needs to know how many elements of the
collection to fetch the data into, as illustrated in the
following sample code. In this way, the arbitrary
maximum limit of attributes becomes ingrained within
the whole application.

DECLARE
TYPE P_LIST IS TABLE OF
eq_denorm.param_val_1%TYPE;
Figure 1. Normalized event queue. p_id NUMBER := 1001;

12 Oracle Professional September 2004 www.pinnaclepublishing.com


CURSOR C_SEL( p_id IN NUMBER ) IS readings, for example, where readings are obtained from
SELECT *
FROM eq_denorm thousands of meters, each recording 48 half hourly
WHERE id = p_id;
periods per day, this saving can be significant.
l_rec C_SEL%rowtype; The disadvantages are:
l_list P_LIST := P_LIST();
BEGIN • The designer still needs to make a choice on the
OPEN C_SEL( p_id ); maximum number of parameters that the event
FETCH C_SEL INTO l_rec;
IF C_SEL % FOUND queue can have. This is used to create the cluster.
THEN • There’s an overhead on insert/update performance
l_list.EXTEND;
l_list( 1 ) := l_rec.param_val_1; of the data due to the performance overhead involved
l_list.EXTEND; in maintaining the internal storage structure. Such
l_list( 2 ) := l_rec.param_val_2;
.. -- and so on until the number of overheads are due to the fact that clustered values
-- parameters is reached
l_list.EXTEND;
for a single cluster key must be stored together, and
l_list( 10 ) := l_rec.param_val_10; hence the insertion of a new value may result in
END IF;
CLOSE C_SEL; Oracle having to move or reorganize the data.
END; • Client applications need to join the two tables to
obtain the event queue and its parameters, rather
Clustering than being able to fetch them from a single location.
Another approach to the problem is to allow the database Compared to the first approach, this is an extra table
to physically store the event queue with its parameters join, resulting in more complex SQL statements and
by using Oracle Index clustering, a facility that’s been coding efforts.
around since Oracle 7. Index clustering allows the two
normalized tables for the event queue and event queue Nested tables
parameters defined previously to be stored together The nested table was introduced as part of the Oracle 8
physically, within a cluster. In effect, the database stores release, and it would seem to be the ideal structure to
the tables in a similar manner to the traditional attribute support attribute denormalization. The feature allows a
sequence given earlier, with the event queue record table of objects to be nested within a relational table,
followed by a set number of parameters. hence allowing an unlimited list of parameters to be
The difference is that the internal storage is invisible stored with the event queue. The entire list of parameters
to the consumers of the data, and any SQL that accesses can be read directly into PL/SQL collections, and can be
it sees the event queue and event queue parameters as written back similarly. The DDL for creating the structure
normalized tables. The DDL for creating the cluster is is shown here:
shown in the following code. Note that the size of the
cluster is set to 10, the maximum number of parameters CREATE OR REPLACE TYPE PARAM_REC AS
OBJECT ( param_id VARCHAR2( 30 ),
per queue. param_val VARCHAR2( 200 ) );
/
CREATE CLUSTER eq_cluster CREATE OR REPLACE TYPE PARAM_TAB AS TABLE
( id NUMBER ) OF PARAM_REC;
SIZE 10; /
CREATE TABLE eq_nt
CREATE TABLE eq_clust ( id NUMBER NOT NULL,
( id NUMBER NOT NULL, event_type VARCHAR2( 30 ) NOT NULL,
event_type VARCHAR2( 30 ) NOT NULL, eq_name VARCHAR2( 30 ) NOT NULL,
eq_name VARCHAR2( 30 ) NOT NULL, param_vals PARAM_TAB,
PRIMARY KEY ( id ) ) PRIMARY KEY ( id ) )
CLUSTER eq_cluster( id ); NESTED TABLE param_vals STORE AS eq_nt_st(
( PRIMARY KEY ( NESTED_TABLE_ID, param_id ) )
CREATE TABLE eq_clust_param ORGANIZATION INDEX COMPRESS
( param_id VARCHAR2( 30 ) NOT NULL, PCTFREE 50
eq_id NUMBER NOT NULL, );
param_val VARCHAR2( 200 ),
FOREIGN KEY ( eq_id ) REFERENCES eq_clust( id ),
PRIMARY KEY ( param_id ) ) As ever, in order to facilitate any kind of acceptable
CLUSTER eq_cluster( eq_id ); performance, it became necessary to gain an
CREATE INDEX eq_idx ON CLUSTER eq_cluster; understanding of the way that Oracle represents nested
tables. With my simple test, which didn’t involve
The advantage to this approach is that the consumers extensive database-level tuning, an initial selection of
of the data need not be aware of any denormalization, 1,000 records in EQ_NT via the primary key was taking
since this is managed internally. There can also be great 20 seconds!
space savings—the foreign key references to the event So, returning to the preceding DDL, the table EQ_NT
queue aren’t stored against each parameter (eq_id in my is created with a nested table, param_vals, which adopts
example), as the cluster maintains the link between the an object structure. The nested table clause causes the
event queue and its parameters. In the case of meter nested table to be stored in a separate storage table,

www.pinnaclepublishing.com Oracle Professional September 2004 13


EQ_NT_ST. The nested table is automatically assigned a The analysis was performed on a Sun Fire 880/
column, NESTED_TABLE_ID, which holds a reference to Solaris 8, 8-CPU machine running Oracle 9.2.0.4.0. Each
the row in the parent table to which it relates. investigation considered 10,000 rows of data.
Oracle recommends that this storage table be defined The results are shown in Table 1.
as an Index Organized Table (IOT), which has been used
here. An IOT combines a B-tree index with the data for the
Table 1. Results of the analysis.
table. So, in this structure, there’s no need to access data
by first scanning the index and then looking up the data. Insert event queue Insert parameter Select
The data is stored on the leaf nodes of the index, and so Normalized 29.23s 0.29s 0.19s
only the index need be scanned. Denormalized 3.86s 0.18s 0.18s
Cluster 26.32s 1.31s 0.18s
Figure 2 describes the storage of the EQ_NT table. Nested table 25.19s 2.99s 0.36s
Note that the option to compress the IOT index results
in the nested table ID being stored only once for each The actual values aren’t to be taken definitively. They
parent row. could well be improved by DBA analysis and by using
If an IOT structure isn’t deemed appropriate for the bulk updates/selects for the non-nested table structures.
nested table, then an index must be defined separately Bulk selects and updates were used for the nested table
for the NESTED_TABLE_ID (param_id columns in this scenario and demonstrate that nested table data can be
case); otherwise, the nested table will be fully scanned fetched into a multi-dimensional nested table.
for each access.
I soon became aware that the DBA parameters DECLARE
PCTFREE and PCTUSED are particularly important for -- Multi dimensional array
IOTs. Performance is significantly degraded on update/ TYPE PARAM_TAB_LIST IS TABLE OF PARAM_TAB;
insert if the index needs to reorganize itself. Thus, an -- NUM_LIST is defined as a database type
assessment of the percentage block size that needs to be -- TABLE OF NUMBER
held back for such eventualities is required. Such analysis l_list NUM_LIST := NUM_LIST();
would most probably involve an assessment of the data l_res_list NUM_LIST := NUM_LIST();

size of each nested table row combined with the average l_i NUMBER := 0;
number of rows per parent row. As a result, the best guess l_j NUMBER := 0;
l_p_list PARAM_TAB_LIST := PARAM_TAB_LIST();
maximum parameters per event queue hasn’t vanished
BEGIN
completely in this solution. FOR l_i IN 1..1000
LOOP
l_list.EXTEND;
Benchmarking l_list( l_i ) := ( l_i * 10 );
Before drawing any conclusions about the advantages/ END LOOP;

disadvantages of each approach, I thought it advisable to -- Bulk select the parameters into a multi
consider performance issues. My analysis considered -- dimensional array

three basic scenarios: SELECT Z.id, Z.param_vals


BULK COLLECT INTO l_res_list, l_p_list
• Inserting event queues FROM eq_nt Z,
• Inserting parameters TABLE( CAST( l_list AS NUM_LIST ) ) L
WHERE Z.id = L.column_value;
• Selecting event queues END;

The lower time for the insertion of 10,000


denormalized items most probably results from the
30,000 fewer inserts and associated context PL/SQL
switches. There’s overhead in fetching from and
maintaining the IOT index within the nested table
structure. Approximately 90 percent of the manipulation
time was spent on the bulk update statement.

Conclusion
I’ve considered a variety of methods for implementing a
denormalized attribute structure and, as is often the case
with design, have found no single “best case” solution.
The denormalized enumeration of attribute_1..
attribute_n may be appropriate when the maximum
number of lines is known, and when insert/update
Figure 2. Nested table storage. performance is important. The client developer will have
14 Oracle Professional September 2004 www.pinnaclepublishing.com
to take the pain of coding around this. reap the rewards of a heterogeneous PL/SQL interface. ▲
Clustering may be appropriate when a set number
of values for a key is known (for instance, 48 half hourly 409HAMILTON.ZIP at www.pinnaclepublishing.com
meter readings per day), and when storage space is at
a premium. Peter Hamilton is an independent consultant specializing in Oracle
The nested table structure should be considered application development. He has more than 15 years of IT development
when insert/update performance of the nested table experience and has worked with Oracle products for the past 10 years.
elements isn’t critical, and when the number of nested He’s currently helping IBM develop the central fund clearing system for
table elements is uncertain. The client developer may then the UK banking industry. PeterHamilton@bacs.co.uk.

Error Handling in PL/SQL... one or more tables at the time the exception is raised, and
then pass the handle to the instance in my call to RAISE_
Continued from page 7 APPLICATION_ERROR. The host environment can then
The error instance handle is the key decide what it wants to do, how much information to
We offer more and better information to Qnxo users than display, and so on. ▲
I’ve previously been able to do with my error handling
Steven Feuerstein is considered one of the world’s leading experts
packages, such as PLVexc of PL/Vision—and I owe it all
on the Oracle PL/SQL language, having written nine books on PL/SQL,
to the idea of those error instances. If I rely on the error
including Oracle PL/SQL Programming and Oracle PL/SQL Best Practices
code and message passed by RAISE_APPLICATION_
(all from O’Reilly Media). Steven has been developing software since
ERROR, there’s only so much information I can pass—or I 1980 and serves as a Senior Technology Advisor to Quest Software.
pass it in a single string, packed with data, separated by His current projects include Qnxo (www.qnxo.com), a new active
various delimiters, and very hard to manage, particularly mentoring software product, and the Refuser Solidarity Network
by the front end. (www.refusersolidarity.net), which supports the Israeli military refuser
With the error instance, I can store information in movement. steven@stevenfeuerstein.com.

Know a clever shortcut? Have an idea for an article for Oracle Professional?
Visit www.pinnaclepublishing.com and click on “Write For Us” to submit your ideas.

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


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

NAME ❑ Check enclosed (payable to Pinnacle Publishing)


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

CITY STATE/PROVINCE ZIP/POSTAL CODE


SIGNATURE (REQUIRED FOR CARD ORDERS)

COUNTRY IF OTHER THAN U.S.


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

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


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

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

www.pinnaclepublishing.com Oracle Professional September 2004 15


The GZIP Classes... compressed string will be longer than the original string
prior to compression.
Continued from page 11

What’s next
end if; This article covered the use of the GZIP classes.
end loop;
if vCtr >= 499 then
java.util.zip also contains methods for processing ZIP
dbms_job.submit(vjob, files. ZIP is, of course, the standard format for both
'compressJob('||to_char(pInitial + 500)||');',
sysdate);
compressing and archiving files. Because a ZIP file (or
end if; BLOB!) may contain multiple files, along with names,
end;
comments, dates, and so on, the API that’s needed to
make full use of these classes is more complex than what
If the job processes a full 500 rows, it resubmits
I created here to use GZIP. I plan to discuss a PL/SQL
itself, passing <pInitial + 500> as the starting point for
API for creating and reading ZIP files in a future Oracle
the next iteration. This allows any number of rows to be
Professional article. ▲
processed without expanding Session PGA Memory to
unworkable limits. Using this strategy, I was able to 409MENCHEN.TXT at www.pinnaclepublishing.com
complete my test case of verifying compression and
decompression on all 25,000 plus rows in the ALL_ Gary Menchen is a senior programmer analyst at Dartmouth College
OBJECTS view. Please note that this exercise was for in Hanover, NH. One of his current interests is document generation
testing purposes only—there’s no space savings to be in PL/SQL, and he’s putting the finishing touches on a package that
realized in using GZIP to compress a column that will allow generation of PDF documents from within PL/SQL.
averages less that 50 characters in length; the resulting Gary.E.Menchen@Dartmouth.Edu.

September 2004 Downloads


• 409MENCHEN.TXT—Source code to accompany Gary • 409HAMILTON.ZIP—Source code to accompany Peter
Menchen’s article, “Accessing java.util.zip from PL/SQL: Hamilton’s article, “Attribute Denormalization.”
The GZIP Classes.”

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

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


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

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

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

16 Oracle Professional September 2004 www.pinnaclepublishing.com

You might also like