Skip to Main Content
  • Questions
  • view containing in ,or clause with outer join

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, jamie.

Asked: September 20, 2002 - 12:20 pm UTC

Last updated: June 28, 2004 - 1:10 pm UTC

Version: 8.1.7

Viewed 1000+ times

You Asked

Hi Tom,
I am currently rewriting sql server views into our oracle for an application.
One of the views that I have rewritten contains outer joins in the where clause along with the "or" clause.This will not compile in the database it gives me ORA-01719: outer join operator (+) not allowed in operand of OR or IN.I rewrote the where portion that failed into a select statement in the from clause in order to have this compile.Could you explain to me why it will not let me have outer join and or in where portion of statement.
It also appears that there
are left outer joins on primary key columns.Shouldn't these be equi join or have the (+) on the column other then pkey.Would there be an instance where I would want (+) join on a primary key column.
Could you please clarify this area for me so I can better understand
under what conditions (+) join is appropriate.
Below is the original sql server view followed by it being rewritten with the "or" clause failing and then the compiled copy with the "or" portion written in the from clause.
As usual any advice is greatly appreciated and thanks for your help.

(sql server)
CREATE VIEW imrpt_work_load_schd_by_craft
AS
SELECT
wor.pkey,
wor.id ,
wor.wo_num,
wor.date_scheduled,
wor.date_completed,
wos.id as status_id,
wor.description as wo_description,
CASE WHEN (wor.id IS NULL OR wor.id = ' ')
THEN CONVERT(NVARCHAR(25), wor.wo_num) ELSE
RTRIM(wor.id) + '-' + CONVERT(NVARCHAR(25), wor.wo_num) END AS WO_ID,
c.id as resource_id,
cnt.id as 'craft_id',
st.id AS site_id,
st.description AS site_description

FROM imtbl_woap_est_res war LEFT OUTER JOIN
imtbl_craft cnt ON cnt.pkey = war.craft LEFT OUTER JOIN
imtbl_contact c ON war.resources = c.pkey LEFT OUTER JOIN
imtbl_woap woa ON woa.pkey = war.work_order_asset_procedure LEFT OUTER JOIN imtbl_work_order wor ON wor.pkey = woa.work_order LEFT OUTER JOIN imtbl_work_order_status wos ON wor.status = wos.pkey,
imtbl_site st
WHERE wos.category = 1 OR wos.category = 2 OR wos.category = 3 AND war.Remaining_Hours > 0


(oracle failed view)
CREATE OR REPLACE VIEW imrpt_work_load_schd_by_craft (
pkey,
id,
wo_num,
date_scheduled,
date_completed,
status_id,
wo_description,
wo_id,
resource_id,
craft_id,
site_id,
site_description )
AS
SELECT
wor.pkey,
wor.id ,
wor.wo_num,
wor.date_scheduled,
wor.date_completed,
wos.id as status_id,
wor.description as wo_description,
CASE WHEN (wor.id IS NULL OR wor.id = ' ')
THEN to_char(wor.wo_num) ELSE
RTRIM(wor.id)||'-'||to_char( wor.wo_num) END AS WO_ID,
c.id as resource_id,
cnt.id as craft_id,
st.id AS site_id,
st.description AS site_description
FROM imtbl_woap_est_res war,
imtbl_craft cnt,
imtbl_contact c,
imtbl_woap woa,
imtbl_work_order wor,
imtbl_work_order_status wos,
imtbl_site st
WHERE

WOS.CATEGORY = 1 OR WOS.category = 2 OR WOS. category = 3 AND
war.Remaining_Hours > 0 AND
cnt.pkey(+) = war.craft AND
war.resources = c.pkey(+) AND
woa.pkey(+) = war.work_order_asset_procedure AND
wor.pkey(+) = woa.work_order AND
wor.status = wos.pkey


(or claues moved into from clause)

CREATE OR REPLACE VIEW imrpt_work_load_schd_by_craft (
pkey,
id,
wo_num,
date_scheduled,
date_completed,
status_id,
wo_description,
wo_id,
resource_id,
craft_id,
site_id,
site_description )
AS
SELECT
wor.pkey,
wor.id ,
wor.wo_num,
wor.date_scheduled,
wor.date_completed,
wos.id as status_id,
wor.description as wo_description,
CASE WHEN (wor.id IS NULL OR wor.id = ' ')
THEN to_char(wor.wo_num) ELSE
RTRIM(wor.id)||'-'||to_char( wor.wo_num) END AS WO_ID,
c.id as resource_id,
cnt.id as craft_id,
st.id AS site_id,
st.description AS site_description
FROM imtbl_woap_est_res war,
imtbl_craft cnt,
imtbl_contact c,
imtbl_woap woa,
imtbl_work_order wor,
(select id,pkey from imtbl_work_order_status where CATEGORY 1 OR category = 2 OR category = 3) wos,
imtbl_site st
WHERE

war.Remaining_Hours > 0 AND
cnt.pkey(+) = war.craft AND
war.resources = c.pkey(+) AND
woa.pkey(+) = war.work_order_asset_procedure AND
wor.pkey(+) = woa.work_order AND
wor.status = wos.pkey










and Tom said...

No chance of using a database written this century?

but anyway, there is NO outer join using an OR in the original query???


That, and the use of the OUTER JOIN is in VAIN in this case (eg: the original author did not understand what they were doing! hope it wasn't you ;)

using OR's and AND's without paren's is a recipe for disaster as well! Your where clause should use parens so it is obvious that:

WHERE wos.category = 1 OR wos.category = 2 OR wos.category = 3 AND
war.Remaining_Hours > 0

is being intrepreted as:

WHERE (wos.category = 1 OR wos.category = 2 OR wos.category = 3) AND
war.Remaining_Hours > 0)

(hope that's what you meant to have it mean!!!)





FROM
imtbl_woap_est_res war LEFT OUTER JOIN
imtbl_craft cnt ON cnt.pkey = war.craft LEFT OUTER JOIN
imtbl_contact c ON war.resources = c.pkey LEFT OUTER JOIN
imtbl_woap woa ON woa.pkey = war.work_order_asset_procedure LEFT OUTER JOIN
imtbl_work_order wor ON wor.pkey = woa.work_order LEFT OUTER JOIN
imtbl_work_order_status wos ON wor.status = wos.pkey,
imtbl_site st
WHERE wos.category = 1
OR wos.category = 2
OR wos.category = 3
AND war.Remaining_Hours > 0

Looking at that, you must understand that the WHERE clause is done AFTER the joining. So, just using THREE tables (to make it readable):

ops$tkyte@ORA920.US.ORACLE.COM> create table t1 ( x int );
Table created.

ops$tkyte@ORA920.US.ORACLE.COM> create table t2 ( x int );
Table created.

ops$tkyte@ORA920.US.ORACLE.COM> create table t3 ( x int );
Table created.

ops$tkyte@ORA920.US.ORACLE.COM> insert into t1 values ( 1 );
1 row created.

ops$tkyte@ORA920.US.ORACLE.COM> insert into t3 values ( 2 );
1 row created.

ops$tkyte@ORA920.US.ORACLE.COM> insert into t3 values ( 3 );
1 row created.

ops$tkyte@ORA920.US.ORACLE.COM> select *
2 from t1 LEFT OUTER JOIN t2 wos on (t1.x = wos.x), t3
3 /

X X X
---------- ---------- ----------
1 2
1 3

So, this is similar to your query. We have a bunch of tables "left outer joined" to each other and the CARTESIAN producted with a last table (imtbl_site st in your case).

Without a where clause, we get values back (t2 wos -- has NO rows). Now, we add the where clause -- which HAPPENS AFTER THE OUTER JOIN REMEMBER!!!


ops$tkyte@ORA920.US.ORACLE.COM> select *
2 from t1 LEFT OUTER JOIN t2 wos on (t1.x = wos.x), t3
3 where wos.x = 1 or wos.x = 2
4 /

no rows selected

and since we have a predicate on a column that would be NULL if we actually outer join to it -- it immediate drops out of the result set. The outer join to WOS in your case is *totally and 100% a waste of time and resources*. The only thing it is doing is removing other possibly better access paths from consideration by the optimzer!!!!!

If we EVER outer join to WOS, it can *never ever* satisfy the where clause (cause the columns are NULL).


So -- three things here:

o you are not having an outer join in the JOINS at all
o your outer join to WOS is totally useless here
o if you used the current production release of Oracle -- the outer join syntax would just "go", no rewrite.



This is the 8i rewrite:

FROM imtbl_woap_est_res war,
imtbl_craft cnt ,
imtbl_contact c ,
imtbl_woap woa ,
imtbl_work_order wor ,
imtbl_work_order_status wos ,
imtbl_site st
WHERE cnt.pkey(+) = war.craft
and war.resources = c.pkey (+)
and woa.pkey(+) = war.work_order_asset_procedure
and wor.pkey(+) = woa.work_order
and wor.status = wos.pkey(+)
and wos.category in (1,2,3)
and war.Remaining_Hours > 0

Although, as I mentioned -- the outer join to WOS is toally not needed -- you can drop the (+) on the wos.pkey join condition without changing the meaning at all!!! In fact, that'll make the optimizer have a much easier go at it.




Rating

  (7 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

view outer join, or clause

A reader, September 20, 2002 - 10:02 pm UTC

Tom,
Thanks for the review. Your final solution
FROM imtbl_woap_est_res war,
imtbl_craft cnt ,
imtbl_contact c ,
imtbl_woap woa ,
imtbl_work_order wor ,
imtbl_work_order_status wos ,
imtbl_site st
WHERE cnt.pkey(+) = war.craft
and war.resources = c.pkey (+)
and woa.pkey(+) = war.work_order_asset_procedure
and wor.pkey(+) = woa.work_order
and wor.status = wos.pkey(+)
and wos.category in (1,2,3)
and war.Remaining_Hours > 0

is what I originally had but for some reason I had problems compiling that as well.As for the original author no it's not me and trying to get reason as to why they are writing the (250+)views in sql server this way is impossible.The original creators cannot actually explain or give reason why they are doing this so I am left to figure out their work and then try and fix it as I rewrite it into oracle.
Hope you can understand my situation and why there is some confusion.

Tom Kyte
September 21, 2002 - 11:06 am UTC

did the above work for you or not? If not, supply the error code/message.

I guess it is because SQLServer is so incredibly easy that they don't even need to learn SQL and understand what they are doing. The software just must intuitively know what they want and give it to them ;) (totally tongue in cheek there)

That was not my final solution however, I looked at this more and it turns out that more of the outer joins are an utter waste (I suppose they didn't really understand the question they were asking, just some syntax).

If the outer join to WOR happens, WOS.* will be null.
If the outer join to WOA happnes, WOR will be null and outer joined to and WOS.* will be null.


So, you should find that:

WHERE cnt.pkey(+) = war.craft
and war.resources = c.pkey (+)
and woa.pkey = war.work_order_asset_procedure
and wor.pkey = woa.work_order
and wor.status = wos.pkey
and wos.category in (1,2,3)
and war.Remaining_Hours > 0

is what you need. No outer joins to WOA, WOR, WOS -- for if they actually happen the WHERE clause will remove them!



how left outer join work

lizhuohua, November 23, 2003 - 3:35 am UTC

hi,tom
look this
create table t1(a int,b int);
create table t2(a int,b int);
insert into t1 values(1,2);
insert into t1 values(2,2);
insert into t1 values(3,2);
insert into t1 values(4,2);
insert into t2 values(1,1);
insert into t2 values(2,1);
insert into t2 values(3,2);
insert into t2 values(4,2);


select *
from t1 left outer join t2 on t1.a=t2.b and (t2.b=1 or t2.b=2)
where t2.b is not null;
a b a b
------------------------------------
1 2 1 1
1 2 2 1
2 2 3 2
2 2 4 2
3 2
4 2




select *
from t1 left outer join t2 on t1.a=t2.b and (t2.b=1 or t2.b=2)
where t2.b is null;
a b a b
-----------------------------------
1 2
2 2
3 2
4 2

this is right:
select *
from t1 left outer join (select * from t2 where (t2.b =1 or t2.b=2)) t on t1.a=t.b
where t.a is null;

A B A B
---------- ---------- ---------- ----------
4 2
3 2
could you explain to me how the outer join work.

(
Actually t2 maybe has lots of other data,
so I use (t2.b=1 or t2.b=2)
)
(sorry!My english is very poor ,I hope you can understand me)

Tom Kyte
November 23, 2003 - 8:45 am UTC

I do NOT get the results you do in 9.2.  I get very different answers (cut and pastes from sqlplus are always appreciated).  In 9iR1, there does seem to be an issue with these queries (recommend 9iR2 for sure, at the very least, contact support please)

conceptually  T1 left outer join T2 on (any condition)
will be processed using the following logic. below are the outputs I recieve from your queries and the procedural code that emulates the behaviour


ops$tkyte@ORA920> select *
  2  from t1 left outer join t2 on t1.a=t2.b and (t2.b=1 or t2.b=2)
  3  where t2.b is not null;
 
         A          B          A          B
---------- ---------- ---------- ----------
         1          2          1          1
         1          2          2          1
         2          2          3          2
         2          2          4          2
 
ops$tkyte@ORA920>
ops$tkyte@ORA920> declare
  2      l_found boolean := false;
  3      l_made_up  t2%rowtype;
  4  begin
  5      for x in ( select * from t1 )
  6      loop
  7          l_found := FALSE;
  8          for y in ( select *
  9                       from t2
 10                      where x.a=t2.b
 11                        and (t2.b=1 or t2.b=2) )
 12          loop
 13              l_found := true;
 14              if ( y.b is not null )
 15              then
 16                  dbms_output.put_line
 17                  ( 'joined ' || x.a ||','||x.b||','||
 18                                 y.a||','||y.b );
 19              end if;
 20          end loop;
 21          if ( NOT l_found )
 22          then
 23              if ( l_made_up.b is not null )
 24              then
 25                  dbms_output.put_line
 26                  ( 'made up ' ||
 27                     x.a ||','||x.b||','||
 28                     l_made_up.a||','||l_made_up.b );
 29              end if;
 30          end if;
 31      end loop;
 32  end;
 33  /
joined 1,2,1,1
joined 1,2,2,1
joined 2,2,3,2
joined 2,2,4,2
 
PL/SQL procedure successfully completed.


ops$tkyte@ORA920> select *
  2  from t1 left outer join t2 on t1.a=t2.b and (t2.b=1 or t2.b=2)
  3  where t2.b is null;
 
         A          B          A          B
---------- ---------- ---------- ----------
         3          2
         4          2
 
ops$tkyte@ORA920>
ops$tkyte@ORA920> declare
  2      l_found boolean := false;
  3      l_made_up  t2%rowtype;
  4  begin
  5      for x in ( select * from t1 )
  6      loop
  7          l_found := FALSE;
  8          for y in ( select *
  9                       from t2
 10                      where x.a=t2.b
 11                        and (t2.b=1 or t2.b=2) )
 12          loop
 13              l_found := true;
 14              if ( y.b is null )
 15              then
 16                  dbms_output.put_line
 17                  ( 'joined ' || x.a ||','||x.b||','||
 18                                 y.a||','||y.b );
 19              end if;
 20          end loop;
 21          if ( NOT l_found )
 22          then
 23              if ( l_made_up.b is null )
 24              then
 25                  dbms_output.put_line
 26                  ( 'made up ' ||
 27                     x.a ||','||x.b||','||
 28                     l_made_up.a||','||l_made_up.b );
 29              end if;
 30          end if;
 31      end loop;
 32  end;
 33  /
made up 3,2,,
made up 4,2,,
 
PL/SQL procedure successfully completed.


ops$tkyte@ORA920> select *
  2  from t1 left outer join (select * from t2 where (t2.b =1   or t2.b=2)) t on  3  t1.a=t.b
  4  where t.a is null;
 
         A          B          A          B
---------- ---------- ---------- ----------
         4          2
         3          2
 
ops$tkyte@ORA920>
ops$tkyte@ORA920> declare
  2      l_found boolean := false;
  3      l_made_up  t2%rowtype;
  4  begin
  5      for x in ( select * from t1 )
  6      loop
  7          l_found := FALSE;
  8          for y in ( select *
  9                       from (select * from t2 where (b=1 or b=2)) t2
 10                      where x.a=t2.b )
 11          loop
 12              l_found := true;
 13              if ( y.a is null )
 14              then
 15                  dbms_output.put_line
 16                  ( 'joined ' || x.a ||','||x.b||','||
 17                                 y.a||','||y.b );
 18              end if;
 19          end loop;
 20          if ( NOT l_found )
 21          then
 22              if ( l_made_up.a is null )
 23              then
 24                  dbms_output.put_line
 25                  ( 'made up ' ||
 26                     x.a ||','||x.b||','||
 27                     l_made_up.a||','||l_made_up.b );
 28              end if;
 29          end if;
 30      end loop;
 31  end;
 32  /
made up 3,2,,
made up 4,2,,
 
PL/SQL procedure successfully completed.

 

Thx

lizhuohua, November 24, 2003 - 6:50 am UTC

Tom,
Thanks for the review.
I try it again,and i get this:

SQL*Plus: Release 9.2.0.1.0 - Production on 星期一 11月 24 19:10:12 2003

Copyright (c) 1982, 2002, Oracle Corporation. All rights reserved.


连接到:
Oracle9i Enterprise Edition Release 9.2.0.1.0 - Production
With the Partitioning, OLAP and Oracle Data Mining options
JServer Release 9.2.0.1.0 - Production

lizh@lizhdb>select * from t1;

A B
---------- ----------
1 2
2 2
3 2
4 2

lizh@lizhdb>select * from t2;

A B
---------- ----------
1 1
2 1
3 2
4 2

lizh@lizhdb>select *
2 from t1 left outer join t2 on t1.a=t2.b and (t2.b=1 or t2.b=2)
3 where t2.b is not null;

A B A B
---------- ---------- ---------- ----------
1 2 1 1
1 2 2 1
2 2 3 2
2 2 4 2
3 2
4 2

lizh@lizhdb>select *
2 from t1 left outer join t2 on t1.a=t2.b and (t2.b=1 or t2.b=2)
3 where t2.b is null;

A B A B
---------- ---------- ---------- ----------
1 2
2 2
3 2
4 2
lizh@lizhdb>select *
2 from t1 left outer join t2 on t1.a=t2.b and t2.b in (1,2)
3 where t2.b is null;

A B A B
---------- ---------- ---------- ----------
1 2
2 2
3 2
4 2




If IN or OR in the condition the result is wrong.
I try it on another machine(Release 9.2.0.1.0),I get the same results.
(I try that procedure ,I get same results as you.)

Tom Kyte
November 24, 2003 - 8:55 am UTC

please contact support -- also, it does not reproduce in 9204 - the current patch set for 92

ops$tkyte@ORA920PC> select *
  2    from t1 left outer join t2 on t1.a=t2.b and (t2.b=1 or t2.b=2)
  3    where t2.b is not null;
 
         A          B          A          B
---------- ---------- ---------- ----------
         1          2          1          1
         1          2          2          1
         2          2          3          2
         2          2          4          2
 
ops$tkyte@ORA920PC>
ops$tkyte@ORA920PC> select *
  2    from t1 left outer join t2 on t1.a=t2.b and (t2.b=1 or t2.b=2)
  3    where t2.b is null;
 
         A          B          A          B
---------- ---------- ---------- ----------
         3          2
         4          2
 
ops$tkyte@ORA920PC>
ops$tkyte@ORA920PC> select *
  2    from t1 left outer join t2 on t1.a=t2.b and t2.b in (1,2)
  3   where t2.b is null;
 
         A          B          A          B
---------- ---------- ---------- ----------
         3          2
         4          2
 
ops$tkyte@ORA920PC> select * from v$version;
 
BANNER
----------------------------------------------------------------
Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production
PL/SQL Release 9.2.0.4.0 - Production
CORE    9.2.0.3.0       Production
TNS for Linux: Version 9.2.0.4.0 - Production
NLSRTL Version 9.2.0.4.0 - Production
 
 

:-)

lizhuohua, November 24, 2003 - 6:53 am UTC

星期一 11月 is Chinese.

Thank you.

lizhuohua, November 25, 2003 - 6:05 am UTC

tom,
Thank you very much.:-)

View with Outer Join

Sami, February 04, 2004 - 9:28 am UTC

Dear Tom

My understanding is that "Step(1) = Step(2)+Step(3)"

Step(1) returns 24 records.
Step(2)+Step(3) returns 0 records.

Please enlighten me if I am wrong?


STEP (1)
========
SELECT T1.USERSTATUSID,
NVL(T21.DESCRIPTION,T22.DESCRIPTION) USERSTATUSDESC,
NVL(T21.TOKENVALUE,T22.TOKENVALUE) USERSTATUSABBREV,
T1.HSBCSERVICEID,
T1.ACTIVEFLAG,
T21.LANGUAGEID LANGUAGEID1,
T22.LANGUAGEID LANGUAGEID2,
T1.UPDATEDATE,
T1.CREATEDATE
FROM USERSTATUS T1,
crmf.NLS_TOKEN_DETAILS T21,
crmf.NLS_TOKEN_DETAILS T22
WHERE T1.TOKENID=T21.TOKENID(+)
AND T1.TOKENID=T22.TOKENID
AND T22.LANGUAGEID=30
AND T21.languageid(+)=1;

24 rows returned. <=========********

STEP (2)
========
CREATE OR REPLACE VIEW userstatus_v2 AS
SELECT T1.USERSTATUSID,
NVL(T21.DESCRIPTION,T22.DESCRIPTION) USERSTATUSDESC,
NVL(T21.TOKENVALUE,T22.TOKENVALUE) USERSTATUSABBREV,
T1.HSBCSERVICEID,
T1.ACTIVEFLAG,
T21.LANGUAGEID LANGUAGEID1,
T22.LANGUAGEID LANGUAGEID2,
T1.UPDATEDATE,
T1.CREATEDATE
FROM USERSTATUS T1,
crmf.NLS_TOKEN_DETAILS T21,
crmf.NLS_TOKEN_DETAILS T22
WHERE T1.TOKENID=T21.TOKENID(+)
AND T1.TOKENID=T22.TOKENID
AND T22.LANGUAGEID=30;

View created.

STEP (3)
========
SELECT * FROM userstatus_v2
WHERE languageid1(+)=1;
no rows returned. <==========*******




Tom Kyte
February 04, 2004 - 5:04 pm UTC

you need to outer join to "something"  - there is no "set" to outer join to here.

Just like this:

ops$tkyte@ORA10G> l
  1* select * from dual where dummy(+) = 'y'
ops$tkyte@ORA10G> /
 
no rows selected
 
ops$tkyte@ORA10G> select * from dual, (select 'y' xxx from dual) where dummy(+) = xxx;
 
D X
- -
  y


if you query

select * from userstatus_v2, (select 1 xxx from dual)
where languageid1(+) = xxx;


then you will get 1 row back (the xxx value) unless there are languageid1 = 1 values. 

Question

Vidya, June 28, 2004 - 11:55 am UTC

Hi Tom,

I have 2 tables with the following structures

create table bio
(
bio_id number,
dem_id number,
portrait blob,
signature blob,
file_fmt char(1)
);

create table bio1
(
bio_id number,
portrait blob,
signature blob
);

I have created a view from the table bio as

create view bioview as select * from bio;

Later I found out that some portrait and signature values in bio can be null and their file_fmt is set to 'R'. Also those particular bio_ids have valid portrait and signature values in bio1 table. So how do I go about creating the view such that I get all the portrait and signature values from both the tables without getting any null values. Can I use join views for this? Thanks for your help.

Thanks,
Vidya.

Tom Kyte
June 28, 2004 - 1:10 pm UTC

a join will do, outer join if not every row in bio has a match in bio1.

scalar subqueries could work as well


ops$tkyte@ORA9IR2> create table t1 ( x int, y clob );
 
Table created.
 
ops$tkyte@ORA9IR2> create table t2 ( x int, y clob );
 
Table created.
 
ops$tkyte@ORA9IR2>
ops$tkyte@ORA9IR2> insert into t1 values ( 1, null );
 
1 row created.
 
ops$tkyte@ORA9IR2> insert into t1 values ( 2, 'hello' );
 
1 row created.
 
ops$tkyte@ORA9IR2> insert into t2 values ( 1, 'hey' );
 
1 row created.
 
ops$tkyte@ORA9IR2>
ops$tkyte@ORA9IR2>
ops$tkyte@ORA9IR2> select x, nvl(y,(select y from t2 where t2.x = t1.x)) y
  2    from t1
  3  /
 
         X Y
---------- --------------------
         1 hey
         2 hello
 

More to Explore

PL/SQL demos

Check out more PL/SQL tutorials on our LiveSQL tool.

PL/SQL docs

PL/SQL reference manual from the Oracle documentation library