Oracle PL/SQL Tips of the Month - 2005
| January | FORMAT_ERROR_STACK versus SQLERRM |
| February | PL/SQL Distance Calculator |
| March | Use DBMS_UTILITY.FORMAT_ERROR_STACK instead of SQLERRM |
| April | Running Hierarchy Queries Against DUAL |
| May | Using a Comma-separated List in an SQL Query |
| June | Analyzing Laggards Updated 7/11/05 |
| July | NVL Smarts |
| August | Execute Shell Commands from PL/SQL |
| September | Record-level DML |
| October | Getting the SQL Plan for a Cursor with DBMS_XPLAN |
| November | Procedure to Convert Numbers to Text |
| December | Name Those Exceptions! |
January's Tip of the Month
FORMAT_ERROR_STACK versus SQLERRM
Compliments of Steven Feuerstein, Pipeline SYSOP.
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20501
It is generally preferable to use DBMS_UTILITY.FORMAT_ERROR_STACK in place of SQLERRM. Both
SQLERRM and DBMS_UTILITY.FORMAT_ERROR_STACK return the text describing the most recent
error. SQLERRM is, however, limited to the first 255 characters of that text.
DBMS_UTILITY.FORMAT_ERROR_STACK returns the full error message text, and for that reason should
be used in place of SQLERRM.
You may still want to use SQLERRM if you need to retrieve the error message for a given error
code, as in:
BEGIN
DBMS_OUTPUT.PUT_LINE (SQLERRM (-1855));
END;
/
SQLERRM accepts an error code as its only parameter and returns the error message string
(default value is SQLCODE). The DBMS_UTILITY.FORMAT_ERROR_STACK function only returns the error
message for the current SQLCODE value.
If you are using Oracle10g, you may also make use of the new built-in function,
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE, to obtain the line number on which an error was originally
raised. Note that this line number is reset whenever an exception is re-raised with RAISE; or
another exception is raised within an exception.
February's Tip of the Month
PL/SQL Distance Calculator
Compliments of Leon Rzhemovskiy, Unika Solutions (unika_solutions-leon@yahoo.ca).
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20502
The success or failure of a marketing campaign can often be determined by the distance
bewteen the customer and the retail outlet. Using the same city or area code as a distance
measurement is rarely accurate enough. Measuring the actual distance between the retail outlet
and the customer's postal code is a reliable solution.
The following function calculates the distance between 2 points using the Great Circle
formula:
FUNCTION get_geo_distance(lat1 IN NUMBER, lon1 IN NUMBER,lat2 IN NUMBER, lon2 IN NUMBER) RETURN
NUMBER
IS
v_r NUMBER :=6378.7;
BEGIN
IF lat1=lat2 AND lon1=lon2 THEN
RETURN 0;
END IF;
RETURN v_r * ACOS(
SIN(lat1) * SIN(lat2) +
COS(lat1) * COS(lat2) * COS(lon2- lon1)
);
EXCEPTION WHEN OTHERS THEN
RETURN NULL;
END;
The only missing part is a data file with longitude and latitude information for every postal
code. This is available as a subscription with monthly updates or as a one time purchase. My
comparison between vendors suggests ZIPCodeWorld.com as a winner with regards to data accuracy.
March's Tip of the Month
Use DBMS_UTILITY.FORMAT_ERROR_STACK instead of SQLERRM
Compliments of Steven Feuerstein, Pipeline SYSOP.
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20503
Note: This is an amplified version of our January tip, also provided by Steven.
SQLERRM is a function that returns the error message for a particular error code. If you do not
pass an error code to SQLERRM, then it returns the error message associated with the value returned
by SQLCODE.
DBMS_UTILITY.FORMAT_ERROR_STACK, like SQLERRM, returns the message associated with the current
error, i.e., the value returned by SQLCODE. It differs from SQLERRM in two ways:
Its length is not restricted; it will return the full error message string. SQLERRM will
truncate the string at either 255 or 512 bytes (depending on your version of Oracle).
You cannot pass an error code number to this function; it cannot be used to return the message
for a "random" error code.
You should as a rule call this function inside your exception handler logic to obtain the full
error message. Of course, you will not want to call DBMS_OUTPUT.PUT_LINE to display the value, since
PUT_LINE has trouble with strings of more than 255 characters!
While it is true that you should use DBMS_UTILITY.FORMAT_ERROR_STACK in place of SQLERRM, that
doesn't mean SQLERRM is totally irrelevant. In fact, you can use it to answer the following questions:
As mentioned earlier, SQLERRM will return the error message for an error code. If, however, you
pass SQLERRM a code that is not valid, it does not raise an exception. Instead, it returns a string
in one of the following two forms:
If the number is negative...
ORA-NNNNN: Message NNNNN not found; product=RDBMS; facility=ORA
If the number is positive...
-NNNNN: non-ORACLE exception
You can use these facts to build functions to neatly return information about whatever code you
are currently working with. Here is the code for a package with such programs:
CREATE OR REPLACE PACKAGE oracle_error_info
IS
FUNCTION is_app_error (code_in IN INTEGER)
RETURN BOOLEAN;
FUNCTION is_valid_oracle_error (
code_in IN INTEGER
, app_errors_ok_in IN BOOLEAN DEFAULT TRUE
, user_error_ok_in IN BOOLEAN DEFAULT TRUE
)
RETURN BOOLEAN;
PROCEDURE validate_oracle_error (
code_in IN INTEGER
, message_out OUT VARCHAR2
, is_valid_out OUT BOOLEAN
, app_errors_ok_in IN BOOLEAN DEFAULT TRUE
, user_error_ok_in IN BOOLEAN DEFAULT TRUE
);
END oracle_error_info;
/
CREATE OR REPLACE PACKAGE BODY oracle_error_info
IS
FUNCTION is_app_error (code_in IN INTEGER)
RETURN BOOLEAN
IS
BEGIN
RETURN code_in BETWEEN -20999 AND -20000;
END is_app_error;
PROCEDURE validate_oracle_error (
code_in IN INTEGER
, message_out OUT VARCHAR2
, is_valid_out OUT BOOLEAN
, app_errors_ok_in IN BOOLEAN DEFAULT TRUE
, user_error_ok_in IN BOOLEAN DEFAULT TRUE
)
IS
l_message VARCHAR2 (32767);
PROCEDURE set_failure
IS
BEGIN
message_out := NULL;
is_valid_out := FALSE;
END set_failure;
BEGIN
l_message := SQLERRM (code_in);
IF is_app_error (code_in) AND NOT app_errors_ok_in
THEN
set_failure;
ELSIF code_in = 1 AND NOT user_error_ok_in
THEN
set_failure;
ELSIF l_message LIKE 'ORA-_____: Message%not found;%'
OR l_message LIKE '%: non-ORACLE exception%'
THEN
set_failure;
ELSE
message_out := l_message;
is_valid_out := TRUE;
END IF;
EXCEPTION
WHEN OTHERS
THEN
IF SQLCODE = -1426
THEN
set_failure;
ELSE
RAISE;
END IF;
END validate_oracle_error;
FUNCTION is_valid_oracle_error (
code_in IN INTEGER
, app_errors_ok_in IN BOOLEAN DEFAULT TRUE
, user_error_ok_in IN BOOLEAN DEFAULT TRUE
)
RETURN BOOLEAN
IS
l_message VARCHAR2 (32767);
retval BOOLEAN;
BEGIN
validate_oracle_error (code_in
, l_message
, retval
, app_errors_ok_in
, user_error_ok_in
);
RETURN retval;
END is_valid_oracle_error;
END oracle_error_info;
/
You can find this program and many other useful templates and reusable code inside Qnxo, a new
product that Steven has created. For more information, visit www.qnxo.com.
April's Tip of the Month
Running Hierarchy Queries Against DUAL
Compliments of Oleg Savkin.
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20504
The following are some findings regarding the use of hierarchical queries from DUAL.
For example, consider the following table with amounts/numbers:
select *
from my_table:
20
30
31
45
2000
...
N
Now consider the task of finding all missing amounts/numbers in this table. It was easy to do
like this:
select rownum
from dual
connect by level <= N
minus
select col
from my_table
This query worked fast enough, as you can see below from the tests done by Daniel Clamage.
So, the following query...
select rownum
from dual
connect by level <= N
...can be used to generate sequences of numbers, and is very simple to utilize.
Test for Oracle 9i (9.2.0)
This query runs fine in Oracle 9i, but will return only 1 record if you run it as it is:
SQL>SELECT rownum from dual connect by level < 100;
ROWNUM
----------
1
The next example will return all records:
SQL>select * from (SELECT rownum from dual connect by level < 10)
2 /
ROWNUM
----------
1
2
3
4
5
6
7
8
9
9 rows selected.
Test for Oracle 8i (8.1.7)
SQL>SELECT rownum from dual connect by level < 10;
ROWNUM
----------
1
Result: one row.
SQL>select * from (SELECT rownum from dual connect by level < 10);
ERROR:
ORA-01436: CONNECT BY loop in user data
no rows selected
Result: Oracle error
Test for Oracle 10g
I didn't run it in 10g, but Daniel Clamage mentioned it runs normally, and returns all records.
Of course, there are lots of other ways to generate sequences of numbers. Consider the following
package example was proposed by Daniel Clamage:
CREATE OR REPLACE
type typ_num is table of number;
/
create or replace package numtest is
function num(p_num in integer)
return typ_num;
end numtest;
/
create or replace package body numtest is
function num(p_num in integer)
return typ_num is
n typ_num := typ_num();
begin
n.extend(p_num);
return(n);
end num;
end numtest;
/
select rownum from table(numtest.num(9));
ROWNUM
----------
1
2
3
4
5
6
7
8
9
Here is the timing for both approaches - query from dual and from package:
Query from dual:
SQL> select count(*) cnt
2 from (
3 select rownum
4 from dual
5 connect by level <= 1000000
6 );
CNT
----------
1000000
Elapsed: 00:00:00.90
SQL> /
CNT
----------
1000000
Elapsed: 00:00:00.90
Query from package:
SQL> select count(*) cnt
2 from (
3 select rownum
4 from table(numtest.num(1000000))
5 );
CNT
----------
1000000
Elapsed: 00:00:00.64
SQL> /
CNT
----------
1000000
Finally, here are static explain plans for each:
begin
explan9i('select count(*) cnt
from (
select rownum
from dual
connect by level <= 1000000
)');
end;
/
Plan hash value: 4168197557
----------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 2 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | | | |
| 2 | VIEW | | 1 | | 2 (0)| 00:00:01 |
| 3 | COUNT | | | | | |
| 4 | CONNECT BY WITHOUT FILTERING| | | | | |
| 5 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
----------------------------------------------------------------------------------------
begin
explan9i('select count(*) cnt
from (
select rownum
from table(numtest.num(1000000))
)');
end;
/
Plan hash value: 2760869553
---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 24 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | | | |
| 2 | VIEW | | 8168 | | 24 (0)| 00:00:01 |
| 3 | COUNT | | | | | |
| 4 | COLLECTION ITERATOR PICKLER FETCH| NUM | | | | |
---------------------------------------------------------------------------------------------
The connect-by has a much lower cost (2 vs. 24).
May's Tip of the Month
Using a Comma-separated List in an SQL Query
Compliments of William Robertson, PL/SQL SYSOP.
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20505
After "How do I display multiple rows as one comma-separated list?", one of the next most-asked
Oracle questions on the Web is a related one:
My procedure is passed a comma-separated list of IDs, for example 7369,7499,7839,7902. I tried to
use it in my code like this:
DECLARE
p_empno_list CONSTANT VARCHAR2(20) := '7369,7499,7839,7902';
BEGIN
FOR r IN (
SELECT * FROM emp
WHERE empno IN (p_empno_list)
)
LOOP
DBMS_OUTPUT.PUT_LINE(RPAD(r.empno,9) || r.ename);
END LOOP;
END;
/
but it just gives me an error:
ERROR at line 1:
ORA-01722: invalid number
ORA-06512: at line 4
(When SQL is expecting a character string such as SMITH, but is passed a comma-separated list
such as SMITH,JONES,FORD,MILLER, no error is produced, but the query simply returns no rows.)
Clearly, SQL does not consider...
WHERE empno IN (p_empno_list)
...to be equivalent to:
WHERE empno IN (7369,7499,7839,7902)
And of course it cannot, and should not. What if you had a value such as '123, Fake St,
Springfield' that represented a single address line? How would you feel if you used v_address_line
in a query similar to the one above, and SQL chose to treat it as not one but three values ('123',
'Fake St' and 'Springfield'), just because it happened to contain commas and the query used IN?
It would be wrong! SQL has to treat each variable as one value.
What the programmer is really looking for is a different type of variable that explicitly
represents a set of values. Fortunately Oracle SQL supports exactly this, in the form of
collections.1
You create a scalar collection type like this:
CREATE TYPE INTEGER_TT AS TABLE OF INTEGER
/
The name itself is not important except that it should give an indication of what it represents,
and the fact that you will have to live with it. I use _TT ("table type") for collection types (see
my PL/SQL Coding Standards for more
suggestions regarding naming standards). Since
it should be a generic type that you will reuse in many different places, it is better to refer to
the base datatype, INTEGER, than the fact that right now you want it for an employee ID. While you
are at it, why not create a couple more:
CREATE TYPE NUMBER_TT AS TABLE OF NUMBER
/
CREATE TYPE VARCHAR2_TT AS TABLE OF VARCHAR2(4000)
/
Now, when your list of IDs is an INTEGER_TT instead of a plain old character string, you can
use it in SQL:
DECLARE
p_empno_list CONSTANT INTEGER_TT := INTEGER_TT(7369,7499,7839,7902);
BEGIN
FOR r IN (
SELECT empno, ename 2
FROM emp
WHERE empno IN
( SELECT column_value 3
FROM TABLE(p_empno_list) )
)
LOOP
DBMS_OUTPUT.PUT_LINE(RPAD(r.empno,9) || r.ename);
END LOOP;
END;
/
7369 SMITH
7499 ALLEN
7839 KING
7902 FORD
The only problem now is how to get an INTEGER_TT collection into your procedure in place of a
character string. The simplest solution is to change your procedure's parameter list so that it is
by definition passed one. If an existing application has been built to pass character strings (I
can never understand why anyone would do this as it just creates more work, but apparently it happens
frequently), here are some suggestions:
- Change the calling procedure so that it calls your procedure as
YOURPROC(INTEGER_TT(7369,7499,7839,7902));
instead of
YOURPROC('7369,7499,7839,7902');
- Create an overloaded version of your procedure that accepts a VARCHAR2 parameter. Within this
second version, convert the VARCHAR2 string into an INTEGER_TT collection using a SPLIT() function
(you'll have to write your own - an example is
here), and
call the first version passing the INTEGER_TT collection.
- As a last resort, continue to accept a character string as a parameter to your procedure, but
convert it to an INTEGER_TT within the procedure.
It is also worth mentioning the construction WHERE empno MEMBER OF p_empno_list available from
10g onwards. For a demo of this, see
Alternative IN-list binding in 10g on
Adrian Billington's site.
1 The only types of collection that SQL can handle are those you define in SQL - types
defined in a PL/SQL package are not recognised (PL/SQL understands SQL, not the other way around).
This gives you a choice between Nested Table types and VARRAYs. Use Nested Table types and not
VARRAYs unless there is some good reason to set a limit to the number of elements (this is the only
difference between Nested Tables and VARRAYs), when the collection is designed to contain some
particular number of elements (twelve months, seven days, five sexes etc).
2 In 9i, SELECT * used with TABLE() and no explicit CAST() gives error ORA-22905
cannot access rows from a non-nested table item.
3 COLUMN_VALUE is the default attribute name in a scalar collection type.
June's Tip of the Month
Analyzing Laggards
Compliments of Darryl Hurley, PL/SQL SYSOP.
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20506
Recently I had to write code to parse through simple test results to quickly find any differences.
Given two set ID values I needed to find any values in the second set that differ from those in the
first. This was to be used as a polling mechanism to determine if downstream actions need to be
taken.
Here’s the table I had to query the test results from:
SQL> desc demo
Name Null? Type
------------- -------- ------
SET_ID NOT NULL NUMBER
LINK_ID NOT NULL NUMBER
RESULT NOT NULL NUMBER
I needed to identify the RESULT differences by LINK_ID. Here’s an example data set:
SET_ID LINK_ID RESULT
---------- ---------- ----------
1 1 22
1 2 22
2 1 22
2 2 33
2 3 44
The differences in this set would be link 2 and 3. My first thought was to use the MINUS operator that
would build two result sets and then subtract the matching entries to leave the unmatched ones
behind.
SQL> SELECT link_id,
2 result
3 FROM demo
4 WHERE set_id = 2
5 MINUS
6 SELECT link_id,
7 result
8 FROM demo
9 WHERE set_id = 1;
LINK_ID RESULT
---------- ----------
2 33
3 44
However, my polling code will be running almost constantly as new test results arrive throughout
the day and some result sets will be very large. Thus I need to consider the amount of work the
database has to do to determine the differences. To get a baseline I’ll use AUTOTRACE on my MINUS
query.
Statistics
---------------------------------------------------
0 recursive calls
0 db block gets
18 consistent gets
0 physical reads
0 redo size
258 bytes sent via SQL*Net to client
345 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)
2 rows processed
Nothing too worrisome there but I still want to look at ways to reduce the numbers.
I’ve always liked Oracle’s set operators (UNION, MINUS and INTERSECT) and have used them in a lot
of situations requiring result set comparisons. But perhaps I’d become too reliant upon them and
should start looking at alternatives. For my current requirement it might be easier to work through
the records once ordered by LINK_ID and SET_ID and compare the values in the RESULTS column across
sequential rows. The first two rows I’d look at would be these:
SET_ID LINK_ID RESULT
---------- ---------- ----------
1 1 22
2 1 22
I’d quickly determine that they were the same, meaning the test of link 1 in both set 1 and 2
produced a result of 22. Unfortunately Oracle queries are very focused on the present - the present
row that is. It would be nice to look at fields in the previous record for comparison. As luck would
have it I was reading about Oracle’s great new analytical query functions and decided this might be a
good time to get my feet wet.
It so happens that Oracle offers a function that does exactly what I want – to look at a previous
record for comparison. It’s called the LAG function. I assume that’s a scientific term and not short
for laggard. I quickly set about seeing how it might help. Here’s what I came up with.
SQL> SELECT set_id,
2 link_id,
3 LAG(result) OVER (ORDER BY link_id) prev_result,
4 result
5 FROM demo
6 WHERE set_id IN (1,2)
7 ORDER BY link_id;
SET_ID LINK_ID PREV_RESULT RESULT
---------- ---------- ----------- ----------
1 1 22
2 1 22 22
1 2 22 22
2 2 22 33
2 3 33 44
The result set is exactly what I need. Each record includes the previous record's RESULT column.
The exception, of course, is the first record. The way I like to think of the LAG function specified
on line 3 is that it performs a pseudo-query of the demo table ordered by LINK_ID getting values from
the RESULT column. It then matches these values up, lagging 1 record behind to assemble the result
set. That’s some very handy functionality but the really exciting thing is the AUTOTRACE output.
Statistics
---------------------------------------------------
0 recursive calls
0 db block gets
9 consistent gets
0 physical reads
0 redo size
355 bytes sent via SQL*Net to client
345 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
5 rows processed
The consistent gets have been cut in half. That’s because Oracle only had to visit each block in
memory one time to build the result set instead of the two visits required by the MINUS operator.
Now I’ll put it into an inline view to get the list of differing links.
SQL> SELECT link_id
2 FROM ( SELECT set_id,
3 link_id,
4 LAG(result) OVER (ORDER BY link_id) prev_result,
5 result
6 FROM demo
7 WHERE set_id IN (1,2)
8 ORDER BY link_id )
9 WHERE set_id = 2
10 AND prev_result <> result;
LINK_ID
----------
2
3
The moral of the story is just because these functions are labeled as analytics, don’t be afraid to
investigate them for their other benefits as well - mainly cheaper queries.
Update to Tip - 7/11/05
Eagle-eyed Pipeliner András Gábor found a potential flaw in the June PL/SQL Tip Of The Month.
The tip makes an assumption about the order of the records in the demo table. Here's a corrected
version of the query.
SELECT link_id
FROM ( SELECT set_id,
link_id,
LAG(result) OVER (ORDER BY link_id, set_id) prev_result,
result
FROM demo
WHERE set_id IN (1,2)
ORDER BY link_id, set_id )
WHERE set_id = 2
AND prev_result <> result;
Even this version will have issues if looking for the smaller set id (1), thus proving the
old adage that when something new is used, one cannot make any assumptions.
July's Tip of the Month
NVL Smarts
Reprinted from oracledba.co.uk
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20507
Oracle appears to have a new smart means for handling NVL around bind variables.
A common issue for reports and any query where users can pass parameters is how to handle the
"optional" parameter. Here's a typical example:
Table CP can be queried where column X is equal to optional parameter P. Should we code:
select *
from CP
where ( X = :P or :P is null)
Or
select *
from CP
where X = NVL(:P,X)
As always, the best way to find this out, is with a test case. First, some test data:
SQL> create table CP ( x number not null, y number);
Table created.
SQL> insert into cp select rownum,rownum
2 from all_objects
3 where rownum < 20000;
19999 rows created.
SQL> commit;
Commit complete.
SQL> create index cp1 on cp (x );
Index created.
SQL> analyze table cp estimate statistics;
Table analyzed.
Now lets see what happens with each scenario:
SQL> variable p number
SQL> set autotrace traceonly explain
SQL> select *
2 from cp
3 where ( x = :p or :p is null )
4 /
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=33 Card=1001 Bytes=68068)
1 0 TABLE ACCESS (FULL) OF 'CP' (Cost=33 Card=1001 Bytes=68068)
SQL> select *
2 from cp
3 where x = nvl(:p,x);
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=34 Card=2 Bytes=136)
1 0 CONCATENATION
2 1 FILTER
3 2 TABLE ACCESS (FULL) OF 'CP' (Cost=1 Card=1 Bytes=68)
4 1 FILTER
5 4 TABLE ACCESS (BY INDEX ROWID) OF 'CP' (Cost=1 Card=1 Bytes=68)
6 5 INDEX (RANGE SCAN) OF 'CP1' (NON-UNIQUE) (Cost=1 Card=1)
The second one possibly looks worse because it looks like a full scan and an index scan, but
look at the cost for the full scan. This looks a little odd - the cost is "1" whereas the previous
explain plan thinks the full scan costs "33". In the second case, what Oracle is doing is smart,
where it will defer the decision on the whether to do the full scan or the index scan based on
whether the parameter is actually provided or not. This apparently has come in somewhere around
the 8i stage - it does not appear to do this on 8.0. We can prove this "smart-choice" with some
timing results:
SQL> set autotrace traceonly statistics
SQL> exec :p := 123;
PL/SQL procedure successfully completed.
SQL> select *
2 from cp
3 where x = nvl(:p,x);
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
4 consistent gets
3 physical reads
0 redo size
307 bytes sent via SQL*Net to client
214 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
So we used the index to quickly get to the row, but when we null out the parameter:
SQL> exec :p := null;
PL/SQL procedure successfully completed.
SQL> select *
2 from cp
3 where x = nvl(:p,x);
19999 rows selected.
Statistics
----------------------------------------------------------
0 recursive calls
4 db block gets
1537 consistent gets
210 physical reads
0 redo size
623623 bytes sent via SQL*Net to client
92189 bytes received via SQL*Net from client
1336 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
19999 rows processed
And here we did the full scan (the best option because no parameter was given).
You'll find that if you use the other syntax, you will get a full tablescan every time. The reason
for this is that the queries actually could return a different result. It all all depends on the
column being queried not containing any nulls. If the column could contain nulls, then the check...
where x = nvl(:p,x)
...will not pick up any rows for which X is null (whereas the other query will).
The moral of the story:
- Ensure that any columns that will not be null are defined as such in the database
- Use the NVL clause on those columns to handle optional parameters
- If the column can contain nulls, if possible, dissolve the SQL into two queries:
if :p is null then
select *
from CP
else
select *
from CP
where x = :p
end if;
to at least get the index benefit when the parameter is actually given.
August's Tip of the Month
Execute Shell Commands from PL/SQL
Compliments of Labo-Oracle.com
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20508
A shell command can be executed from PL/SQL using a Java class. The creation of a Java class is
divided into three steps :
- Create the Java Source
- Publish the Call specification
- Grant privileges
So first, we need to create the Java class to perform the shell command:
CREATE OR REPLACE AND COMPILE JAVA SOURCE NAMED "Host" AS
import java.io.*;
public class Host {
public static void executeCommand(String command) {
try {
String[] finalCommand;
if (isWindows()) {
finalCommand = new String[4];
finalCommand[0] = "C:\\windows\\system32\\cmd.exe";
finalCommand[1] = "/y";
finalCommand[2] = "/c";
finalCommand[3] = command;
}
else {
finalCommand = new String[3];
finalCommand[0] = "/bin/sh";
finalCommand[1] = "-c";
finalCommand[2] = command;
}
final Process pr = Runtime.getRuntime().exec(finalCommand);
new Thread(new Runnable() {
public void run() {
try {
BufferedReader br_in = new BufferedReader(new InputStreamReader(pr.getInputStream()));
String buff = null;
while ((buff = br_in.readLine()) != null) {
System.out.println("Process out :" + buff);
try {Thread.sleep(100); } catch(Exception e) {}
}
br_in.close();
}
catch (IOException ioe) {
System.out.println("Exception caught printing process output.");
ioe.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
public void run() {
try {
BufferedReader br_err = new BufferedReader(new InputStreamReader(pr.getErrorStream()));
String buff = null;
while ((buff = br_err.readLine()) != null) {
System.out.println("Process err :" + buff);
try {Thread.sleep(100); } catch(Exception e) {}
}
br_err.close();
}
catch (IOException ioe) {
System.out.println("Exception caught printing process error.");
ioe.printStackTrace();
}
}
}).start();
}
catch (Exception ex) {
System.out.println(ex.getLocalizedMessage());
}
}
public static boolean isWindows() {
if (System.getProperty("os.name").toLowerCase().indexOf("windows") != -1)
return true;
else
return false;
}
};
/
show errors java source "Host"
Next we publish the call specification using a PL/SQL "wrapper" procedure:
CREATE OR REPLACE PROCEDURE Host_Command (p_command IN VARCHAR2)
AS LANGUAGE JAVA
NAME 'Host.executeCommand (java.lang.String)';
/
The relevant permissions must be granted from SYS for JServer to access the file system:
EXEC Dbms_Java.Grant_Permission('SCHEMA-NAME', 'java.io.FilePermission',
'<>', 'read ,write, execute, delete');
EXEC Dbms_Java.Grant_Permission('SCHEMA-NAME', 'SYS:java.lang.RuntimePermission',
'writeFileDescriptor', '');
EXEC Dbms_Java.Grant_Permission('SCHEMA-NAME', 'SYS:java.lang.RuntimePermission',
'readFileDescriptor', '');
The affects of the grant will not be noticed until the grantee reconnects.
And finally, to test it, we call the PL/SQL procedure with our command text:
SET SERVEROUTPUT ON SIZE 1000000
CALL DBMS_JAVA.SET_OUTPUT(1000000);
BEGIN
Host_Command (p_command => 'move C:\test1.txt C:\test2.txt');
END;
/
September's Tip of the Month
Record-level DML
Compliments of Steven Feuerstein, from the August issue of OPP/News
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20509
PL/SQL is tightly integrated with SQL, no doubt about it. That doesn't mean,
however, that it isn't possible to make that integration even tighter. Oracle
took another step in this direction in Oracle9i Database Release 2, when it
added native support for inserting and updating rows of data with PL/SQL
records.
Prior to this release, if I had filled up a %ROWTYPE record with data and
wanted to insert that record into my table, I would have to separate out each of
the individual fields in the INSERT statement. Here is an example. Given this
table:
CREATE TABLE books ( isbn VARCHAR2(13), title VARCHAR2(200), summary VARCHAR2(2000), author VARCHAR2(200), page_count NUMBER ) /
I can perform a record-level insert as follows:
DECLARE my_book books%ROWTYPE; BEGIN my_book.isbn := '1-56592-335-9'; my_book.title := 'ORACLE PL/SQL PROGRAMMING'; my_book.summary := 'General user guide and reference'; my_book.author := 'FEUERSTEIN, STEVEN AND BILL PRIBYL'; my_book.page_count := 1000;
INSERT INTO books VALUES my_book; END; /
Notice that I do not place the record inside parentheses. That difference in syntax
tells Oracle that you are passing an entire record. Notice also that with this
approach, you will need to have already generated the primary key value and
placed it into your record, or rely on a database trigger to generate the
value.
You can also perform updates with a record. The following example inserts a
row into the books table with a %ROWTYPE record. Notice that I use a new
keyword, ROW, to indicate that I am updating the entire row with a record:
DECLARE my_book books%ROWTYPE; BEGIN my_book.isbn := '1-56592-335-9'; my_book.title := 'ORACLE PL/SQL PROGRAMMING'; my_book.summary := 'General user guide and reference'; my_book.author := 'FEUERSTEIN, STEVEN AND BILL PRIBYL'; my_book.page_count := 980; -- new page count for 3rd edition UPDATE books SET ROW = my_book WHERE isbn = my_book.isbn; END; /
There are some restrictions on record-based updates:
I think that generally we will find record-based INSERTs more useful than
record-based UPDATEs. Regardless, it is always a pleasure to see (and use) a
PL/SQL that is ever more tightly integrated with the underlying SQL language!
October's Tip of the Month
Getting the SQL Plan for a Cursor with DBMS_XPLAN
Compliments of Adrian Billington
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20510
DBMS_XPLAN has been extended in 10g to show the actual SQL plan for a cursor ( i.e. a
particular execution of a SQL statement ).
In 9i, the method to achieve the same involved creating a view over V$SQL_PLAN that emulated
the PLAN_TABLE and then "tricking" DBMS_XPLAN into thinking it was reporting on an explain plan.
While this worked very nicely, Oracle has now supplied a proper means of accessing this
information. This information is useful to examine the plan of statements that didn't behave in
the way you expected. It is especially useful in that it also exposes the execution plan of
currently running SQL.
Let's run a simple SQL statement that will give a fairly large plan as it's over a
dictionary view...
@set_prompt
set echo on trimspool on pages 100 lines 155
spool dbms_xplan_display_cursor_demo.lst
SELECT /*+ FIND_ME_PLEASE */ COUNT(*)
FROM user_tables;
COUNT(*)
----------
12
Now let's get the relevant information from v$sql. To enable this to run as a single script,
I'm using substitution variables for simplicity. Normally, I would expect to just issue the
query manually and examine the output just in case there were multiple rows returned from
similar statements...
col sql_id new_value sqlid
col child_number new_value childno
SELECT sql_id, child_number
FROM v$sql
WHERE sql_text LIKE '%FIND_ME_PLEASE%'
AND LOWER(sql_text) NOT LIKE 'select sql_id%';
SQL_ID CHILD_NUMBER
------------- ------------
24v0zf5as1y1t 0
Now we have the relevant information to get the ACTUAL execution plan from V$SQL_PLAN...
col plan_table_output format a120
SELECT plan_table_output
FROM TABLE( DBMS_XPLAN.DISPLAY_CURSOR( '&sqlid', &childno ) );
old 2: FROM TABLE( DBMS_XPLAN.DISPLAY_CURSOR( '&sqlid', &childno ) )
new 2: FROM TABLE( DBMS_XPLAN.DISPLAY_CURSOR( '24v0zf5as1y1t', 0 ) )
PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------
SQL_ID 24v0zf5as1y1t, child number 0
-------------------------------------
SELECT /*+ FIND_ME_PLEASE */ COUNT(*) FROM user_tables
Plan hash value: 3933268824
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 254 (100)| |
| 1 | SORT AGGREGATE | | 1 | 132 | | |
|* 2 | HASH JOIN OUTER | | 8 | 1056 | 253 (1)| 00:00:04 |
| 3 | NESTED LOOPS OUTER | | 8 | 1032 | 251 (1)| 00:00:04 |
| 4 | NESTED LOOPS OUTER | | 8 | 976 | 243 (1)| 00:00:03 |
| 5 | NESTED LOOPS | | 8 | 944 | 243 (1)| 00:00:03 |
| 6 | NESTED LOOPS OUTER | | 8 | 888 | 235 (1)| 00:00:03 |
|* 7 | HASH JOIN | | 8 | 800 | 227 (1)| 00:00:03 |
| 8 | MERGE JOIN CARTESIAN | | 5 | 355 | 51 (0)| 00:00:01 |
| 9 | NESTED LOOPS | | 1 | 68 | 48 (0)| 00:00:01 |
|* 10 | FIXED TABLE FULL | X$KSPPI | 1 | 55 | 24 (0)| 00:00:01 |
|* 11 | FIXED TABLE FIXED INDEX| X$KSPPCV (ind:2) | 1 | 13 | 24 (0)| 00:00:01 |
| 12 | BUFFER SORT | | 5 | 15 | 27 (0)| 00:00:01 |
| 13 | TABLE ACCESS FULL | TS$ | 5 | 15 | 3 (0)| 00:00:01 |
|* 14 | TABLE ACCESS FULL | TAB$ | 15 | 435 | 175 (0)| 00:00:03 |
| 15 | TABLE ACCESS CLUSTER | SEG$ | 1 | 11 | 1 (0)| 00:00:01 |
|* 16 | INDEX UNIQUE SCAN | I_FILE#_BLOCK# | 1 | | 0 (0)| |
|* 17 | TABLE ACCESS BY INDEX ROWID| OBJ$ | 1 | 7 | 1 (0)| 00:00:01 |
|* 18 | INDEX UNIQUE SCAN | I_OBJ1 | 1 | | 0 (0)| |
|* 19 | INDEX UNIQUE SCAN | I_OBJ1 | 1 | 4 | 0 (0)| |
| 20 | TABLE ACCESS BY INDEX ROWID | OBJ$ | 1 | 7 | 1 (0)| 00:00:01 |
|* 21 | INDEX UNIQUE SCAN | I_OBJ1 | 1 | | 0 (0)| |
| 22 | TABLE ACCESS FULL | USER$ | 59 | 177 | 2 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("CX"."OWNER#"="CU"."USER#")
7 - access("T"."TS#"="TS"."TS#")
10 - filter("KSPPI"."KSPPINM"='_dml_monitoring_enabled')
11 - filter("KSPPI"."INDX"="KSPPCV"."INDX")
14 - filter(BITAND("T"."PROPERTY",1)=0)
16 - access("T"."TS#"="S"."TS#" AND "T"."FILE#"="S"."FILE#" AND "T"."BLOCK#"="S"."BLOCK#")
17 - filter("O"."OWNER#"=:B1)
18 - access("O"."OBJ#"="T"."OBJ#")
19 - access("T"."BOBJ#"="CO"."OBJ#")
21 - access("T"."DATAOBJ#"="CX"."OBJ#")
48 rows selected.
spool off
November's Tip of the Month
Procedure to Convert Numbers to Text
Compliments of Zakir Gulamali, positiveimager@yahoo.com
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20511
The procedure below converts a value between 1 and a (Zillion -1) to its text equivalent.
CREATE OR REPLACE PROCEDURE Num2Txt(p_num IN number, p_txt OUT varchar2) IS
v_char varchar2(50):=null;
v_int integer:=null;
v_dec_int integer:=null;
v_dec_char varchar2(25):=null;
c_trillions_hundreds varchar2(25):=null;
c_trillions_tens varchar2(25):=null;
c_trillions_units varchar2(25):=null;
c_trillions_unit varchar2(25):=null;
c_billions_hundreds varchar2(25):=null;
c_billions_tens varchar2(25):=null;
c_billions_units varchar2(25):=null;
c_billions_unit varchar2(25):=null;
c_millions_hundreds varchar2(25):=null;
c_millions_tens varchar2(25):=null;
c_millions_units varchar2(25):=null;
c_millions_unit varchar2(25):=null;
c_thousands_hundreds varchar2(25):=null;
c_thousands_tens varchar2(25):=null;
c_thousands_units varchar2(25):=null;
c_thousands_unit varchar2(25):=null;
c_hundreds varchar2(25):=null;
c_tens varchar2(25):=null;
c_units varchar2(25):=null;
c_unit varchar2(25):=null;
c_dec_tens varchar2(25):=null;
c_dec_units varchar2(25):=null;
c_dec_unit varchar2(25):=null;
p_trillions_hundreds varchar2(25):=null;
p_trillions_tens varchar2(25):=null;
p_trillions_units varchar2(25):=null;
p_trillions_unit varchar2(25):=null;
p_billions_hundreds varchar2(25):=null;
p_billions_tens varchar2(25):=null;
p_billions_units varchar2(25):=null;
p_billions_unit varchar2(25):=null;
p_millions_hundreds varchar2(25):=null;
p_millions_tens varchar2(25):=null;
p_millions_units varchar2(25):=null;
p_millions_unit varchar2(25):=null;
p_thousands_hundreds varchar2(25):=null;
p_thousands_tens varchar2(25):=null;
p_thousands_units varchar2(25):=null;
p_thousands_unit varchar2(25):=null;
p_hundreds varchar2(25):=null;
p_tens varchar2(25):=null;
p_units varchar2(25):=null;
p_unit varchar2(25):=null;
p_dec_tens varchar2(25):=null;
p_dec_units varchar2(25):=null;
p_dec_unit varchar2(25):=null;
v_one varchar2(25):=' One';
v_two varchar2(25):=' Two';
v_three varchar2(25):=' Three';
v_four varchar2(25):=' Four';
v_five varchar2(25):=' Five';
v_six varchar2(25):=' Six';
v_seven varchar2(25):=' Seven';
v_eight varchar2(25):=' Eight';
v_nine varchar2(25):=' Nine';
v_ten varchar2(25):=' Ten';
v_eleven varchar2(25):=' Eleven';
v_twelve varchar2(25):=' Twelve';
v_thirteen varchar2(25):=' Thirteen';
v_fourteen varchar2(25):=' Fourteen';
v_fifteen varchar2(25):=' Fifteen';
v_sixteen varchar2(25):=' Sixteen';
v_seventeen varchar2(25):=' Seventeen';
v_eighteen varchar2(25):=' Eighteen';
v_nineteen varchar2(25):=' Nineteen';
v_twenty varchar2(25):=' Twenty';
v_thirty varchar2(25):=' Thirty';
v_forty varchar2(25):=' Forty';
v_fifty varchar2(25):=' Fifty';
v_sixty varchar2(25):=' Sixty';
v_seventy varchar2(25):=' Seventy';
v_eighty varchar2(25):=' Eighty';
v_ninety varchar2(25):=' Ninety';
v_hundred varchar2(25):=' Hundred';
v_thousand varchar2(25):=' Thousand';
v_million varchar2(25):=' Million';
v_billion varchar2(25):=' Billion';
v_Trillion varchar2(25):=' Trillion';
BEGIN
v_int:=floor(p_num);
v_dec_int:=(p_num - v_int)*100;
v_char:=to_char(v_int);
v_dec_char:=to_char(v_dec_int);
c_trillions_hundreds :=substr(v_char,-15,1);
c_trillions_tens :=substr(v_char,-14,1);
c_trillions_units :=substr(v_char,-14,2);
c_trillions_unit :=substr(v_char,-13,1);
c_billions_hundreds :=substr(v_char,-12,1);
c_billions_tens :=substr(v_char,-11,1);
c_billions_units :=substr(v_char,-11,2);
c_billions_unit :=substr(v_char,-10,1);
c_millions_hundreds :=substr(v_char,-9,1);
c_millions_tens :=substr(v_char,-8,1);
c_millions_units :=substr(v_char,-8,2);
c_millions_unit :=substr(v_char,-7,1);
c_thousands_hundreds :=substr(v_char,-6,1);
c_thousands_tens :=substr(v_char,-5,1);
c_thousands_units :=substr(v_char,-5,2);
c_thousands_unit :=substr(v_char,-4,1);
c_hundreds :=substr(v_char,-3,1);
c_tens :=substr(v_char,-2,1);
c_units :=substr(v_char,-2,2);
c_unit :=substr(v_char,-1,1);
c_dec_tens :=substr(v_dec_char,1,1);
c_dec_units :=substr(v_dec_char,1,2);
c_dec_unit :=substr(v_dec_char,2,1);
IF c_dec_units < '20' THEN
CASE c_dec_units
WHEN '00' THEN p_dec_units:=null;
WHEN '01' THEN p_dec_units:=v_one;
WHEN '02' THEN p_dec_units:=v_two;
WHEN '03' THEN p_dec_units:=v_three;
WHEN '04' THEN p_dec_units:=v_four;
WHEN '05' THEN p_dec_units:=v_five;
WHEN '06' THEN p_dec_units:=v_six;
WHEN '07' THEN p_dec_units:=v_seven;
WHEN '08' THEN p_dec_units:=v_eight;
WHEN '09' THEN p_dec_units:=v_nine;
WHEN '10' THEN p_dec_units:=v_ten;
WHEN '11' THEN p_dec_units:=v_eleven;
WHEN '12' THEN p_dec_units:=v_twelve;
WHEN '13' THEN p_dec_units:=v_thirteen;
WHEN '14' THEN p_dec_units:=v_fourteen;
WHEN '15' THEN p_dec_units:=v_fifteen;
WHEN '16' THEN p_dec_units:=v_sixteen;
WHEN '17' THEN p_dec_units:=v_seventeen;
WHEN '18' THEN p_dec_units:=v_eighteen;
WHEN '19' THEN p_dec_units:=v_nineteen;
ELSE p_dec_units:=null;
END CASE c_dec_units;
ELSE
CASE c_dec_unit
WHEN '1' THEN p_dec_unit:=v_one;
WHEN '2' THEN p_dec_unit:=v_two;
WHEN '3' THEN p_dec_unit:=v_three;
WHEN '4' THEN p_dec_unit:=v_four;
WHEN '5' THEN p_dec_unit:=v_five;
WHEN '6' THEN p_dec_unit:=v_six;
WHEN '7' THEN p_dec_unit:=v_seven;
WHEN '8' THEN p_dec_unit:=v_eight;
WHEN '9' THEN p_dec_unit:=v_nine;
ELSE p_dec_unit:=null;
END CASE c_dec_unit;
END IF;
CASE c_dec_tens
WHEN '2' THEN p_dec_tens:=v_twenty;
WHEN '3' THEN p_dec_tens:=v_thirty;
WHEN '4' THEN p_dec_tens:=v_forty;
WHEN '5' THEN p_dec_tens:=v_fifty;
WHEN '6' THEN p_dec_tens:=v_sixty;
WHEN '7' THEN p_dec_tens:=v_seventy;
WHEN '8' THEN p_dec_tens:=v_eighty;
WHEN '9' THEN p_dec_tens:=v_ninety;
ELSE p_dec_tens:=null;
END CASE c_dec_tens;
IF c_units < '20' THEN
CASE c_units
WHEN '00' THEN p_units:=null;
WHEN '01' THEN p_units:=v_one;
WHEN '02' THEN p_units:=v_two;
WHEN '03' THEN p_units:=v_three;
WHEN '04' THEN p_units:=v_four;
WHEN '05' THEN p_units:=v_five;
WHEN '06' THEN p_units:=v_six;
WHEN '07' THEN p_units:=v_seven;
WHEN '08' THEN p_units:=v_eight;
WHEN '09' THEN p_units:=v_nine;
WHEN '10' THEN p_units:=v_ten;
WHEN '11' THEN p_units:=v_eleven;
WHEN '12' THEN p_units:=v_twelve;
WHEN '13' THEN p_units:=v_thirteen;
WHEN '14' THEN p_units:=v_fourteen;
WHEN '15' THEN p_units:=v_fifteen;
WHEN '16' THEN p_units:=v_sixteen;
WHEN '17' THEN p_units:=v_seventeen;
WHEN '18' THEN p_units:=v_eighteen;
WHEN '19' THEN p_units:=v_nineteen;
ELSE p_units:=null;
END CASE c_units;
ELSE
CASE c_unit
WHEN '1' THEN p_unit:=v_one;
WHEN '2' THEN p_unit:=v_two;
WHEN '3' THEN p_unit:=v_three;
WHEN '4' THEN p_unit:=v_four;
WHEN '5' THEN p_unit:=v_five;
WHEN '6' THEN p_unit:=v_six;
WHEN '7' THEN p_unit:=v_seven;
WHEN '8' THEN p_unit:=v_eight;
WHEN '9' THEN p_unit:=v_nine;
ELSE p_unit:=null;
END CASE c_unit;
END IF;
CASE c_tens
WHEN '2' THEN p_tens:=v_twenty;
WHEN '3' THEN p_tens:=v_thirty;
WHEN '4' THEN p_tens:=v_forty;
WHEN '5' THEN p_tens:=v_fifty;
WHEN '6' THEN p_tens:=v_sixty;
WHEN '7' THEN p_tens:=v_seventy;
WHEN '8' THEN p_tens:=v_eighty;
WHEN '9' THEN p_tens:=v_ninety;
ELSE p_tens:=null;
END CASE c_tens;
CASE c_hundreds
WHEN '1' THEN p_hundreds:=v_one||v_hundred;
WHEN '2' THEN p_hundreds:=v_two||v_hundred;
WHEN '3' THEN p_hundreds:=v_three||v_hundred;
WHEN '4' THEN p_hundreds:=v_four||v_hundred;
WHEN '5' THEN p_hundreds:=v_five||v_hundred;
WHEN '6' THEN p_hundreds:=v_six||v_hundred;
WHEN '7' THEN p_hundreds:=v_seven||v_hundred;
WHEN '8' THEN p_hundreds:=v_eight||v_hundred;
WHEN '9' THEN p_hundreds:=v_nine||v_hundred;
ELSE p_hundreds:=null;
END CASE c_hundreds;
IF c_thousands_units < '20' THEN
CASE c_thousands_units
WHEN '01' THEN p_thousands_units:=v_one||v_thousand;
WHEN '02' THEN p_thousands_units:=v_two||v_thousand;
WHEN '03' THEN p_thousands_units:=v_three||v_thousand;
WHEN '04' THEN p_thousands_units:=v_four||v_thousand;
WHEN '05' THEN p_thousands_units:=v_five||v_thousand;
WHEN '06' THEN p_thousands_units:=v_six||v_thousand;
WHEN '07' THEN p_thousands_units:=v_seven||v_thousand;
WHEN '08' THEN p_thousands_units:=v_eight||v_thousand;
WHEN '09' THEN p_thousands_units:=v_nine||v_thousand;
WHEN '10' THEN p_thousands_units:=v_ten||v_thousand;
WHEN '11' THEN p_thousands_units:=v_eleven||v_thousand;
WHEN '12' THEN p_thousands_units:=v_twelve||v_thousand;
WHEN '13' THEN p_thousands_units:=v_thirteen||v_thousand;
WHEN '14' THEN p_thousands_units:=v_fourteen||v_thousand;
WHEN '15' THEN p_thousands_units:=v_fifteen||v_thousand;
WHEN '16' THEN p_thousands_units:=v_sixteen||v_thousand;
WHEN '17' THEN p_thousands_units:=v_seventeen||v_thousand;
WHEN '18' THEN p_thousands_units:=v_eighteen||v_thousand;
WHEN '19' THEN p_thousands_units:=v_nineteen||v_thousand;
ELSE p_thousands_units:=null;
END CASE c_thousands_units;
ELSE
CASE c_thousands_unit
WHEN '1' THEN p_thousands_unit:=v_one||v_thousand;
WHEN '2' THEN p_thousands_unit:=v_two||v_thousand;
WHEN '3' THEN p_thousands_unit:=v_three||v_thousand;
WHEN '4' THEN p_thousands_unit:=v_four||v_thousand;
WHEN '5' THEN p_thousands_unit:=v_five||v_thousand;
WHEN '6' THEN p_thousands_unit:=v_six||v_thousand;
WHEN '7' THEN p_thousands_unit:=v_seven||v_thousand;
WHEN '8' THEN p_thousands_unit:=v_eight||v_thousand;
WHEN '9' THEN p_thousands_unit:=v_nine||v_thousand;
ELSE p_thousands_unit:=null;
END CASE c_thousands_unit;
END IF;
CASE c_thousands_tens
WHEN '2' THEN p_thousands_tens:=v_twenty;
WHEN '3' THEN p_thousands_tens:=v_thirty;
WHEN '4' THEN p_thousands_tens:=v_forty;
WHEN '5' THEN p_thousands_tens:=v_fifty;
WHEN '6' THEN p_thousands_tens:=v_sixty;
WHEN '7' THEN p_thousands_tens:=v_seventy;
WHEN '8' THEN p_thousands_tens:=v_eighty;
WHEN '9' THEN p_thousands_tens:=v_ninety;
ELSE p_thousands_tens:=null;
END CASE c_thousands_tens;
CASE c_thousands_hundreds
WHEN '0' THEN p_thousands_hundreds:=null;
WHEN '1' THEN p_thousands_hundreds:=v_one||v_hundred;
WHEN '2' THEN p_thousands_hundreds:=v_two||v_hundred;
WHEN '3' THEN p_thousands_hundreds:=v_three||v_hundred;
WHEN '4' THEN p_thousands_hundreds:=v_four||v_hundred;
WHEN '5' THEN p_thousands_hundreds:=v_five||v_hundred;
WHEN '6' THEN p_thousands_hundreds:=v_six||v_hundred;
WHEN '7' THEN p_thousands_hundreds:=v_seven||v_hundred;
WHEN '8' THEN p_thousands_hundreds:=v_eight||v_hundred;
WHEN '9' THEN p_thousands_hundreds:=v_nine||v_hundred;
ELSE p_thousands_hundreds:=null;
END CASE c_thousands_hundreds;
IF c_millions_units < '20' THEN
CASE c_millions_units
WHEN '01' THEN p_millions_units:=v_one||v_million;
WHEN '02' THEN p_millions_units:=v_two||v_million;
WHEN '03' THEN p_millions_units:=v_three||v_million;
WHEN '04' THEN p_millions_units:=v_four||v_million;
WHEN '05' THEN p_millions_units:=v_five||v_million;
WHEN '06' THEN p_millions_units:=v_six||v_million;
WHEN '07' THEN p_millions_units:=v_seven||v_million;
WHEN '08' THEN p_millions_units:=v_eight||v_million;
WHEN '09' THEN p_millions_units:=v_nine||v_million;
WHEN '10' THEN p_millions_units:=v_ten||v_million;
WHEN '11' THEN p_millions_units:=v_eleven||v_million;
WHEN '12' THEN p_millions_units:=v_twelve||v_million;
WHEN '13' THEN p_millions_units:=v_thirteen||v_million;
WHEN '14' THEN p_millions_units:=v_fourteen||v_million;
WHEN '15' THEN p_millions_units:=v_fifteen||v_million;
WHEN '16' THEN p_millions_units:=v_sixteen||v_million;
WHEN '17' THEN p_millions_units:=v_seventeen||v_million;
WHEN '18' THEN p_millions_units:=v_eighteen||v_million;
WHEN '19' THEN p_millions_units:=v_nineteen||v_million;
ELSE p_millions_units:=null;
END CASE c_millions_units;
ELSE
CASE c_millions_unit
WHEN '1' THEN p_millions_unit:=v_one||v_million;
WHEN '2' THEN p_millions_unit:=v_two||v_million;
WHEN '3' THEN p_millions_unit:=v_three||v_million;
WHEN '4' THEN p_millions_unit:=v_four||v_million;
WHEN '5' THEN p_millions_unit:=v_five||v_million;
WHEN '6' THEN p_millions_unit:=v_six||v_million;
WHEN '7' THEN p_millions_unit:=v_seven||v_million;
WHEN '8' THEN p_millions_unit:=v_eight||v_million;
WHEN '9' THEN p_millions_unit:=v_nine||v_million;
ELSE p_millions_unit:=null;
END CASE c_millions_unit;
END IF;
CASE c_millions_tens
WHEN '2' THEN p_millions_tens:=v_twenty;
WHEN '3' THEN p_millions_tens:=v_thirty;
WHEN '4' THEN p_millions_tens:=v_forty;
WHEN '5' THEN p_millions_tens:=v_fifty;
WHEN '6' THEN p_millions_tens:=v_sixty;
WHEN '7' THEN p_millions_tens:=v_seventy;
WHEN '8' THEN p_millions_tens:=v_eighty;
WHEN '9' THEN p_millions_tens:=v_ninety;
ELSE p_millions_tens:=null;
END CASE c_millions_tens;
CASE c_millions_hundreds
WHEN '0' THEN p_millions_hundreds:=null;
WHEN '1' THEN p_millions_hundreds:=v_one||v_hundred;
WHEN '2' THEN p_millions_hundreds:=v_two||v_hundred;
WHEN '3' THEN p_millions_hundreds:=v_three||v_hundred;
WHEN '4' THEN p_millions_hundreds:=v_four||v_hundred;
WHEN '5' THEN p_millions_hundreds:=v_five||v_hundred;
WHEN '6' THEN p_millions_hundreds:=v_six||v_hundred;
WHEN '7' THEN p_millions_hundreds:=v_seven||v_hundred;
WHEN '8' THEN p_millions_hundreds:=v_eight||v_hundred;
WHEN '9' THEN p_millions_hundreds:=v_nine||v_hundred;
ELSE p_millions_hundreds:=null;
END CASE c_millions_hundreds;
IF c_billions_units < '20' THEN
CASE c_billions_units
WHEN '01' THEN p_billions_units:=v_one||v_billion;
WHEN '02' THEN p_billions_units:=v_two||v_billion;
WHEN '03' THEN p_billions_units:=v_three||v_billion;
WHEN '04' THEN p_billions_units:=v_four||v_billion;
WHEN '05' THEN p_billions_units:=v_five||v_billion;
WHEN '06' THEN p_billions_units:=v_six||v_billion;
WHEN '07' THEN p_billions_units:=v_seven||v_billion;
WHEN '08' THEN p_billions_units:=v_eight||v_billion;
WHEN '09' THEN p_billions_units:=v_nine||v_billion;
WHEN '10' THEN p_billions_units:=v_ten||v_billion;
WHEN '11' THEN p_billions_units:=v_eleven||v_billion;
WHEN '12' THEN p_billions_units:=v_twelve||v_billion;
WHEN '13' THEN p_billions_units:=v_thirteen||v_billion;
WHEN '14' THEN p_billions_units:=v_fourteen||v_billion;
WHEN '15' THEN p_billions_units:=v_fifteen||v_billion;
WHEN '16' THEN p_billions_units:=v_sixteen||v_billion;
WHEN '17' THEN p_billions_units:=v_seventeen||v_billion;
WHEN '18' THEN p_billions_units:=v_eighteen||v_billion;
WHEN '19' THEN p_billions_units:=v_nineteen||v_billion;
ELSE p_billions_units:=null;
END CASE c_billions_units;
ELSE
CASE c_billions_unit
WHEN '0' THEN p_billions_unit:=v_billion;
WHEN '1' THEN p_billions_unit:=v_one||v_billion;
WHEN '2' THEN p_billions_unit:=v_two||v_billion;
WHEN '3' THEN p_billions_unit:=v_three||v_billion;
WHEN '4' THEN p_billions_unit:=v_four||v_billion;
WHEN '5' THEN p_billions_unit:=v_five||v_billion;
WHEN '6' THEN p_billions_unit:=v_six||v_billion;
WHEN '7' THEN p_billions_unit:=v_seven||v_billion;
WHEN '8' THEN p_billions_unit:=v_eight||v_billion;
WHEN '9' THEN p_billions_unit:=v_nine||v_billion;
ELSE p_billions_unit:=null;
END CASE c_billions_unit;
END IF;
CASE c_billions_tens
WHEN '2' THEN p_billions_tens:=v_twenty;
WHEN '3' THEN p_billions_tens:=v_thirty;
WHEN '4' THEN p_billions_tens:=v_forty;
WHEN '5' THEN p_billions_tens:=v_fifty;
WHEN '6' THEN p_billions_tens:=v_sixty;
WHEN '7' THEN p_billions_tens:=v_seventy;
WHEN '8' THEN p_billions_tens:=v_eighty;
WHEN '9' THEN p_billions_tens:=v_ninety;
ELSE p_billions_tens:=null;
END CASE c_billions_tens;
CASE c_billions_hundreds
WHEN '0' THEN p_billions_hundreds:=null;
WHEN '1' THEN p_billions_hundreds:=v_one||v_hundred;
WHEN '2' THEN p_billions_hundreds:=v_two||v_hundred;
WHEN '3' THEN p_billions_hundreds:=v_three||v_hundred;
WHEN '4' THEN p_billions_hundreds:=v_four||v_hundred;
WHEN '5' THEN p_billions_hundreds:=v_five||v_hundred;
WHEN '6' THEN p_billions_hundreds:=v_six||v_hundred;
WHEN '7' THEN p_billions_hundreds:=v_seven||v_hundred;
WHEN '8' THEN p_billions_hundreds:=v_eight||v_hundred;
WHEN '9' THEN p_billions_hundreds:=v_nine||v_hundred;
ELSE p_billions_hundreds:=null;
END CASE c_billions_hundreds;
IF c_trillions_units < '20' THEN
CASE c_trillions_units
WHEN '01' THEN p_trillions_units:=v_one||v_trillion;
WHEN '02' THEN p_trillions_units:=v_two||v_trillion;
WHEN '03' THEN p_trillions_units:=v_three||v_trillion;
WHEN '04' THEN p_trillions_units:=v_four||v_trillion;
WHEN '05' THEN p_trillions_units:=v_five||v_trillion;
WHEN '06' THEN p_trillions_units:=v_six||v_trillion;
WHEN '07' THEN p_trillions_units:=v_seven||v_trillion;
WHEN '08' THEN p_trillions_units:=v_eight||v_trillion;
WHEN '09' THEN p_trillions_units:=v_nine||v_trillion;
WHEN '10' THEN p_trillions_units:=v_ten||v_trillion;
WHEN '11' THEN p_trillions_units:=v_eleven||v_trillion;
WHEN '12' THEN p_trillions_units:=v_twelve||v_trillion;
WHEN '13' THEN p_trillions_units:=v_thirteen||v_trillion;
WHEN '14' THEN p_trillions_units:=v_fourteen||v_trillion;
WHEN '15' THEN p_trillions_units:=v_fifteen||v_trillion;
WHEN '16' THEN p_trillions_units:=v_sixteen||v_trillion;
WHEN '17' THEN p_trillions_units:=v_seventeen||v_trillion;
WHEN '18' THEN p_trillions_units:=v_eighteen||v_trillion;
WHEN '19' THEN p_trillions_units:=v_nineteen||v_trillion;
ELSE p_trillions_units:=null;
END CASE c_trillions_units;
ELSE
CASE c_trillions_unit
WHEN '0' THEN p_trillions_unit:=v_trillion;
WHEN '1' THEN p_trillions_unit:=v_one||v_trillion;
WHEN '2' THEN p_trillions_unit:=v_two||v_trillion;
WHEN '3' THEN p_trillions_unit:=v_three||v_trillion;
WHEN '4' THEN p_trillions_unit:=v_four||v_trillion;
WHEN '5' THEN p_trillions_unit:=v_five||v_trillion;
WHEN '6' THEN p_trillions_unit:=v_six||v_trillion;
WHEN '7' THEN p_trillions_unit:=v_seven||v_trillion;
WHEN '8' THEN p_trillions_unit:=v_eight||v_trillion;
WHEN '9' THEN p_trillions_unit:=v_nine||v_trillion;
ELSE p_trillions_unit:=null;
END CASE c_trillions_unit;
END IF;
CASE c_trillions_tens
WHEN '2' THEN p_trillions_tens:=v_twenty;
WHEN '3' THEN p_trillions_tens:=v_thirty;
WHEN '4' THEN p_trillions_tens:=v_forty;
WHEN '5' THEN p_trillions_tens:=v_fifty;
WHEN '6' THEN p_trillions_tens:=v_sixty;
WHEN '7' THEN p_trillions_tens:=v_seventy;
WHEN '8' THEN p_trillions_tens:=v_eighty;
WHEN '9' THEN p_trillions_tens:=v_ninety;
ELSE p_trillions_tens:=null;
END CASE c_trillions_tens;
CASE c_trillions_hundreds
WHEN '0' THEN p_trillions_hundreds:=null;
WHEN '1' THEN p_trillions_hundreds:=v_one||v_hundred;
WHEN '2' THEN p_trillions_hundreds:=v_two||v_hundred;
WHEN '3' THEN p_trillions_hundreds:=v_three||v_hundred;
WHEN '4' THEN p_trillions_hundreds:=v_four||v_hundred;
WHEN '5' THEN p_trillions_hundreds:=v_five||v_hundred;
WHEN '6' THEN p_trillions_hundreds:=v_six||v_hundred;
WHEN '7' THEN p_trillions_hundreds:=v_seven||v_hundred;
WHEN '8' THEN p_trillions_hundreds:=v_eight||v_hundred;
WHEN '9' THEN p_trillions_hundreds:=v_nine||v_hundred;
ELSE p_trillions_hundreds:=null;
END CASE c_trillions_hundreds;
IF (c_thousands_unit='0' AND c_thousands_units='00' AND c_thousands_tens='0'
AND c_thousands_hundreds !='0') THEN
p_thousands_unit:=' Thousand';
END IF;
IF (c_millions_unit='0' AND c_millions_units='00' AND c_millions_tens='0'
AND c_millions_hundreds !='0') THEN
p_millions_unit:=' Million';
END IF;
IF (c_billions_unit='0' AND c_billions_units='00' AND c_billions_tens='0'
AND c_billions_hundreds !='0') THEN
p_billions_unit:=' Billion';
END IF;
IF (c_trillions_unit='0' AND c_trillions_units='00' AND c_trillions_tens='0'
AND c_trillions_hundreds !='0') THEN
p_trillions_unit:=' Trillion';
END IF;
IF v_dec_char = '0' THEN
p_dec_unit:=' zero';
END IF;
p_txt:='Dollars<'||p_trillions_hundreds||p_trillions_tens||p_trillions_units
||p_trillions_unit||p_billions_hundreds||p_billions_tens
||p_billions_units||p_billions_unit||p_millions_hundreds
||p_millions_tens||p_millions_units||p_millions_unit
||p_thousands_hundreds||p_thousands_tens||p_thousands_units
||p_thousands_unit||p_hundreds||p_tens||p_units||p_unit||'>'
||' AND <'||p_dec_tens||p_dec_units||p_dec_unit||'> Cents';
END;
/
Pipeliner Rich Jesse points out that this code, while valid, is a bit verbose and notes that there
are alternative solutions available:
"A search for "spell a
number" on http://asktom.oracle.com shows a simpler method.
A big
benefit being that the latter will be much easier to maintain for
locale, since it's using Oracle's date functions to get the majority of
the spelling."
December's Tip of the Month
Name Those Exceptions!
Compliments of Steven Feuerstein, from the October issue of OPP/News
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20512
When a SELECT INTO does not return at least one row, Oracle raises the
NO_DATA_FOUND exception. Most PL/SQL developers probably think this exception is
some sort of built-in, reserved word exception in the PL/SQL language. It is
not. rather, it is simply one of a relatively small number of exceptions that
are defined in the STANDARD package. This package is one of two default packages in
PL/SQL (the other being
DBMS_STANDARD), which means that when you reference elements in either of these
packages, you don't have to qualify them with the name of their package (though
you can if you want to). Consider the following block of code:
DECLARE l_counter PLS_INTEGER; l_counter2 STANDARD.PLS_INTEGER; VARCHAR2 NUMBER; BEGIN SELECT 1 INTO l_counter FROM SYS.DUAL WHERE 1 = 2;
DBMS_STANDARD.COMMIT; EXCEPTION WHEN STANDARD.NO_DATA_FOUND THEN DBMS_STANDARD.raise_application_error (-20000, 'Yikes!'); END;>
This code will compile, which might come as a surprise to you. Conclusions to
draw from this example:
-
Many of the elements you consider to be reserved words in PL/SQL are simply
datatypes, structures or programs defined in the STANDARD package. You can use
those names in your own code (notice my declaration of a variable named
"varchar2"), but I certainly suggest that you not do that. The resulting code is just too,
too confusing.
-
You can qualify references to elements in the default packages with the
package name, but again I would recommend you not do this, to avoid extra
clutter in your code.
Now, I strongly encourage you to take some time to study the STANDARD package
(found in the Rdbms/Admin subdirectory under your Oracle Home directory, now
defined in two separate files: stdspec.sql and stdbody.sql). You can learn an
awful lot about the PL/SQL language. For one thing, you will see that all of the
system exceptions like NO_DATA_FOUND are created using the EXCEPTION_INIT
pragma. Here is the statement for NO_DATA_FOUND:
NO_DATA_FOUND EXCEPTION; PRAGMA EXCEPTION_INIT (NO_DATA_FOUND, 100);
And here you thought Oracle errors were negative! Well, NO_DATA_FOUND is an
oddity in the Oracle world: it has two
error numbers associated with it, 100 (carried over from the earliest days of
compliance with the ANSI standard) and -1403. The EXCEPTION_INIT pragma
references 100, but -1403 comes along for the ride. [Actually, for whatever
reason, Oracle will not allow you to use -1403 in an EXCEPTION_INIT pragma!]
Thus, you could replace this exception section:
EXCEPTION WHEN NO_DATA_FOUND THEN ...
with this one:
EXCEPTION WHEN OTHERS THEN IF SQLCODE = -1403 THEN ...
but you certainly wouldn't want to do
this. The code is much less readable.
Well, that same rule (namely, it is better to reference exceptions by name,
rather than number) applies to Oracle exceptions for which there are no
associated names. In this case, you will need to declare your own named exception and use the EXCEPTION_INIT
pragma yourself. Let's take a look at an example.
Hopefully you are all aware of the FORALL statement, which allows you to
perform multiple DML statements much
more efficiently than through row-by-row processing. If FORALL is news to you,
then please go out and buy my book, Oracle PL/SQL Programming,
and dive into chapter 14. Or visit Oracle's on-line doc center at:
PL/SQL
User Guide and Reference: FORALL
Now, in Oracle9i Database Release 2, Oracle added the SAVE EXCEPTIONS clause
to FORALL. If you use this clause, then Oracle will execute each DML statement
associated with the specified rows of the driving collection. If it encounters
one or more errors, it saves those errors in a pseudo collection named
SQL%BULK_EXCEPTIONS. When it has completed all the DML processing and at least
one error has occurred, Oracle will then raise the generic "FORALL failure"
error, which has been assigned the number -24381.
So you could write code like this
when working with FORALL and SAVE EXCEPTIONS:
EXCEPTION WHEN OTHERS THEN IF SQLCODE = -24381 THEN FOR indx IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP ... process the errors ... END LOOP; END IF; END;
This would, however, be a bad idea. Do you think you will remember what
-24381 refers to? I am pretty certain I will, but that is because I am an
obsessive individual when it comes to PL/SQL. You surely have better things to
do with your mind. Putting aside the
issue of remembering, code like "SQLCODE
= -24381" is also downright bewildering and intimidating. Developers coming
across this code in later years will feel terribly inadequate because the code
clearly expects them to know what that
means, but they have no idea. So this code creates bad vibes, thereby reducing
the quality of life of programmers, leading to more bugs, and possibly the
collapse of civilization as we know it.
See? Programmers can be super-heroes and save our world, too! Just don't hard
code error numbers in the exception seciton. Well, yes, it would certainly be
nice to reference a named exception in that part of this program, but Oracle did
not choose to associate a name with -24381. That's all right, though. Don't give
up. Just because Oracle didn't see fit to do this doesn't mean that you have to
write nasty code. Instead, you can give your own name to this exception. Consider the
following procedure (truncated implementation shown to allow us to focus on the
key elements for the tip at hand):
PROCEDURE upd_for_dept ( dept_in IN employee.department_id%TYPE , newsal IN employee.salary%TYPE ) IS bulk_errors EXCEPTION; PRAGMA EXCEPTION_INIT (bulk_errors, -24381); ... BEGIN ... FORALL indx IN employees.FIRST .. employees.LAST SAVE EXCEPTIONS INSERT INTO employee_history (employee_id, salary, hire_date ) VALUES (employees (indx), salaries (indx), hire_dates (indx) ); EXCEPTION WHEN bulk_errors THEN FOR indx IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP log_error (SQL%BULK_EXCEPTIONS (indx).ERROR_CODE); END LOOP; END upd_for_dept;
Notice that at the very beginning of my procedure I declare an exception and
associate it with -24381. Then in my exception section, I reference the
exception by name. The result is much more readable code, which will make
programmers feel better about themselves and also preserve our quality of
life.
Bottom line: give names to any Oracle errors which you need to trap and
handle in your code. Generally avoid placing obscure, scary, hard-coded numbers
in any part of your application.
For questions
or comments on this site: webmaster@quest-pipelines.com
All content Copyright
© Quest Software, Inc. All rights reserved.
|