Visit the Quest Software Home Page. Pipelines Home

Please Note: Unless otherwise noted, all of the PL/SQL Pipeline's monthly tips are contributed by Steven Feuerstein.

PL/SQL Pipeline  Oracle PL/SQL Tips of the Month - 1999

January
February
March
April
May
June
July
August
September
October
November
December

1997 Tips
1998 Tips


January's Tip of the Month

As of Oracle 7.3.4 and Oracle 8.0.4, you can now call DBMS_OUTPUT and DBMS_PIPE from within functions which are then called from within SQL. This allows you to build a trace feature into your SQL. The following function, for example, uses DBMS_OUTPUT.PUT_LINE to display the ROWID of each row touched by a query:

CREATE OR REPLACE FUNCTION traceit (
   tab IN VARCHAR2,
	rowid_in IN ROWID)
	RETURN INTEGER
IS
BEGIN
   DBMS_OUTPUT.PUT_LINE (tab || '-' || 
      ROWIDTOCHAR (rowid_in));
	RETURN 0;
END;
/
This function returns a "dummy" value of zero; it has to be a function to be callable directly in SQL, but its return value is irrelevant. I can then use this function inside a query as follows:
SELECT ename, traceit ('emp', ROWID)
  FROM emp;
But, but, but! There is one little complication to keep in mind. If I run the above query on my standard demo emp table, I see this output:
ENAME      TRACEIT('EMP',ROWID)

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

SMITH                         0

ALLEN                         0

WARD                          0

JONES                         0

MARTIN                        0

BLAKE                         0

CLARK                         0

SCOTT                         0

KING                          0

TURNER                        0

ADAMS                         0

JAMES                         0

FORD                          0

MILLER                        0
Well, that's odd...where is my trace information? Here, I will produce it by executing a "do nothing" PL/SQL block:
SQL> BEGIN NULL; END;
  2  /

emp-AAAAfBAACAAAAEqAAA

emp-AAAAfBAACAAAAEqAAB

emp-AAAAfBAACAAAAEqAAC

emp-AAAAfBAACAAAAEqAAD

emp-AAAAfBAACAAAAEqAAE

emp-AAAAfBAACAAAAEqAAF

emp-AAAAfBAACAAAAEqAAG

emp-AAAAfBAACAAAAEqAAH

emp-AAAAfBAACAAAAEqAAI

emp-AAAAfBAACAAAAEqAAJ

emp-AAAAfBAACAAAAEqAAK

emp-AAAAfBAACAAAAEqAAL

emp-AAAAfBAACAAAAEqAAM

emp-AAAAfBAACAAAAEqAAN
Ah! Now we see the trace information (and the new Oracle8 extended ROWID format). Remember that the DBMS_OUTPUT buffer is only flushed to the screen by SQL*Plus when the PL/SQL block terminates. The "SELECT ... FROM emp" query I executed is not a PL/SQL block, so when it finished running, the DBMS_OUTPUT buffer was not flushed. It was then necessary to run some PL/SQL block after the SQL statement.

Of course, if you run the above query from within a PL/SQL program, its output will be displayed when the program finishes.


February's Tip of the Month

Used to be that the legal range for date values in Oracle was January 1, 4712 BCE to January 1, 4712 AD. Now, we will all probably be ready to agree that seems like enough of a date range, but Oracle has expanded even this large range. How do I know? Well, a Pipeliner recently asked the what the maximum date allowed really is, because Dave found that he could go past that 4712 limit.

Rather than look up the value in some reference book or other (including mine), I decided to determine the maximum date by running a test (surely a more definitive answer can be found that way):

CREATE OR REPLACE PROCEDURE showmaxdate
IS
	old_date DATE;
	curr_date DATE := SYSDATE;
BEGIN
	LOOP
		old_date := curr_date;
		curr_date := curr_date + 1;
	END LOOP;
EXCEPTION
	WHEN OTHERS
	THEN
		DBMS_OUTPUT.PUT_LINE (SQLERRM);
		DBMS_OUTPUT.PUT_LINE ('Last date: ' ||
		   TO_CHAR (old_Date, 'MM-DD-YYYY')); 
END;
/
And here are the results:
SQL> exec showmaxdate

ORA-01841: (full) year must be between -4713 and +9999, 

   and not be 0

Last date: 12-31-9999
I don't have Oracle7 running in my office, so I cannot test this same script under 7.3. If anyone reading this does run the test, though, we will post the results.


March's Tip of the Month

Umesh Patel of Oracle Corporation contributed a very useful technique for sending email using PL/SQL, when you don't have the wonders of Oracle8 available to you. Here is what he wrote:

You can easliy implement this one [email from PL/SQL] if you work in the Unix environment. The following steps show you how.

Create the table mail_data with following the definition:

mailid number not null
seqno varchar2 not null,
body long

Then insert values into the above table as your PL/SQL code executes.

Here is an example of the kind of rows that might be placed in this table for an email message:

100,1,'/usr/lib/sendmail 

-ba -t <  100,2,'FROM : {actual_from text)'

100,3,'TO : ' {actual_to_text}'

100,4,'Cc : ' {actual_cc_text}'

100,5,'Subject : {acutal_subject_text}'

100,6,'body_text'

100,7,'EOF!'
So you've got the data sitting in the table. Now the trick is to extract that text within a program that can send some email. Here is Umesh's suggestion for doing this:

Create and execute the following shell script send_mail.sh as frequently as necessary by submitting it to cron job.

#send_mail.sh
# start here 
# set proper PATH variable here
export ORACLE_SID=
     orasid
export ORAENV_ASK=
 NO.
oraenv #
run sqlplus command to execute sql file #
which can be found after this script sqlplus
login/pwd @/path/genmail.sql #
remove spaces after EOF cut
-d! -f1 dbmail.sh >  /path/dbmail.sh
# Change the permission to execute this file
chmod 700 /path/dbmail.sh
# execute the generated file
/path/dbmail.sh
# end here
As you can see, this Unix shell script executes a SQL*Plus script named genmail.sql. Here is the definition of that script:
--genmail.sql file
set serveroutput on size 1000000
set feedback off
set echo off
spool /path/dbmail.sh
exec getmail
spool off
set serveroutput off
set feedback on
set echo on
-- end of sql script
But wait! This script runs a stored procedure, defined as follows:
CREATE OR REPLACE PROCEDURE getmail
IS
BEGIN
   FOR mail IN  (SELECT body
                 FROM mail_data
                ORDER BY mailid, seqno)
   LOOP
      DBMS_OUTPUT.put_line (mail.body);
   END LOOP;
END;
With all of these pieces in place, you will be sending email in Unix from PL/SQL in no time at all!


April's Tip of the Month

In-line Conditional Logic for PL/SQL

PL/SQL does not support DECODE natively; instead, you must call it inside a SELECT statement. That's too bad. DECODE can be very handy because (a) it implements a CASE statement, unavailable in PL/SQL and (b) allows you to do "in-line" conditional logic.

In other words, rather than write an IF statement like this:

IF myvar = 'A'
THEN
   mydate := SYSDATE;
ELSE
   mydate := SYSDATE + 10;
END IF;
You could use DECODE like this:
SELECT DECODE (myvar, 'A', SYSDATE, SYSDATE+10)
  INTO mydate
  FROM dual;
Well, it's not like we're really saving much code, are we? And personally I hate using SELECT FROM dual in my code. It is such an awkward work-around. So I came up with my one DECODE-like program in PL/SQL: the ifelse function. Here it is:
CREATE OR REPLACE FUNCTION ifelse
  (bool_in IN BOOLEAN, tval_in IN VARCHAR2, fval_in IN VARCHAR2)
   RETURN VARCHAR2
IS
BEGIN
  IF bool_in
  THEN
     RETURN tval_in;
  ELSE
     RETURN fval_in;
  END IF;
END;
Now I can transform the earlier IF statement to nothing more than this:
mydate := ifelse (myvar = 'A', SYSDATE, SYSDATE+10);
This is especially handy when you are constructing a string from multiple components, some of which may be null. Here is an example of my use of the ifelse function to do just that in a "watch" or debug package available in the demo.zip file in the Archives (see PL/SQL Seminar Files entry):
msg := 
   '***WATCHing at: ' || PLV.now || CHR(10) ||
   '   Context: ' || prog || CHR(10) || 
   ifelse (val IS NOT NULL, '   Message: ' || val || CHR(10), 'NO VAL') || 
   ifelse (using_cs, ' Callstack: ' || DBMS_UTILITY.FORMAT_CALL_STACK,
      NULL);
Without ifelse, I would have to write something like this:
msg := 
   '***WATCHing at: ' || PLV.now || CHR(10) ||
   '   Context: ' || prog || CHR(10);
IF val IS NOT NULL
THEN
   msg := msg || '   Message: ' || val || CHR(10);
ELSE
   msg := msg || 'NO VAL';
END IF;
IF using_cs
THEN
   msg := msg || ' Callstack: ' || DBMS_UTILITY.FORMAT_CALL_STACK;
END IF;
Finally, if you like the ifelse function, I suggest you overload it into a package, as I have done in the PLV package of PL/Vision:
PACKAGE PLV
IS
   FUNCTION ifelse (bool_in IN BOOLEAN, tval_in IN BOOLEAN, 
			fval_in IN BOOLEAN)
       RETURN BOOLEAN;
   PRAGMA RESTRICT_REFERENCES (ifelse, WNDS);

   FUNCTION ifelse (bool_in IN BOOLEAN, tval_in IN DATE, 
			fval_in IN DATE)
       RETURN DATE;
   PRAGMA RESTRICT_REFERENCES (ifelse, WNDS);

   FUNCTION ifelse (bool_in IN BOOLEAN, tval_in IN NUMBER,  
			fval_in IN NUMBER)
       RETURN NUMBER;
   PRAGMA RESTRICT_REFERENCES (ifelse, WNDS);

   FUNCTION ifelse (bool_in IN BOOLEAN, tval_in IN VARCHAR2, 
			fval_in IN VARCHAR2)
       RETURN VARCHAR2;
   PRAGMA RESTRICT_REFERENCES (ifelse, WNDS);
END;
From: Solomon Yakobson
To: Steven Feuerstein

function IFELSE mimicking DECODE is quite handy, however it does not mimic it 100%. There is a difference between decoded expressions and the function IFELSE. Actually, the same difference exists between IF statement mimicking DECODE and function IFELSE. I completely agree that DECODE can be 100% mimicked by IF statement:

SELECT DECODE (myvar, 'A', SYSDATE, SYSDATE+10)
INTO mydate
FROM dual;
is same as:
IF myvar = 'A'
THEN
mydate := SYSDATE;
ELSE
mydate := SYSDATE + 10;
END IF;
However, it is different from:
mydate := ifelse (myvar = 'A', SYSDATE, SYSDATE+10);
I will explain why. In both DECODE and IF statement above the logic is as follows:
  1. Compare myvar with 'A'
  2. If result is true calculate and return SYSDATE
  3. If result is false calculate and return SYSDATE + 10
Now lets take a look at function call to IFELSE:
  1. Compare myvar to 'A' and assign it to some temp variable to be passed to IFELSE as first argument.
  2. Calculate sysdate and assign it to some temp variable to be passed to IFELSE as second argument.
  3. calculate sysdate+10 and assign it to some temp variable to be passed to IFELSE as third argument.
As you can see all decoded expressions are calculated BEFORE applying function IFELSE logic, another words before decoding. This means that if decoded expressions are interdependent we will get wrong results. I agree, it is not a good practice to use interdependent expressions in one statement, but... I just simply wanted to point this out. So if someone is using interdependent objects, function IFELSE will not work same way as DECODE (or IF statement) and will produce wrong results. For example:
SQL> create or replace package pkg1

2 as

3 X number := 0;

4 function f1 return number;

5 pragma restrict_references(f1,wnds);

6 end;

7 /

Package created.

SQL> create or replace package body pkg1

2 as

3 function f1 return number

4 is

5 begin

6 x := x + 1;

7 return x;

8 end;

9 end;

10 /

Package body created.

SQL> select decode('X','Y',pkg1.f1,pkg1.f1+10) from dual;

DECODE('X','Y',PKG1.F1,PKG1.F1+10)

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

11

SQL> set serveroutput on

SQL> begin

2 pkg1.x := 0; -- reset package variable so we start with 0 again.

3 dbms_output.put_line('DECODE via IF Statement');

4 if 'X' = 'Y'

5 then

6 dbms_output.put_line(pkg1.f1);

7 else

8 dbms_output.put_line(pkg1.f1+10);

9 end if;

10 end;

11 /

DECODE via IF Statement

11

PL/SQL procedure successfully completed.

SQL> begin

2 pkg1.x := 0; -- reset package variable so we start with 0 again.

3 dbms_output.put_line('DECODE via IFELSE Function.');

4 dbms_output.put_line(ifelse('X' = 'Y',pkg1.f1,pkg1.f1+10));

5 end;

6 /

DECODE via IFELSE Function.

12

PL/SQL procedure successfully completed.
As you can see, function IFELSE returned wrong (12 not 11) result. Why 12? DECODE and IF statement checked 'X' = 'Y' and since it is FALSE calculated expression PKG1.F1+10 only. Therefore, package variable PKG1.X was 0 when PKG1.F1+10 was calculated and PKG1.F1+10 resulted in 11. Function IFELSE call calculated 'X'='Y', which resulted in FALSE, then calculated PKG1.F1 which resulted in 1, however it incremented PKG1.X to 1. Then PKG1.F1+10 was calculated and since PKG1.X was changed to 1, resulted in 12. Only then function IFELSE was actually executed. Since first argument was FALSE it returned whatever was passed as third argument - 12.

There is one more aspect in the difference between DECODE and IFELSE. Since (as I explained in the posting) DECODE calculates only one of the decoded expressions and IFELSE calculates all of them, in can, in case of complex expressions, create some performance degradation.


May's Tip of the Month

Some Tips on Handling Mutating Table Errors

Susan Kleinfelter (skleinfelter@logility.com) offers these useful thoughts about dealing with mutating table errors (with some minor editing by that Feuerstein guy):

I noticed recently at least three questions in Pipetalk about the ORA-4091 error (table is mutating), where an insert or update trigger on table A tries to write to table B, but B has a FK back to A, causing the problem. A traditional to this problem solution (as offered, for example, in Scott Urman's Oracle8 PL/SQL Programming) involves using two triggers on A, one row level and one statement level. This approach often does not take into account any error handling, so I am suggest below some improvements (and some remaining problems).

The main idea is this: when the row level trigger on A fires, save the needed info from A into a PL/SQL table defined in a package. Then when the after statement level trigger fires (and A is no longer mutating), write the accumulated info from the PL/SQL table out to table B. After writing to B, delete all the rows from PL/SQL table so that it is ready for the next DML operation.

Problem 1: let's say the row-level trigger stored some info from A in the PL/SQL table, but when statement-level trigger attempts to write to B you get an error, such as unique constraint violated (may indicate a design flaw elsewhere, but I often had this problem during testing when I failed to blow all my test data away). You will not arrive at the line of code that empties the PL/SQL table, so old data will remain in it from this statement when it is called for the next statement.

Solution: add an error handler, and always empty the PL/SQL table when any error occurs. Then re-raise the error to the caller unless it is something you can handle. However, this still leaves...

Problem 2: If your row-level trigger on A is before insert or update, you may add something to the PL/SQL table, but then the insert or update to A may fail, due perhaps to a constraint violation. This error does not propagate out to the before trigger, which has already completed execution for this row, so there is no way to empty the PL/SQL table in response to this error. For this reason it is better to add the info to PL/SQL table in an after row-level trigger only.

Problem 3: Imagine a single statement is inserting 10 rows to A, and first 5 succeed, so 5 rows get added to PL/SQL table, then 6th row fails and caller gets the error (no trigger gets the error if it is on the actual insert or update, because triggers only execute before or after, not during). The after statement trigger does not fire in this case because the statement didn't finish, so the 5 rows don't get written from PL/SQL table to B and also don't get removed from PL/SQL table. Next time an insert or update succeeds on A, they will be written to B even though their corresponding A rows did not get inserted. Solution to this: I added a before insert or update statement level trigger to A that empties out the PL/SQL table, so it never begins a statement with data left over from an earlier statement that errored out.

Just thought I would share my experience with this, since I have found a number of the published solutions tantalizing but woefully incomplete.

- Susan


June's Tip of the Month

Problem: Generate PDF files and HTML pages from Oracle data for publication on the Internet.:

Solution from Stanislav Pichmanov: From the PL/SQL cartridge application, we place requests in an Oracle database table. Then we use the Report Queue Engine or Oracle Reports, running outside of the application server, to generates reports in HTML and PDF format whenever it finds requests in that Oracle table. Once the HTML or PDF file is generated, we can display it easily in our browser


July's Tip of the Month

The Dangers of Flushing

To a file, that is. You use UTL_FILE.PUT_LINE to write a line to a file, and while your program is still running and generating more output, you look at the file. To your severe disappointment, you don't see anything (or at least not the latest data) in the file. What's the problem? The actual write to disk is usually an asynchronous process; the operating system decides when to "flush" the data to disk to improve throughput.

UTL_FILE offers the FFLUSH procedure to force all information physically out to the specified file, as in:

DECLARE
   fID UTL_FILE.FILE_TYPE;
BEGIN
   ...
   UTL_FILE.FFLUSH (fID);
If you want to make sure you see data in the file as soon as it is written, you could call FFLUSH after every PUT_LINE call. You could even create a little "combo" program to handle this for you:
CREATE PROCEDURE file_put (
   fID IN UTL_FILE.FILE_TYPE,
   line IN VARCHAR2)
IS
BEGIN
   UTL_FILE.PUT_LINE (fID, line);
   UTL_FILE.FFLUSH (fID);
END;
/
You should, however, be judicious in your calls to FFLUSH. It could slow down quite dramatically the performance of your application. This experience was related in Pipetalk this month in the File I/O Conference, under the title "SLOOOOOOOOOOOW".

You might want to build an occasional flush into your writes to a file. You could build a package about UTL_FILE that sets a counter to 0 when you open the file. Then every time you call PUT_LINE, it increments the counter. When the counter hits the specified threshold, FFLUSH is called automatically.


August's Tip of the Month

A common request in the world of SQL*Plus is to change the prompt to reflect information about the environment, such as the Oracle instance. Mike Ault, RevealNet Oracle Administration Knowledge Base author, offers this code to accomplish the prompt change:

column xdb_name new_value xdb noprint;
select name||': ' xdb_name from v$database;
set sqlprompt &xdb
You can put these three lines inside your glogin.sql or login.sql scripts so that the prompt is set when you start up SQL*Plus (note that when you CONNECT from within SQL*Plus to another schema you will have to run one of these files manually to reset the prompt).

So there's one example of a SQL*Plus prompt change. Do any of our brilliant and creative Pipeliners from around the world have any other useful prompt settings that they use and would like to share? If so, please go to Pipetalk and add your entry in the new FAQ conference under the "Modifying the SQL*Plus prompt" posting.

Thanks, SF


September's Tip of the Month

Passing Environmental Variables to an External Procedure

When you call an external procedure, Oracle passes only three environment variables - ORACLE_HOME, ORACLE_SID, and LD_LIBRARY_PATH - to that external procedure. If you want to pass any other environment variable besides those three, you need to edit the listener.ora file and add a line of this format:

(ENVS='environment_variable=value,environment_variable=
	value,...environment_variable=value')
to the extproc service. When you define these environmental variables, you should type them all on the same line, separating each by a comma without any whitespace characters, such as spaces or tabs.

Note: NLS_NUMERIC_CHARACTERS must not be the last entry in the ENVS list.

Suppose (as was asked on Pipetalk and answered by Solomon Yakobson, who provides this month's tip) that you want to pass the location of the external procedure so that it does not have to be hard-coded in your PL/SQL application. In this case you would add the following line:

(ENVS='PATH=/usr/bin')


October's Tip of the Month

Querying from an Index-By Table

So you've defined an index-by table (formerly known as PL/SQL tables), and you want to be able use SQL to query the results. Sadly, index-by tables are 100% programmatic PL/SQL constructs and cannot be referenced natively from within SQL. There is, however, a workaround, one that was suggested by several people on the PL/SQL Pipeline (most notably and with content for this tip drawn from Susanna Batson, at smbatson@technologist.com):

Instead of referencing the index-by table contents directly, you can call a function that returns the value in a row.

Here is one such example:

SELECT Col1, Col2
  FROM My_DB_Table
 WHERE Col3 IN (
    SELECT ibtable_row (ColId)
      FROM Place_holder_table);
where Place_holder_table is a table that simply provides a context in which to reference the ibtable_row function, which would look something like this..
CREATE OR REPLACE FUNCTION ibtable_row (colid IN NUMBER)
   RETURN VARCHAR2
IS
BEGIN
   RETURN mypkg.ibtable (colid);
END;
Where mypkg is the package that contains the declaration of ibtable. If the function is defined in the same package, you do not have to qualify the reference to the index-by table.

The place_holder_table might be dual, so that it only returns one value/row. You could also correlate the row number passed to ibtable_row with information in that table.


November's Tip of the Month

Oracle Support for PL/SQL...

As I travel to different locales for seminars and presentations on PL/SQL, I am asked rather consistently the following questions:

Is Oracle going to drop support for PL/SQL and just move to offering Java in the database?

For Oracle's official positioning on this, check out the following URL:

Regrettably, Oracle has removed this URL from their web site and this link is no longer valid. We apologize for the inconvenience.

RevealNet Technical Support
March 31, 2000

http://www.oracle.com/java/8i/faq/815faq0005.html


December's Tip of the Month

Specifying UTL_FILE Directories

With the help of my friend and Oracle Technologist, Jay Smith (jaysmith@tri-smith.com), I discovered something new and different about the way the UTL_FILE_DIR parameter is handled, at least in Oracle8i.

The UTL_FILE_DIR parameter specifies one or more directories in which you can read/write files with the UTL_FILE package. You can list multiple directories in one statement, separated by a semi-colon, as in:

UTL_FILE_DIR=/tmp;/tmp/accounts;/apps/oracle/ar
You can also specify these directories in separate parameter entries, as in:
UTL_FILE_DIR=/tmp
UTL_FILE_DIR=/tmp/accounts
UTL_FILE_DIR=/apps/oracle/ar
Our recent discovery is that if you do not list these multiple entries CONTIGUOUSLY (ie, if you separate the entries with other parameter settings), then only the last set (one or more) processed by Oracle at instance startup will be recognized.

So if I specify my three directories shown above in this fashion:

UTL_FILE_DIR=/tmp
compatible = 8.1.0
UTL_FILE_DIR=/tmp/accounts
UTL_FILE_DIR=/apps/oracle/ar
Then the settings in the database will not include /tmp, as can be verified with this query:
SQL> SELECT value FROM v$parameter WHERE name='utl_file_dir';
VALUE
----------------------------------------------------------------
/tmp/accounts, /apps/oracle/ar

PL/SQL Pipeline

For questions or comments on this site: webmaster@quest-pipelines.com
All content Copyright © Quest Software, Inc. All rights reserved.