Oracle Database Tips
Oracle Database Tips
if ratio > 95 you may decrease the buffer size in case required for other purpose
if hit_ratio between 70 and 94 then its OK
if hit_ratio 60 to 69 then one has to add more buffers
==================
suppose it is less than 70, then question arises as to how many buffers to add. In order to determ
only on SYS login and DB_BLOCK_LRU_EXTENDED_STATISTICS set to non-zero value in
Description:
Select 100*TRUNC(indx/100) + 1 || '-' || 100*(TRUNC(indx/100)+1 range,
SUM(count) additional_hits
FROM x$KCBRBH
GROUP BY TRUNC(indx/100);
in this case if you notice increasing the buffer size by 200 is sufficient, further increases will no
decrease, then perform above operation with X$KCBCDH view. For this, the init.ora prameter t
to true
Summary: How to find v
Ever spend so
values you ca
easily find th
Description: table. This tab
"TERRITOR
considered va
(Also shows
Summary: NLS_LANG parameter c
Description: While running an export o
character set of WE8ISO8
a warning message saying
possible character conver
NLS_LANG parm to Eng
the export worked proper
mind when running expor
something other than the
you could find that specia
mysteriously disappear fr
running the export will ne
NLS_LANG=English_Ca
NLS_LANG
Summary: "Query is executing..."?
Description: "Query is executing..." isn't
it? Who knows? This does, if
your query uses rollback
segments (updates, inserts,
etc.) Using the statement
below, you can find out if
your UPDATE (or whatever)
is proceeding or rolling back.
Migrating
Summary: using
ODMA
Description: I'm
migrating
more than
242 oracle
7.3.4
databases to
Oracle 8.1.6
Using
ODMA
(Oracle Data
Migration
Asistant)
could find a
big error in
the Oracle
Note
76460.1
from
Metalink
(Checklist
for
Migrating
from
Oracle7 to
Oracle8.1 on
UNIX).
Step 13,
when you
have to
CONVERT
the database,
does not
work as
indicated,
so, what you
have to do
is:
1) startup
your db
2) backup
your control
file to trace
3) shutdown
your db
4) startup
nomount
5) recreate
your control
files using
script
generated in
step 2).
6) alter
database
open
7) continue
with Oracle
Checklist.
Believe me,
it works fine
!!! Hope this
could be
useful.
Email me if
any problem
comes up.
Summary: Migrating using ODMA
Description: I'm migrating more than 242 oracle 7.3.4
databases to Oracle 8.1.6
Using ODMA (Oracle Data Migration Asistant)
could find a big error in the Oracle Note 76460.1
from Metalink (Checklist for Migrating from
Oracle7 to Oracle8.1 on UNIX).
Step 13, when you have to CONVERT the
database, does not work as indicated, so, what you
have to do is:
1) startup your db
2) backup your control file to trace
3) shutdown your db
4) startup nomount
5) recreate your control files using script
generated in step 2).
6) alter database open
7) continue with Oracle Checklist.
Believe me, it works fine !!! Hope this could be
useful. Email me if any problem comes up.
Summary: Running SQLPLUS in NT scripts
Be aware, you can't setup an environment
variable SQLPLUS in an NT script. It will fail.
Oracle 8.1.6 has changed from plus80.exe to
sqlplus.exe. If you have an environment variable
set, i.e.
Description:
SQLPLUS=d:\oracle\ora81\bin\sqlplus.exe, the
batch job will fail.
Change the environment variable to anything
else other than SQLPLUS. i.e. set
SPLUS=d:\oracle\ora81\bin\sqlplus.exe
Summary: How to cleanup unused temporary segments
To cleanup or remove unused temporary
segments you can use the following SQL
statement on the temporary tablespace:
load data
infile ""
append
into table
fields terminated by ' ' (
)
That's all.
Some clarification.
Better use clause "...fields terminated by
X'09'..." in order to specify the tab character
explicitly.
You can also try saving the file from Excel in
text file CSV-format or TAB-delimited and
load with the SQL*Loader (Oracle Utilities) or
Borland DataPump (from Delphi or C++
distribut).
Tip #15: SQL scripts that compare schemas in two different instances for
differences. (Type: SQL)
So you have your application installed in three different instances (Development, Test,
Production). Or maybe that is six (conversion, demo, training), or eight? Well, how do
you keep all of that in sync? No matter how good your migration procedures, differences
between the instances somehow seem to always creep in. (How is it that something gets
into production, that has never been in development?)
This tip is a couple of SQL scripts that will use database links and the SQL MINUS
operator to compare the objects and table definitions in the same schema in two different
instances.
First, an example of using the two scripts will be shown, then the actual scripts
themselves. They can also be downloaded below.
The first script lists the objects that are not in both of the selected instances:
DB_LINK
------------------------------------
TESTLINK.WORLD
SQL> @objdiff
Object Owner: SHARED
First instance DB Link (Include @):
Second instance DB Link (Include @):@TESTLINK
Page: 1
OWNER: SHARED
Objects in devl but not demo
Object Name Object Type Status
----------------------------------- ---------------
----------
TSU_SELECT_CAD_FN FUNCTION VALID
TSU_SELECT_FN FUNCTION VALID
FMU_PLS INDEX VALID
FMU_PRECIP INDEX VALID
SYSPIPE PACKAGE VALID
SYSPIPE PACKAGE BODY VALID
ABLE_FK_CONSTRAINTS PROCEDURE INVALID
WTRSHD_SEQ SEQUENCE VALID
ADMIN_CODE TABLE VALID
FMPT_FORM_HELP TABLE VALID
FMU_COUNTY_V VIEW VALID
FMU_TRUST_V VIEW VALID
12 rows selected.
Page: 1
OWNER: SHARED
Objects in demo but not devl
7 rows selected.
Notes: If no database link is entered, then the script uses the CURRENT instance. Also,
the title uses the instance name in each instances v$parameter table (Objects in
{instance_1_name} but not in {instance_2_name}.
The second script compares the actual table definitions in two instances:
SQL> @tabdiff
Table Owner: SHARED
First instance DB Link (Include @):
Second instance DB Link (Include @):@TESTLINK
Page: 1
OWNER: SHARED
Differences between
devl and demo
Instance Table Column
DataType Len Pr Null?
-------- -------------------------
------------------------- -------- ---- ---- -----
demo FMU FMU_RESTR_BEG_DT_BAD
DATE 7 =0 Y
demo FMU FMU_RESTR_END_DT_BAD
DATE 7 =0 Y
2 rows selected.
And now listings of the actual scripts:
/**********************************************************
***************/
/* objdiff.sql - Lists the objects in a schema that are not
in both of */
/* two instances. Uses database links and
the SQL MINUS */
/* operator to make the comparison.
*/
/*
*/
/* Author: Ken Atkins ([email protected])
*/
/* http://www.arrowsent.com/oratip
*/
/*
*/
/* Written: 5/11/95
*/
/*
*/
/* You need to have a database link setup for any instance
that you want */
/* to make a comparison for.
*/
/*
*/
/* Please feel free to use and modify this script as long
it is not sold */
/* or included in any software without the prior permission
of the author*/
/* If you do make some good improvements, please send them
to me, and I */
/* can incorporate them in a future version and make them
available to */
/* others (giving you credit of course!).
*/
/*
*/
/**********************************************************
***************/
set pagesize 60
set linesize 80
set verify off
set feedback off
set pause off;
--define obj_owner = '&1'
--define inst_1 = '&2'
--define inst_2 = '&3'
accept obj_owner prompt 'Object Owner: '
accept inst_1 prompt 'First instance DB Link (Include @):'
accept inst_2 prompt 'Second instance DB Link (Include @):'
clear breaks
ttitle off
set heading off
set null=0
clear breaks
ttitle off
set heading off
Tip #12: SQL Script to show 'hit ratio' of currently running processes.
(Type: SQL)
Have you ever wondered why your server was running so slow? Who else is running
queries and why are they bogging the system down?? So you go round up a DBA and ask
them to monitor the database using one of those shnazzy DBA type tools. But DBAs are
not always had for the asking, and you do not have access to the tools, so what do you
do? This tip is a couple of simple SQL scripts which will show which Oracle processes
are currently running in an instance, and what the buffer hit ratio is for those processes
(low hit ratios are an indication of poorly tuned SQL, which can slow the WHOLE
instance down).
The first script shows the active processes and their current hit ratio.
/**********************************************************
***************/
/* listproc.sql - Lists currently running processes and
their hit ratios */
/*
*/
/* Author: Ken Atkins ([email protected])
*/
/* http://www.arrowsent.com/oratip
*/
/*
*/
/* You need select access to V$SESSION, V$PROCESS, and
V$SESS_IO */
/* to run this script.
*/
/*
*/
/* The columns returned by this script are:
*/
/ Oracle ID (schemaname) = The oracle 'schema' or
'user' that is */
/* running the SQL statement.
*/
/* System ID (username) = The system id that the
process is */
/* running under. Will be
the unix userid */
/* if Oracle running on unix.
*/
/* Program = The name of the program that is
running the SQL.*/
/* Physical Reads = The number of physical block
reads. */
/* Hit Ratio = The ratio of buffer to physical
block reads. */
/* be an indication of the
efficiency of the query*/
/* running. Anything under 90% is
bad. Very low */
/* hit ratios (< 10-20%) in a
process can slow */
/* down the whole system.
*/
/**********************************************************
***************/
column schemaname format a10 heading 'Oracle ID'
column username format a10 heading 'System ID'
column program format a32 heading 'Program'
column hit_ratio format 9.90 heading 'Hit Ratio'
column physical_reads format 9999999 heading 'Reads'
column sid format 99999
SELECT s.schemaname
, p.username
, s.program
,io.physical_reads
,(io.block_gets+io.consistent_gets)/
(io.block_gets+io.consistent_gets+io.physical_reads
) hit_ratio
FROM V$Session s
,V$Process p
,V$Sess_io io
WHERE s.paddr = p.addr
AND s.sid = io.sid
-- Only look at active processes
AND s.status = 'ACTIVE'
-- Need this predicate to prevent division by 0
AND (io.block_gets+io.consistent_gets+io.physical_reads)
> 0
/
An example of using the script:
SQL> @hitratio
The next script is a simpler version that just shows all of processes and their status, sid
and serial#. The sid and serial# are used in the ALTER SYSTEM KILL SESSION
command to kill oracle processes that are 'stuck'.
/**********************************************************
***************/
/* listproc.sql - Lists currently processes, status, sid &
serial# */
/*
*/
/* Author: Ken Atkins ([email protected])
*/
/* http://www.arrowsent.com/oratip
*/
/*
*/
/* You need select access to V$SESSION, V$PROCESS to run
this script */
/*
*/
/**********************************************************
***************/
column schemaname format a10 heading 'Oracle ID'
column username format a10 heading 'System ID'
column program format a30 heading 'Program'
column user_name format a15 heading 'User Name'
column sid format 99999
SELECT s.schemaname
,p.username
,s.program
,s.sid
,s.serial#
,s.status
FROM V$Session s
,V$Process p
where s.paddr = p.addr
/
An example of running the script:
SQL> @listproc
So you have to reload the data in a table that is maintained in another system. But there
are these pesky Foreign Keys defined TO this table from other tables in your database.
Oh Well, Select the names of the FKs from the constraints table, enter the commands to
disable them, now load the data. What? You missed one? Disable it, reload. Now enable
all of the constraints again. Kind of tedious. This tip details a stored procedure that can
automatically disable or enable all of the FK constraints *TO* a specified table.
The following procedure uses the following steps to enable or disable all of the FK
constraints *TO* a specified table:
PROMPT
PROMPT Creating Procedure able_fk_constraints
CREATE OR REPLACE PROCEDURE able_fk_constraints(
pTable IN VARCHAR2 ,
pAble IN VARCHAR2 )
IS
vPKName VARCHAR2(80);
set pagesize 30
set linesize 40
set feedback off
ttitle CENTER 'Test Employee Report' skip 2
break on dname skip 1
spool tstrep.lst
SELECT d.dname, e.empno, e.ename
FROM Dept d, Emp e
WHERE d.deptno = e.deptno
ORDER BY d.dname, e.ename
/
spool off
As you can see, you have the unwanted message at the bottom of the report.
This problem is caused by having anything OTHER than a blank line at the bottom of
your SQL*Plus script! The last line of the script *must* be a blank line, that is a line
with a carriage return and NOTHING ELSE. For example:
set pagesize 30
set linesize 40
set feedback off
ttitle CENTER 'Test Employee Report' skip 2
break on dname skip 1
spool tstrep.lst
SELECT d.dname, e.empno, e.ename
FROM Dept d, Emp e
WHERE d.deptno = e.deptno
ORDER BY d.dname, e.ename
/
spool off
Blank Line!
I will use the infamous EMP/DEPT tables to illustrate this technique. Using these tables,
you might use the following SQL for a standard hierarchical query:
SQL>
1 SELECT level, LPAD(' ',2*level-2)||emp.ename ename,
emp.empno, emp.mgr, emp.deptno
2 FROM Emp
3 CONNECT BY PRIOR emp.empno = emp.mgr
4* START WITH emp.empno = 7839
SQL> /
Now let's say you want to order alphabetically within each level (i.e. BLAKE, CLARK,
JONES for level 2, and ALLEN, JAMES, MARTIN, TURNER, WARD for level 3 under
BLAKE). Here are some standard attempts at this:
SQL> l
1 SELECT level, LPAD(' ',2*level-2)||emp.ename ename,
emp.empno, emp.mgr, emp.deptno
2 FROM Emp
3 CONNECT BY PRIOR emp.empno = emp.mgr
4* START WITH emp.empno = 7839
14 rows selected.
SQL> l
1 SELECT level, LPAD(' ',2*level-2)||emp.ename ename,
emp.empno, emp.mgr, emp.deptno
2 FROM Emp
3 CONNECT BY PRIOR emp.empno = emp.mgr
4 START WITH emp.empno = 7839
5* order by emp.ename
SQL> /
14 rows selected.
SQL> l
1 SELECT level, LPAD(' ',2*level-2)||emp.ename ename,
emp.empno, emp.mgr, emp.deptno
2 FROM Emp
3 CONNECT BY PRIOR emp.empno = emp.mgr
4 START WITH emp.empno = 7839
5* order by level,emp.ename
SQL> /
The only way I have found to truly resolve this problem is to add a hierarchy ordering
key column to the table with the hierarchy. This column needs to be populated
programmatically in such a way that you get the desired ordering. This key has to be the
concatenation of some sort of order key for EACH of the parent levels above the
hierarchy node. This will allow the hierarchy to be ordered within each level while
allowing the children to be placed directly underneath their parent. For example,
consider the EMP_HIER_ORDER column that I added to the standard emp table below:
EMPNO ENAME EMP_HIER_ORDER
--------- -------------------- -----------------------------
7369 SMITH 0008000700050012
7499 ALLEN 000800030002
7521 WARD 000800030014
7566 JONES 00080007
7654 MARTIN 000800030009
7698 BLAKE 00080003
7782 CLARK 00080004
7788 SCOTT 000800070011
7839 KING 0008
7844 TURNER 000800030013
7876 ADAMS 0008000700110001
7900 JAMES 000800030006
7902 FORD 000800070005
7934 MILLER 000800040010
SQL> l
1 SELECT level, LPAD(' ',2*level-2)||emp.ename ename,
emp.empno,
2 FROM Emp
3 CONNECT BY PRIOR emp.empno = emp.mgr
4 START WITH emp.empno = 7839
5 order by emp_hier_order
SQL> /
Which is exactly what I want. The first four characters of EMP_HIER_ORDER are used
for ordering the top level of the hierarchy ("0008"), the second four are used for ordering
the second level ("0003","0004","0007"), and the third four for the third level, etc.
NOTE: The above query used the hierarchical clauses (CONNECT BY, etc.) Using the
hierarchy ordering column you could construct a query that does not need it. For
instance:
The main problem with this technique is that it requires that extra code be written and
executed to populate the hierarchy ordering key. I used the following stored procedure to
populate the EMP_HIER_ORDER key in the above example:
-- Hierarchy query
CURSOR hier_cur IS
SELECT LEVEL lvl, empno
FROM Emp
START WITH emp.empno = 7839
CONNECT BY PRIOR emp.empno = emp.mgr;
-- Build the full ordering key for the current record. This
will
-- consist of the current record's ordering key preceded in
order
-- by the ordering keys of every level above it in the
hierarchy.
v_hier_key := '';
FOR i IN 1..h.lvl LOOP
v_hier_key := v_hier_key||t_key(i);
END LOOP;
UPDATE Emp
SET emp_hier_order = v_hier_key
WHERE empno = h.empno;
END LOOP;
END;
/
This stored procedure can be called from the client that maintains the hierarchy, executed
either manually (i.e. when the user says they are done editing the hierarchy), or
automatically. But a better method would be to put a call to this procedure into a trigger
for the table. The following trigger definition would work:
This trigger would automatically maintain the hierarchy after any updates to the table
that would affect the hierarchy (i.e. updates to the MGR column).
I will use the infamous EMP table to illustrate this technique. However, since the
standard emp table only has one hierarchy (starting with "KING"), I added a second
hierarchy. I also updated the DEPTNO for all of the standard records to have the same
DEPTNO (which I use to differentiate the two hierarchies). Using this tables, you might
use the following SQL for a standard hierarchical query:
SQL>
1 SELECT level, LPAD(' ',2*level-2)||emp.ename ename,
emp.empno, emp.mgr, emp.deptno
2 FROM Emp
3 CONNECT BY PRIOR emp.empno = emp.mgr
4* START WITH emp.empno = 7839
SQL> /
I placed a second hierarchy in the same table, this one starting with
"SONG":
SQL>
1 SELECT level, LPAD(' ',2*level-2)||emp.ename ename,
emp.empno, emp.mgr, emp.deptno
2 FROM Emp
3 CONNECT BY PRIOR emp.empno = emp.mgr
4* START WITH emp.empno = 6000
SQL> /
Let's try leaving off the "START WITH" in a view in an attempt to make a generic
hierarchy view:
SQL> CREATE OR REPLACE VIEW Emp_Hier AS
2 SELECT level lvl, LPAD(' ',2*level-2)||emp.ename ename,
emp.empno, emp.mgr, emp.deptno
3 FROM Emp
4 CONNECT BY PRIOR emp.empno = emp.mgr
5 /
Now if we select from this view without any predicates, the query will still return, but it
will return the results of a hierarchy starting with EVERY record in the table. For
example:
SQL> SELECT lvl, ename, empno, mgr, deptno
2 FROM Emp_Hier
3 /
LEVEL ENAME EMPNO MGR DEPTNO
--------- -------------------- --------- --------- ---------
1 KING 7839 10
2 BLAKE 7698 7839 10
3 MARTIN 7654 7698 10
3 ALLEN 7499 7698 10
3 TURNER 7844 7698 10
3 JAMES 7900 7698 10
3 WARD 7521 7698 10
2 CLARK 7782 7839 10
3 MILLER 7934 7782 10
2 JONES 7566 7839 10
3 FORD 7902 7566 10
4 SMITH 7369 7902 10
5 Ken 999 7369 10
3 SCOTT 7788 7566 10
4 ADAMS 7876 7788 10
1 BLAKE 7698 7839 10
2 MARTIN 7654 7698 10
2 ALLEN 7499 7698 10
2 TURNER 7844 7698 10
2 JAMES 7900 7698 10
2 WARD 7521 7698 10
1 CLARK 7782 7839 10
2 MILLER 7934 7782 10
1 JONES 7566 7839 10
2 FORD 7902 7566 10
3 SMITH 7369 7902 10
4 Ken 999 7369 10
2 SCOTT 7788 7566 10
3 ADAMS 7876 7788 10
1 MARTIN 7654 7698 10
1 ALLEN 7499 7698 10
1 TURNER 7844 7698 10
1 JAMES 7900 7698 10
1 WARD 7521 7698 10
. . . . .
. . . . .
. . . . .
1 SONG 6000 40
2 GOMEZ 6001 6000 40
3 WILLIAMS 6002 6001 40
4 DIRKSEN 6003 6002 40
5 ATKINS 6004 6003 40
5 DESZELL 6005 6003 40
5 DEVITT 6006 6003 40
2 SMITH 6007 6000 40
3 GEORGE 6008 6007 40
3 JONES 6009 6007 40
4 MILLER 6010 6009 40
4 BAKER 6011 6009 40
1 GOMEZ 6001 6000 40
2 WILLIAMS 6002 6001 40
3 DIRKSEN 6003 6002 40
. . . . .
. . . . .
. . . . .
(NOTE: I did not display the complete results of this query)
Notice that the results start with the standard hierarchy (beginning with "KING"),
followed by another hierarchy starting with "BLAKE" (which is a child of "KING", and
should not have it's own hierarchy), followed by "CLARK", "JONES", etc. There are
even one level hierarchies for the records at the bottom of the tree (i.e. "MARTIN",
"ALLEN", etc.). Now this query is generic, and it will also pick up the 2nd complete
hierarchy (starting with "SONG"). You can use the DEPTNO column to select one or the
other of the hierarchies:
SQL> SELECT lvl, ename, empno, mgr, deptno
2 FROM Emp_Hier
3 WHERE deptno = 40
3 /
1 SONG 6000 40
2 GOMEZ 6001 6000 40
3 WILLIAMS 6002 6001 40
4 DIRKSEN 6003 6002 40
5 ATKINS 6004 6003 40
5 DESZELL 6005 6003 40
5 DEVITT 6006 6003 40
2 SMITH 6007 6000 40
3 GEORGE 6008 6007 40
3 JONES 6009 6007 40
4 MILLER 6010 6009 40
4 BAKER 6011 6009 40
1 GOMEZ 6001 6000 40
2 WILLIAMS 6002 6001 40
3 DIRKSEN 6003 6002 40
4 ATKINS 6004 6003 40
4 DESZELL 6005 6003 40
4 DEVITT 6006 6003 40
1 WILLIAMS 6002 6001 40
2 DIRKSEN 6003 6002 40
3 ATKINS 6004 6003 40
3 DESZELL 6005 6003 40
3 DEVITT 6006 6003 40
1 DIRKSEN 6003 6002 40
2 ATKINS 6004 6003 40
2 DESZELL 6005 6003 40
2 DEVITT 6006 6003 40
1 ATKINS 6004 6003 40
1 DESZELL 6005 6003 40
1 DEVITT 6006 6003 40
1 SMITH 6007 6000 40
2 GEORGE 6008 6007 40
2 JONES 6009 6007 40
3 MILLER 6010 6009 40
3 BAKER 6011 6009 40
1 GEORGE 6008 6007 40
1 JONES 6009 6007 40
2 MILLER 6010 6009 40
2 BAKER 6011 6009 40
1 MILLER 6010 6009 40
1 BAKER 6011 6009 40
This limits the query to one of the hierarchies, but it does not eliminate the spurious
hierarchies. Therefore, leaving off the START WITH predicate is fairly useless
In order to add the "START WITH" back into the view, yet make the view dynamic, you
can create a database function that returns the top parent of a hierarchy given a key that
identifies the hierarchy (the deptno in this example). The following function does this for
our example:
CREATE OR REPLACE FUNCTION Get_Emp_Top(p_DeptNo IN NUMBER) RETURN
NUMBER IS
v_TopParent NUMBER;
BEGIN
RETURN(v_TopParent);
END;
/
Now, we update the view, adding a START WITH clause that uses the function:
SQL> CREATE OR REPLACE VIEW Emp_Hier AS
2 SELECT level lvl, LPAD(' ',2*level-2)||emp.ename ename,
emp.empno, emp.mgr, emp.deptno
3 FROM Emp
4 CONNECT BY PRIOR emp.empno = emp.mgr
5 START WITH emp.empno = Get_Emp_Top(emp.deptno)
6 /
As you can see, we now have a view that will return ONLY the complete hierarchy of
both hierarchies we have defined. We can simply add a predicate to the SELECT from
the view to only display one of the hierarchies, thereby giving us the dynamic selection of
the hierarchy from the view:
SQL> SELECT lvl, ename, empno, mgr, deptno
2 FROM Emp_Hier
3 WHERE DEPTNO = 40
4 /
In a previous tip (Tip #40), I detailed a method to allow joins to hierarchical queries. We
can combine that technique with the one from this tip to give us a very powerful view:
CREATE OR REPLACE VIEW emp_hier AS
SELECT emphier.emplevel, emphier.ename ind_ename, emphier.ename
,emphier.empno
,dept.deptno, dept.dname, dept.loc
,emp.ename mgr_ename
FROM Dept, Emp
,(select level emplevel, LPAD(' ',2*level-2)||ename ename,
empno, mgr, deptno
from Emp
connect by prior empno = mgr
start with empno = Get_Emp_Top(emp.deptno)
) emphier
WHERE emphier.deptno = dept.deptno
AND emphier.mgr = emp.empno (+)
Here is an example of using the view:
SQL> select ind_ename, mgr_ename, dname, loc
2 from emp_hier
3 where deptno = 40
4 /
I will use the infamous EMP/DEPT tables to illustrate this technique. Using these tables,
you might use the following SQL for a standard hierarchical query:
SQL>
1 SELECT level, LPAD(' ',2*level-2)||emp.ename ename,
emp.empno, emp.mgr, emp.deptno
2 FROM Emp
3 CONNECT BY PRIOR emp.empno = emp.mgr
4* START WITH emp.empno = 7839
SQL> /
If you try to join this query to the DEPT table, it won't work:
SQL> l
1 select level, LPAD(' ',2*level-2)||ename ename, empno, mgr,
dept.deptno, dept.dname
2 from emp, dept
3 where emp.deptno = dept.deptno
4 connect by prior empno = mgr
5* start with empno = 7839
SQL> /
from emp, dept
*
ERROR at line 2:
ORA-01437: cannot have join with CONNECT BY
Since Oracle 7.3, we could actually use a complete SELECT statement as one of the
"tables" in a query. Using this technique, we can turn the hierarchical query into a "table"
and join it do the DEPT table:
SQL> l
1 SELECT emphier.emplevel, emphier.ename, emphier.empno,
dept.deptno, dept.dname
2 FROM Dept
3 ,(select level emplevel, LPAD(' ',2*level-2)||ename
ename, empno, mgr, deptno
4 from Emp
5 connect by prior empno = mgr
6 start with empno = 7839
7 ) emphier
8* WHERE emphier.deptno = dept.deptno
SQL> /
The SELECT statement inside the parentheses is treated just as if it were a view that you
are joining to. It is given an alias, "emphier", which is used to refer to it in the SELECT
clause (i.e. "emphier.ename"), and in the WHERE clause (i.e. "emphier.deptno"). Since it
is treated like a view, we can join it to the Dept table with the following predicate:
WHERE emphier.deptno = dept.deptno
This will allow you to display the department name ("DNAME") in your hierarchical
query.
Quite often, these hierarchical queries can be useful in many programs and reports. It is
often helpful to create a view that lists the hierarchy and joins to useful tables. Here is an
example of a view using the EMP/DEPT tables. This view allows you to list the
department name and location and the manager name in the query:
CREATE OR REPLACE VIEW emp_hier AS
SELECT emphier.emplevel, emphier.ename ind_ename, emphier.ename
,emphier.empno
,dept.deptno, dept.dname, dept.loc
,emp.ename mgr_ename
FROM Dept, Emp
,(select level emplevel, LPAD(' ',2*level-2)||ename ename,
empno, mgr, deptno
from Emp
connect by prior empno = mgr
start with empno = 7839
) emphier
WHERE emphier.deptno = dept.deptno
AND emphier.mgr = emp.empno (+)
Here is an example of using the view:
SQL> select ind_ename, mgr_ename, dname, loc
2 from emp_hier
3
SQL> /
The following data (from the infamous EMP table) will be used for all of the examples in
this Tip:
SQL> desc emp
Name Null? Type
------------------------------- -------- ----
EMPNO NOT NULL NUMBER(4)
ENAME CHAR(10)
JOB CHAR(9)
MGR NUMBER(4)
HIREDATE DATE
SAL NUMBER(7,2)
COMM NUMBER(7,2)
DEPTNO NOT NULL NUMBER(2)
EMPNO SAL
--------- ---------
41 4200
46 6800
99 9000
23 2000
11 4000
10 3500
51 4500
52 4500
53 8000
54 2900
10 rows selected.
Many SQL begginers are tempted to try to use ROWNUM along with an ORDER BY to
limit the rows returned to the highest values. However, this does not work, becuase
Oracle sets the ROWNUM value before the query results are ordered! Consider the
following query:
Notice that the records are ordered by the SAL column, but not the ROWNUM column.
If you added a where clause to limit the query to the first three ROWNUMs, you would
get:
SQL> l
1 SELECT empno, sal, rownum
2 FROM Emp
3 WHERE ROWNUM < 4
4* ORDER BY sal DESC
SQL> /
One solution for this problem is to use a correlated subquery to the same table. The
following select will return the correct rows:
SQL> l
1 SELECT empno, sal
2 FROM Emp e1
3 WHERE 3 > (SELECT COUNT(*) FROM Emp e2
4 WHERE e1.sal < e2.sal)
5* ORDER BY SAL desc
SQL> /
EMPNO SAL
--------- ---------
99 9000
53 8000
46 6800
For every row processed by the main query, the correlated subquery returns a count
(COUNT(*) ) of the number of rows with higher salaries (WHERE e1.sal < e2.sal). Then
the main query only returns rows that have fewer than three salaries that are higher
(WHERE 3 > ...). For example, for EMPNO=46, the salary is "6800". There is only 1
row with a higher salary (EMPNO=99), so the subquery returns "1", which is less than 3,
causing the "WHERE 3 > ..." to evaluate to TRUE, thereby returning the row.
However, there is a problem with this method. What if there are more than one row with
the same salary? Consider the following query, where we change it to return the first 4
rows:
SQL> l
1 SELECT empno, sal
2 FROM Emp e1
3 WHERE 4 > (SELECT COUNT(*) FROM Emp e2
4 WHERE e1.sal < e2.sal)
5* ORDER BY SAL desc
SQL> /
EMPNO SAL
--------- ---------
99 9000
53 8000
46 6800
51 4500
52 4500
Instead of returning 4 rows, it returned 5! This is because this technique returns ALL of
the rows with the highest 4 salaries, not the first 4 rows. This is a problem with this
technique, so you need to make sure that it is acceptible in your design before you use it.
If you want to use a join instead of a correlated subquery, you could use the following
select:
SQL> l
1 SELECT e1.deptno, e1.empno, e1.sal, COUNT(distinct e2.empno)
2 FROM Emp e1, Emp e2
3 WHERE e1.sal <= e2.sal
4 GROUP BY e1.deptno, e1.empno, e1.sal
5* ORDER BY COUNT(distinct e2.empno)
SQL> /
This select turns the correlated subquery into a self-join with a GROUP BY. This allows
us to change the count into a sort of RANK. However, the problem with equal salaries
remains (notice the two records with a "rank" of 5).
This rank can then be used to select the first three rows:
SQL> l
1 SELECT e1.deptno, e1.empno, e1.sal, COUNT(distinct e2.empno)
2 FROM Emp e1, Emp e2
3 WHERE e1.sal <= e2.sal
4 GROUP BY e1.deptno, e1.empno, e1.sal
5 HAVING COUNT(distinct e2.empno) < 4
6* ORDER BY COUNT(distinct e2.empno)
SQL> /
One advantage of this method is that it can be easily used to return the record for just one
ranking. For example:
SQL> l
1 SELECT e1.deptno, e1.empno, e1.sal, COUNT(distinct e2.empno)
2 FROM Emp e1, Emp e2
3 WHERE e1.sal <= e2.sal
4 GROUP BY e1.deptno, e1.empno, e1.sal
5 HAVING COUNT(distinct e2.empno) = 7
6* ORDER BY COUNT(distinct e2.empno)
SQL> /
Tip #32: Script to List Trigger Errors & Line Numbers (Type: PL/SQL)
When a trigger is created for a table, sometimes there are compilation errors (hey, were
not all perfect programmers). When you have errors, you can list the trigger's syntax
errors by using the SHOW ERRORS TRIGGER <Trigger_Name> command. However,
many times these messages are cryptic, and it can be difficult to match the error to the
specific line of code in the trigger. This tip shows a method that can be used to list the
errors, along with the trigger source, and indicating which line of source has each error.
My thanks to Tim Onions, a Principal Technical Consultant at AT&T in the UK for asking
if it was possible to do this, and spurring me on to attempting it. He also reviewed the
script and made improvement suggestions. Also, thanks to Jurij Modic of the Republic of
Slovenia Ministry of Finance for pointing out a major flaw in the original tip.
LINE
TEXT
----------
------------------------------------------------------------
DES2OWNER>
As you can see, it is fairly easy to select from this table to see the errors. But I wanted to
do something more. I want to list the code of the trigger along with the arrows, and have
the listing point to the line in the code with the error, just like I did for packages,
procedures, & Functions in Tip #3.
The PL/SQL source for packages, procedures, & functions is stored in USER_SOURCE.
However, the source of the trigger is NOT stored in USER_SOURCE. Instead, it is stored
in the TRIGGER_BODY column of USER_TRIGGER. Since TRIGGER_BODY is a
LONG column, it is difficult to work with directly (i.e. you cannot use SUBSTR and
INSTR on it). Because of this, I wrote a stored procedure that parses it into lines, gives
the lines line numbers, then matches those lines to the value in the LINE column of
USER_ERRORS. Then, using DBMS_OUTPUT to display the results, I can get the
output of this program to look very similar to the stored programs error output I got in
Tip #3.
/**********************************************************************
**/
/* LIST_TRIG_ERR - A procedure that uses DBMS_OUTPUT to list the
*/
/* compilation errors of a trigger, along with the
*/
/* trigger's source. Also indicates the source line
*/
/* with the error.
*/
/*
*/
/* Inputs: p_Trigger = The trigger name.
*/
/*
*/
/* Author: Ken Atkins
([email protected]) */
/* Principal Consultant - ARIS Corporation
*/
/*
*/
/* Please feel free to use and modify this script as long as it is not
*/
/* sold or included in any software without the prior permission of
*/
/* the author. If you do make good improvements, please send them to
*/
/* me and I will incorporate them in a future version of the script
*/
/* (giving you credit of course!).
*/
/*
*/
/* Modifications:
*/
/*---------------------------------------------------------------------
-*/
/* Ver Date By Change
*/
/* --- --------- --------------
----------------------------------------*/
/* 1.0 05-MAR-98 Ken Atkins Written.
*/
/**********************************************************************
**/
CREATE OR REPLACE PROCEDURE LIST_TRIG_ERR(p_Trigger IN VARCHAR2) AS
v_Trig LONG;
b_Continue BOOLEAN := True;
v_NumLines NUMBER := 0;
v_Line VARCHAR2(240);
v_NxtChr NUMBER := 0;
v_LstChr NUMBER := 0;
TYPE LineTabTyp IS TABLE OF VARCHAR2(240)
INDEX by BINARY_INTEGER;
t_Lines LineTabTyp;
CURSOR err_cur IS SELECT line, text
FROM User_Errors
WHERE name = p_Trigger
AND text not like '%Statement ignored%';
i NUMBER;
v_Prefix CHAR(10);
v_DDLCursor NUMBER;
v_DDLReturn NUMBER;
BEGIN
--
-- Fetch the trigger code into a variable that will be used to parse
it.
--
BEGIN
SELECT trigger_body INTO v_Trig
FROM User_Triggers
WHERE Trigger_Name = p_Trigger;
EXCEPTION
WHEN NO_DATA_FOUND THEN
Raise_Application_Error(-20001,'Trigger does not exist: '||
p_Trigger);
END;
--
-- Use DBMS_SQL to execute the command that places the errors into
USER_ERRORS
--
v_DDLCursor := dbms_sql.open_cursor;
DBMS_SQL.Parse(v_DDLCursor,'ALTER TRIGGER '||p_Trigger||' COMPILE
DEBUG',1);
v_DDLReturn := dbms_sql.execute(v_DDLCursor);
DBMS_SQL.Close_Cursor(v_DDLCursor);
--
-- Now loop through the lines in the trigger code and parse it into
separate
-- record in a PL/SQL table.
--
WHILE b_Continue LOOP
v_NumLines := v_NumLines + 1;
v_NxtChr := INSTR(v_Trig, CHR(10),1,v_NumLines);
v_Line := SUBSTR(v_Trig, v_LstChr+1, (v_NxtChr-v_LstChr));
t_Lines(v_NumLines) := v_Line;
IF v_NxtChr = 0 THEN
b_Continue := False;
ELSE
v_LstChr := v_NxtChr;
END IF;
END LOOP;
--
-- Loop through all of the errors in USER_ERRORS for this trigger,
displaying
-- each error, followed by the triggers code, with an ===>
pointing to the
-- error line.
--
DBMS_OUTPUT.Put_Line('.');
FOR e IN err_cur LOOP
DBMS_OUTPUT.Put_Line(e.text);
DBMS_OUTPUT.Put_Line('.');
FOR i IN 1..v_NumLines LOOP
IF e.line = i THEN
END;
/
Here is an example of using the procedure. First, I created a simple sql script to call the
procedure:
set serveroutput on size 100000
execute list_trig_err(UPPER('&1'));
Then, after creating the trigger, I can call this script to detail the errors.
......... 1: BEGIN
......... 2: -- If extended amout is greater than $10,000
set the status
......... 6: ELSE
......... 9: END;
.........
10:
Tip #30: An easy way to EXPLAIN and get some statistics on your SQL.
(Type: SQL*Plus)
Haven't you ever thought there should be an easier way to do the EXPLAIN PLAN and
TKPROF statistics than to edit your queries to add the commands (like EXPLAIN PLAN
SET...), or to have to find or write a script that automates this? It should be an automatic
part of SQL*Plus. Well, as of SQL*Plus 3.3 it is!! The command is called 'SET
AUTOTRACE ON'!
My thanks go out to Jack Applewhite for pointing out this command in a post to the
ODTUG email lists.
In SQL*Plus 3.3 there is a little known command (at least I didn't know about it until
recently) called SET AUTOTRACE. It is documented in the newest SQL*Plus document
set, but who really reads the whole document set for changes? Well I did not. It is very
simple to use. Just type the command:
SET AUTOTRACE ON
And then run your select statement. Example:
4 /
14 rows selected.
Execution Plan
----------------------------------------------------------
1 0 MERGE JOIN
2 1 SORT (JOIN)
4 1 SORT (JOIN)
----------------------------------------------------------
0 recursive calls
4 db block gets
2 consistent gets
0 physical reads
0 redo size
2 sorts (memory)
0 sorts (disk)
14 rows processed
There are also some other options, for example there is a TRACEONLY option which
supresses the SQL output. See the SQL*Plus 3.3 manual for a full description.
If you go off and try this on your instance, you may run into some problems. There are a
few setup steps that need to be taken to make this work:
1. Make sure you have access to PLAN_TABLE. If you don't, create it using
utlxplan.sql (It should be in a directory like $ORACLE_HOME/rdbms73/admin/)
and make sure you have access to it from the user you are using to tune the SQL.
2. You also need to create the PLUSTRACE role, and grant it to the desired users.
The script to create this role is in:
$ORACLE_HOME/plus33/Plustrce.sql
It has to be run from SYS in order to have the correct security access. Then grant
the role to the desired users or ROLEs.
Tip #28: Setting the SQL*Plus prompt to the current directory. (Type:
SQL*Plus)
Have you ever had a SQL*Plus window open, and did not remember what it's current
directory was? This tip will show a method to set your SQL*Plus prompt to include the
current directory.
The SET SQLPROMPT command can be used to set the SQL*Plus prompt to any text
string you want. The trick is to get access to the current directory from SQL*Plus, so you
can use it in SET SQLPROMPT.
We have access to the current directory from the OS (via 'cd' in DOS/NT and 'pwd' in
Unix). We can also call an OS script by using the SQL*Plus HOST command. Using
these two capabilities, I wrote two scripts that together performed the function I wanted:
1. A .bat script that writes a SQL script to do the actual SET SQLPROMPT, using
the 'cd' command to insert the current directory.
2. A sql script that executes the .bat script and runs the SQL script that it has written.
setprmpt.bat
-----------------
echo set define $ > tmp.sql
echo column curdir noprint new_value curdir >> tmp.sql
echo SELECT REPLACE(' >> tmp.sql
cd >> tmp.sql
echo ',CHR(10),'') curdir FROM DUAL; >> tmp.sql
echo set sqlprompt "($curdir) SQL> " >> tmp.sql
setprmpt.sql
-----------------
host setprmpt.bat
@tmp
set define &
Here is an example of using the scripts to set the prompt:
SQL> @setprmpt
(C:\d2k\working) SQL>
set define $
SELECT REPLACE('
C:\d2k\working
The .sql script then executes this temporary script file. The TMP.SQL script had to be so
complicated because I was only using the DOS output redirection capabilities ('>' and
'>>') and I could only get the current directory into the file on it's own line. The
REPLACE(..,CHR(10),'') command removes the carriage return before and after the
directory line.
The 'column .. new_value' command is a SQL*PLUS command that allows the value of a
selected column to be placed into a SQL*Plus variable (in this case 'curdir'). Therefore,
when the following SELECT.. is run, the text string of the current directory is placed in
the 'curdir' SQL*Plus variable. Then the SET SQLPROMPT uses this variable to set the
prompt.
Another feature of this technique, is that you now have the SQL*Plus variable with the
current directory available for other uses in this SQL*Plus session. For example, it can be
used in a select like:
'C:\FMPT\SQ
-----------
C:\fmpt\sql
The above .bat file is NOT pretty. Using Perl or some other scripting language, I could
write a simple script that would just write the SET SQLPROMPT command directly
without the use of the 'column' command or the 'SELECT'.
Have you ever tried to order by a VARCHAR2 column that has numeric information in
it? Your query is sorted ALPHABETICALLY instead of numerically. That is, your order
is 1,10,2,20,200,3,4... instead of 1,2,3,4,10,20,200. If you try to use TO_NUMBER in the
order_by your query blows up if there are any alpha characters in the column. This tip
details a method that can be used to have the order_by return the columns numerically
even if there are some alpha characters in the column.
SORTBY
--------------------
100
10
1AB
20
BBBB
1000
11
30
3
200
21
14 rows selected.
If you do a simple order_by your result will be:
SQL> SELECT sortby
2 FROM Number_Sort
3 ORDER BY sortby;
SORTBY
--------------------
10
100
1000
11
1AB
20
200
21
30
BBBB
14 rows selected.
Which is not what you want! However, the following select WILL return the column
ordered numerically:
SQL> SELECT sortby
2 FROM Number_Sort
3 ORDER BY
DECODE(TO_CHAR(NVL(LENGTH(TRANSLATE(sortby,'A1234567890','A')),0)),'0',LPAD(sor
tby,8),sortby)
4 /
SORTBY
--------------------
10
11
20
21
30
100
200
1000
1AB
BBBB
14 rows selected.
Now let's take that construct apart to see how it works:
Now why do we want to LPAD the numeric values only? Because space (' ') sorts
BEFORE the numbers! This allows for a decimal place by decimal place comparison of
the two numbers. The '1' will return with 7 leading blanks, the '10' with 6, ect. When the
alphabetical sort is done, the values are compared, character by character, and blank sorts
before the numbers. If you selected the SQL construct that you are sorting by, you would
see something like:
SQL> SELECT
DECODE(TO_CHAR(NVL(LENGTH(TRANSLATE(sortby,'A1234567890','A')),0)),'0',LPAD(sortby,8),s
ortby)
2 FROM Number_Sort
3 ORDER BY
DECODE(TO_CHAR(NVL(LENGTH(TRANSLATE(sortby,'A1234567890','A')),0)),'0',LPAD(sortby,8),s
ortby)
4 /
SORTBY
--------------------
10
11
20
21
30
100
200
1000
1AB
BBBB
14 rows selected.
The '8' I used in the LPAD is just arbitrary. If I had 15 digit numbers in
the SORTBY column, I would use a value greater than 15 so that any
number would sort correctlyTip #23: Using a Database Function to Query
by a LONG Column. (Type: SQL)
Have you ever tried to use LONG columns in the WHERE clause of your SQL
statement? Something
like 'WHERE long_column like '%SEARCH%'? If so, you know this does not work!
(You get 'ORA-00932:
inconsistent datatypes') This tip shows how you can use database functions to avoid this
limitation, and
query by LONG columns anyway.
LONG_WHERE
==========
Name Null? Type
------------------- -------- ----
LONG_ID NOT NULL NUMBER
LONG_DESC LONG
If you tried to select by the long column, you might use something like:
LONG_ID
LONG_DESC
---------
-------------------------------------------------------
2 Another bunch of text to
place into a long value. Search for SEARCH2 somewhere
Tip #19: Selecting ONLY the group with the maximum Sum in a group
query. (Type: SQL)
Let's say you have a select that is summing by a key value, and you want to only return
the key that has the maximum sum (not ALL of the rows like a group by will). This tip
will show a SQL statement that does this.
SUM_PARENT
----------
Name Null? Type
------------------- -------- ----
SUM_ID NUMBER
SUM_NAME VARCHAR2(10)
SUM_ID SUM_NAME
--------- ----------
1 ONE
2 TWO
3 THREE
4 FOUR
5 FIVE
SUM_CHILD
---------
Name Null? Type
------------------- -------- ----
SUM_ID NUMBER
QTY NUMBER
SUM_ID QTY
--------- ---------
1 10
1 20
1 5
2 10
2 5
3 3
3 2
4 30
4 2
5 10
The following simple GROUP BY select will return the sum of the QTY for each key
(SUM_NAME):
SELECT p.sum_name, sum(c.qty)
FROM Sum_Parent p, Sum_Child c
WHERE p.sum_id = c.sum_id
GROUP BY p.sum_name
/
SUM_NAME SUM(C.QTY)
---------- ----------
FIVE 10
FOUR 32
ONE 35
THREE 5
TWO 15
Let's say you only want to return the row with the MAXIMUM quantity
(SUM_NAME=ONE). To do this, you can add a HAVING predicate related to a sub-
query. The HAVING is needed to be able to use the SUM(c.qty) in a predicate. An
example of this follows:
SELECT p.sum_name, sum(c.qty)
FROM Sum_Parent p, Sum_Child c
WHERE p.sum_id = c.sum_id
GROUP BY p.sum_name
HAVING SUM(c.qty) =
(SELECT MAX(SUM(c2.qty))
FROM Sum_Child c2
GROUP BY c2.sum_id
)
/
SUM_NAME SUM(C.QTY)
---------- ----------
ONE 35
Which is the desired result. One caution however, if there are more than one key with the
maximum sum, the query will return ALL of them. For instance, if the following data is
added to the SUM_CHILD table:
SUM_ID QTY
--------- ---------
3 30
The above query will return the following result.
SUM_NAME SUM(C.QTY)
---------- ----------
ONE 35
THREE 35
This query is not very efficient however. If you have PL/SQL available to you, you can
get the same result by creating a cursor with the first query, adding an order by clause,
and only fetching the first row. This will avoid the second sum, which will give it better
performance. Here is an example of this:
set serveroutput on
DECLARE
CURSOR sum_cur IS
SELECT p.sum_name, sum(c.qty)
FROM Sum_Parent p, Sum_Child c
WHERE p.sum_id = c.sum_id
GROUP BY p.sum_name
ORDER BY sum(c.qty) desc;
SumName VARCHAR2(10);
SumQty NUMBER;
BEGIN
OPEN sum_cur;
FETCH sum_cur INTO SumName, SumQty;
CLOSE sum_cur;
DBMS_OUTPUT.PUT_LINE('SUM_NAME: '||SumName||' Sum(Qty):
'||to_char(SumQty));
END;
/
SUM_NAME: ONE Sum(Qty): 35
Outer joins are very useful in SQL to return data from queries where some of the
relationships are optional. However, there are times when the outer joins can cause some
problems. Sometimes they will make the query run very slowly. There is also a restriction
that you can only outer a table to ONE other table. In these cases, database functions can
be used to eliminate the need for the outer joins. This tip will detail how to do this for the
following two examples:
OUTER_ELIM
==========
PK KTYPE KEY1 KEY2 KEY3
--------- ----- --------- --------- ---------
1 1 1
2 1 2
3 1 3
5 2 1
6 2 2
7 2 3
9
10 3 1
11 3 2
OUTER_ELIM_CLD CROSS_LABEL
============== ===========
PK KEY4 KEY1 KEY4
CROSS_LABEL
--------- --------- --------- ---------
---------------
1 1 1 2 L1-1 X
L4-2
1 2 3 1 L1-3 X
L4-1
1 3
2 1
2 2
2 3
3 1
3 2
3 3
This data model will be used to illustrate both examples of using a database function to
eliminate outer joins:
Let's say you need a SQL select statement to implement the above arc (from
OUTER_ELIM to LOOKUP1,2,3). For example, you want to display only ONE of the
labels (either LABEL1, LABEL2, or LABEL3) depending on the value of the
KEY_TYPE column. The following SQL statement could be used to do this:
/* tip18q1.sql */
SELECT oe.pk,
DECODE(ktype,'1',l1.label1,'2',l2.label2,'3',l3.label3)
Label
FROM outer_elim oe, lookup1 l1, lookup2 l2, lookup3 l3
WHERE oe.key1 = l1.key1 (+)
AND oe.key2 = l2.key2 (+)
AND oe.key3 = l3.key3 (+)
ORDER BY
DECODE(ktype,'1',l1.label1,'2',l2.label2,'3',l3.label3)
/
PK LABEL
--------- ----------
1 1-ONE
10 1-THREE
5 1-TWO
2 2-ONE
11 2-THREE
6 2-TWO
3 3-ONE
7 3-TWO
9
However, sometimes having multiple outer joins can cause performance problems.
Especially when there are many tables joined together.
A database function can be used to get the same query results without using an outer join.
To do this, first create the following database function:
/* tip18fun.sql */
CREATE OR REPLACE
FUNCTION comb_label(pType IN VARCHAR2
,pKey1 IN NUMBER,pKey2 IN NUMBER,pKey3 IN NUMBER)
RETURN VARCHAR2 IS
vReturn VARCHAR2(10);
BEGIN
IF pType = '1' THEN
BEGIN
SELECT label1 INTO vReturn
FROM lookup1
WHERE key1 = pKey1;
END;
ELSIF pType = '2' THEN
BEGIN
SELECT label2 INTO vReturn
FROM lookup2
WHERE key2 = pKey2;
END;
ELSIF pType = '3' THEN
BEGIN
SELECT label3 INTO vReturn
FROM lookup3
WHERE key3 = pKey3;
END;
END IF;
RETURN(vReturn);
END;
/
This function can be used in the following SQL to return the same results as the first
query, but without any outer joins:
/* tip18q2.sql */
column label format a12;
SELECT oe.pk, comb_label(ktype,key1,key2,key3) Label
FROM outer_elim oe
ORDER BY comb_label(ktype,key1,key2,key3)
/
There are times when you really want to outer join one table to two different tables.
When this happens, the restriction can be frustrating. In the above data model, you might
want to do an outer join from the CROSS_LOOKUP table to both the OUTER_ELIM
and OUTER_ELIM_CLD tables. To do this, you might try to use a SQL statement like:
/* tip18q3.sql */
SELECT oe.pk, l1.label1, l4.label4, cl.cross_label
FROM outer_elim oe, outer_elim_cld cld, lookup1 l1,
lookup4 l4
,cross_lookup cl
WHERE oe.pk = cld.pk
AND oe.key1 = l1.key1
AND cld.key4 = l4.key4
AND cl.key1 (+) = oe.key1
AND cl.key4 (+) = cld.key4
/
Which will cause the following error:
AND cl.key1 (+) = oe.key1
*
ERROR at line 7:
ORA-01417: a table may be outer joined to at most one other
table
This error occurs because Oracle will not let you have an outer join from ONE table to
two DIFFERENT tables.
However, you CAN get the desired effect using a database function. If the following
function is created:
/* tip18fn2.sql */
CREATE OR REPLACE
FUNCTION cross_label(pKey1 IN NUMBER,pKey4 IN NUMBER)
RETURN VARCHAR2 IS
vReturn VARCHAR2(15);
BEGIN
SELECT cross_label INTO vReturn
FROM cross_lookup
WHERE key1 = pKey1
AND key4 = pKey4;
RETURN(vReturn);
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN('');
END;
/
then the following SQL can be used to return the 'outer join' values from the
CROSS_LOOKUP:
/* tip18q4.sql */
column cross_label format a15
SELECT oe.pk, l1.label1, l4.label4, cross_label(oe.key1,
cld.key4) cross_label
FROM outer_elim oe, outer_elim_cld cld, lookup1 l1,
lookup4 l4
WHERE oe.pk = cld.pk
AND oe.key1 = l1.key1
AND cld.key4 = l4.key4
/
This select will return:
PK LABEL1 LABEL4 CROSS_LABEL
--------- ---------- ---------- ---------------
1 1-ONE 1-FOUR
1 1-ONE 2-FOUR L1-1 X L4-2
1 1-ONE 3-FOUR
2 2-ONE 1-FOUR
2 2-ONE 2-FOUR
2 2-ONE 3-FOUR
3 3-ONE 1-FOUR L1-3 X L4-1
3 3-ONE 2-FOUR
3 3-ONE 3-FOUR
Stored procedures and functions are very powerful. These are just some simple examples
to get people thinking about some possible ways of using them.
Tip #17: Calculating a running total with SQL only. (Type: SQL)
There are applications where the running total for a series of numbers needs to be
calculated and displayed. While this might be normally considered something that would
be done with 3GL programming techniques, it IS possible to calculate and query a
running total with SQL only.
Let's assume we have a table with a numeric column that we want. We also need to have
a column to order the display by, or a running total does not make sense! Many times this
will be a date column. This could also be the PK of the table, or a single or multiple
column UK. I will use a date column in the following example. Consider the table:
RUN_DATE RUN_VALUE
--------- ---------
02-APR-97 10
03-APR-97 5
04-APR-97 20
05-APR-97 15
06-APR-97 45
07-APR-97 12
08-APR-97 37
09-APR-97 9
10-APR-97 23
11-APR-97 19
12-APR-97 10
11 rows selected.
In order to produce the running total, a self-join is needed to sum all of the values of the
table less than or equal to each row. The query is grouped by the records in the 'driving'
table in the join, and ordered by the run_date:
11 rows selected.
Tip #6: Using SQL only to Determine the Business Days Between Two
Dates. (Type: SQL)
There are many times in reports or in calculations for forms where the number of
business days between two dates needs to be determined. Here is a method for
caluculating this (excluding holidays) using SQL only.
The following SQL script shows an algorythm that uses the standard Oracle date
functions to calculate the number of business days between to dates. This method cannot
exclude holidays (obviously), however there are many times that just the standard
business days is useful. I am sure there are other algorythms that could be used, however
this one has worked for me. The algorythm is described below in the comments of the
script.
/**********************************************************
*******/
/* An example of business days calculation in SQL
*/
/*
*/
/* The algorythm is:
*/
/*
*/
/* 1) Take the absolute difference between the dates
*/
/* to_date('&todate') - to_date('&frdate')
*/
/* 2) Subtract the weekends (number of weeks in the range
*/
/* TRUNC(to_date('&todate'),'D') = 1st day of week
that */
/* end of period is
in */
/* TRUNC(to_date('&frdate'),'D') = Last day of week
that */
/* start of period
is in */
/* So subtracting these two gives the number of days
*/
/* between the two dates but including all of the
days in */
/* the weeks that the dates start and end in. When
this */
/* number is divided by 7 it gives the number of
weeks. */
/* Multiplying by 2 gives the number of weekend days.
*/
/* 3) Subtract 1 day if the ending date is on a saturday
*/
/* DECODE(to_char(to_date('&todate'),'D'),7,-1,0)
*/
/* --> If the day of the week is saturday (7),
returns -1 */
/* 4) Subtract 1 day if the start date is on a sunday
*/
/* DECODE(to_char(to_date('&frdate'),'D'),1,-1)
*/
/* --> If the day of the week is sunday (1), returns
1 */
/* 5) Add one day to make the range inclusive (The '1 + '
) */
/*---------------------------------------------------------
------*/
/* Author: Kenneth Atkins ([email protected])
*/
/* http://www.olywa.net/katkins/oratip
*/
/**********************************************************
*******/
define frdate = '&1'
define todate = '&2'
set verify off
select
'&frdate' From_Date
,'&todate' To_Date,
1 + to_date('&todate') - to_date('&frdate') -
((TRUNC(to_date('&todate'),'D') -
TRUNC(to_date('&frdate'),'D'))/7)*2
+ DECODE(to_char(to_date('&todate'),'D'),7,-1,0)
+ DECODE(to_char(to_date('&frdate'),'D'),1,-1,0)
Business_Days
from dual
/
1 row selected.
BUSINESS_DAYS('01-AUG-96','15-AUG-96')
--------------------------------------
11
1 row selected.
Of course in a stored function, you could add code to substract holidays also. Perhaps
something like:
SELECT COUNT(*) INTO nHolidays
FROM Holiday_Table
WHERE holiday_date BETWEEN p_from_date AND p_to_date;
Then substract nHolidays from your business_days variable before returning.
Tip #3: Script to List PL/SQL Errors & Line Numbers (Type: PL/SQL)
When PL/SQL stored packages, procedures, or functions are loaded into the database and
compiled (using CREATE OR REPLACE) any syntax errors in the code can be listed
using the 'SHOW ERROR' command. However, many times these messages are cryptic,
and the line numbers specified do not correspond to the line number in the SQL file used
to load the code (because blank lines and comments before the 'CREATE' statement are
not loaded. For large procedures, this can be very frustrating. This hint will show how a
SQL script can be used to list the errors along with the actual source lines, indicating the
line with the error (with a '-->').
Package created.
No errors.
4 vUSER VARCHAR2(30);
5 vTest VARCHAR2(30);
6 nTest NUMBER;
7 BEGIN
8 /* Comment line */
--> 9 vVar := to_num('12');
10 /* Comment line */
11 /* The next line has an error (missing
semicolon) */
12 nVar := 1;
13 /* more comments */
14 nVar := 3;
14 nVar := 3;
15 BEGIN
16 SELECT user INTO vUSER
17 FROM dual;
18 EXCEPTION
--> 19 WHEN NO_DTA_FOUND THEN
20 NULL;
21 END;
22 BEGIN
23 SELECT user INTO vUSER
24 FROM dual;
28 rows selected.
The following select can also be used to just list the source for a stored procedure,
package or function, putting in the line numbers the compiler uses:
/**********************************************************
*******************/
/* listsource.sql - Lists source for PL/SQL programs.
*/
/*
*/
/* Parameter: &1 = The name of the PROCEDURE, PACKAGE,
or FUNCTION */
/*
*/
/* Author: Ken Atkins ([email protected])
*/
/* http://www.arrowsent.com/oratip
*/
/*
*/
/**********************************************************
*******************/
define obj_name = '&1';
column text format a74 heading 'Source Listing';
column line format 9999 heading 'Line';
set verify off
set linesize 80;
set pagesize 0;
set pause off;
spool &obj_name
SELECT
us.line, us.text
from user_source us
where us.name = '&obj_name'
order by type, line
/
spool off
set pause on;
set pagesize 22;
Sometimes it is useful to be able to get the name of the current instance from within
SQL*Plus or another development tool (such as Oracle*Forms or Oracle*Reports). Here
is a simple SQL statement that can be used to get the instance name. Also presented is an
example of using the SQL statement to set the SQL prompt to the instance name.
There is an internal oracle view called 'V$PARAMETER' which holds the values of
many parameters that the database uses. One of these parameters is called 'DB_NAME'.
This parameter holds the name of the database (kind of makes sense, doesn't it?). The
V$PARAMETER view looks like:
desc v$parameter
The following SQL statement will return the database name from this view:
SELECT UPPER(value)
FROM V$Parameter
WHERE UPPER(name) = 'DB_NAME';
A SQL statement like this can be used to replace the standard sql prompt ('SQL>') with
the instance name. This can be useful if you are accessing many different instances in
SQL*Plus. It has saved me from messing up data in the wrong instance many times (like
deleting data from the production instance instead of test). Add the following SQL to
your login.sql file:
rem Create _DB_Name variable for general use.
set termout off
column upper(VALUE) new_value _DB_NAME;
select upper(value), from v$parameter
where upper(name) = 'DB_NAME';
rem
rem Put SGA Name in sql prompt
set SQLPROMPT '&_DB_NAME.> '
rem
rem If pause is on, say something when needed.
set pause '> '
rem
clear breaks
set termout on
set pause on
set feedback on
If the instance name is something like 'TESTINST' then the above SQL in the login.sql
file will change the prompt to:
TESTINST>
• Make the parameter to a procedure conditional. For example, the call to the
'conditional_proc' procedure above could be changed to:
• column userval new_value user_val;
• SELECT user userval FROM DUAL;
• .
• .
• conditional_proc('&user_val');
• Make the name of a procedure conditional. The following lines could be used to
call a procedure that is called _setup, where is the name of the oracle instance.
• column procname new_value proc_name;
• SELECT value||'_setup' procname
• FROM v$parameter
• WHERE upper(name) = 'DB_NAME';
• .
• .
• &proc_name;
Here is a simple PL/SQL procedure that accepts the domain and sub-domain of the
Oracle Portal session and prints the names and values of all of the sessions attributes to
the browser.
v_num_attr := v_elements.count;
htp.p('Session attributes for:<BR><BR><TABLE
BORDER=0><TR><TD>DOMAIN:</TD><TD><B>'||
p_domain||'</B></TD></TR>');
htp.p('<TR><TD>SUB-DOMAIN:</TD><TD><B>'||
p_subdomain||'</B></TD></TR></TABLE>');
htp.br;
htp.p('Number of Attributes: '||to_char(v_num_attr));
htp.br;
htp.br;
htp.p('<TABLE BORDER="0">');
FOR i IN 1..v_num_attr LOOP
htp.p('<TR><TD>'||
v_elements(i).name||'</TD><TD>=</TD><TD>'||
v_elements(i).varchar2_data||'</TD></TR>');
END LOOP;
htp.p('</TABLE>');
END;
/
show errors
You can download the script by clicking here.
Installation
1. Install the procedure either in the PORTAL30 schema, or a schema that has access
to the PORTAL30 programs (for instance a provider schema).
2. Grant execute on the procedure to the portal30 public schema (usually
PORTAL30_PUBLIC). For instance:
3. GRANT EXECUTE ON show_session TO portal30_public;
To use the procedure, simply open a separate navigator window (after you have run the
application which has set some session attributes), and type a call to the procedure into
the URL field. For example, the following URL will call the procedure on a local install
of portal30 with the standard dad:
http://localhost/pls/portal30/portal30.show_session?p_domain=CONTEX
T&p_subdomain=SESS_CRT
This call assumes the procedure is installed in the PORTAL30 schema. If it is installed in
another schema, replace the "portal30" immediately before "show_session" with the
name of the schema in which it is installed.
The value for p_domain and p_subdomain are set to the domain and subdomain of the
session variable you want to see.
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary: How to determine the time and
There is information available
informatoin is not in a conveni
found it to be handier than look
SELECT TO_DATE(VALUE,
FROM V$INSTANCE
WHERE key = 'STARTUP
SELECT TRUNC(VALUE/(6
Description: time”
FROM V$INSTANCE
WHERE KEY = 'STARTUP
select to_char(startu
from v$instance
Summary: How do I load a large (more than 4K) H
To load a large HTML file into a varcha
the data into multiple rows before insert
To use:
create inde
(NLS_UPPER(
select * fr
NLS_UPPER(c
Description:
This is much fas
create inde
table_y(col
select * fr
upper(col_1
..because in the
already converte
and in the secon
conversion as it
userid=scott/tiger
rows=n
indexes=n
grants=n
file=temp_inside.dmp
tables=(user1.Table_x)
select count(distinct
substr(rowid,1,8)||substr(rowid,
from ;
select count(distinct
substr(rowid,10,6)||substr(rowid
from ;
Adjustment of
scripts following
Summary:
de-support of
svrmgrl.
Oracle has
announced future
desupport of
svrmgrl. You may
ask yourself, self,
how am I going to
adjust my scripts
to SQLPLUS and
include the
shutdown/startup
stuff.
Description: Use the following
command:
sqlplus
/nolog
SQL>connect
internal;
connected
SQL>shutdown
immediate;
Cool huh!
dbms_system.ksdwrt(2,'A line of
text');
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
Summary:
Description:
This will cause SQL Loader to correctly interpret data where line feeds or
CRs are a legitimate part of the data.
Summary: SQL Loader parameters when fields contain line feeds or carriage returns
SQL Loader control files use line feed or CR to signify the end of the reco
entry of these characters (most commonly as part of a description or text f
encounters these line feeds, it assumes the end of the record and generates
This will cause SQL Loader to correctly interpret data where line feeds or
CRs are a legitimate part of the data.
Summary: Stored procedure to recompile schema
Description: EXEC DBMS_UTILITY.COMPILE_SCHEMA( 'schema-name' );