Inline Views And Outer
Joins
I had an interesting
question put to me at work
yesterday, where a colleague
was having problems with an
outer join in an SQL
statement.
My collegue was putting
together as system that
recorded bids for contracts,
storing the userid of the
system operator, the details
of the prospective client,
and details of all the
company's sites, including
their billing address. A
query was being put together
that returned the company
address, the details of the
bid, and the billing
address, leaving the address
bit blank if one hadn't yet
been supplied.
The query used four tables,
BID, LOCATION, USERS and
ADDRESS, with BID joined to
LOCATION by LOCATION_ID and
to USER by USER_ID, and
LOCATION joined to ADDRESS
by LOCATION_ID.
The SQL looked like this;
select
BID.COMPANY_NAME,
BID.LOCATION_NAME,
TO_CHAR(BID.START_DATE, 'dd
mon yyyy') || ' to ' ||
TO_CHAR(BID.END_DATE, 'dd
mon yyyy') as BID_PERIOD,
LOCATION.BRN,
USERS.TITLE || ' ' ||
USERS.FORENAME || ' ' ||
USERS.SURNAME as USER_NAME,
ADDRESS.CONTACT_TITLE,
ADDRESS.CONTACT_FORENAME,
ADDRESS.CONTACT_SURNAME,
ADDRESS.POSITION,
ADDRESS.ADDRESS_LINE_1,
ADDRESS.ADDRESS_LINE_2,
ADDRESS.TOWN,
ADDRESS.COUNTY,
ADDRESS.POSTCODE,
ADDRESS.EMAIL,
ADDRESS.TELEPHONE,
ADDRESS.FAX
from
ADDRESS, LOCATION, BID,
USERS
where
BID.LOCATION =
LOCATION.LOCATION_ID
and
BID.USER_ID = USERS.USER_ID
and
LOCATION.LOCATION_ID =
ADDRESS.LOCATION_ID
and
BID.BID_ID = 'Q0011DJR'
and
ADDRESS.ADDRESS_TYPE_ID = 3;
As a particular location
could have more than one
ADDRESS_TYPE_ID (billing
address, postal address and
so on) the ADDRESS_TYPE_ID
field was specified to make
sure only the billing
address was returned. The
output of the query was then
a single row, with details
of the bid, location, user
and the billing address. The
problem came when there was
no ADDRESS_TYPE_ID = 3 (i.e.
billing address) in the
ADDRESS table for that
particular location, which
would lead to no rows being
returned by the query. What
was really required was
that, in this situation, the
query could be tweaked such
that the location, bid and
user details would still be
returned, with the address
details being left blank.
To achieve this, a left
outer join was tried on the
LOCATION.LOCATION_ID =
ADDRESS.LOCATION_ID
predicate, in the hope that
this would lead to the
location, bid and user
details being returned even
when no address table rows
were retrieved. The
resulting SQL statement
looked like this (changes
highlighted in red);
select
BID.COMPANY_NAME,
BID.LOCATION_NAME,
TO_CHAR(BID.START_DATE, 'dd
mon yyyy') || ' to ' ||
TO_CHAR(BID.END_DATE, 'dd
mon yyyy') as BID_PERIOD,
LOCATION.BRN,
USERS.TITLE || ' ' ||
USERS.FORENAME || ' ' ||
USERS.SURNAME as USER_NAME,
ADDRESS.CONTACT_TITLE,
ADDRESS.CONTACT_FORENAME,
ADDRESS.CONTACT_SURNAME,
ADDRESS.POSITION,
ADDRESS.ADDRESS_LINE_1,
ADDRESS.ADDRESS_LINE_2,
ADDRESS.TOWN,
ADDRESS.COUNTY,
ADDRESS.POSTCODE,
ADDRESS.EMAIL,
ADDRESS.TELEPHONE,
ADDRESS.FAX
from
ADDRESS, LOCATION, BID,
USERS
where
BID.LOCATION =
LOCATION.LOCATION_ID
and
BID.USER_ID = USERS.USER_ID
and
LOCATION.LOCATION_ID =
ADDRESS.LOCATION_ID (+)
and
BID.BID_ID = 'Q0011DJR'
and
ADDRESS.ADDRESS_TYPE_ID = 3;
However, the query still
resulted in no rows being
returned.
After having looked at the
query, it became clear that
the left outer join was in
this instance a 'red
herring' - even though all
of the rows from the
LOCATION table were being
returned in the query, the
query itself was still
returning no rows, because
of the
ADDRESS.ADDRESS_TYPE_ID = 3
predicate later in the
query. I had a talk through
with
Jon Mead and we came up
with the following solution.
What was required was a way
of separating out the
ADDRESS part of the query
into an inline view,
retrieving only those rows
with an ADDRESS_TYPE_ID = 3,
and then left outer joining
this inline view to the rest
of the query. An inline view
is a subquery in the FROM
clause of an outer query,
which is then aliased and
dealt with as if it was
another table or view.
Inline views are evaluated
at runtime, and unlike
normal views are not stored
in the data dictionary;
they're effectively named
subqueries that derive their
rows at run-time during the
execution of the outer
query.
Using the inline view, the
SELECT statement that we
came up with was as follows;
select
BID.COMPANY_NAME,
BID.LOCATION_NAME,
TO_CHAR(BID.START_DATE, 'dd
mon yyyy') || ' to ' ||
TO_CHAR(BID.END_DATE, 'dd
mon yyyy') as BID_PERIOD,
LOCATION.CRN,
USERS.TITLE || ' ' ||
USERS.FORENAME || ' ' ||
USERS.SURNAME as USER_NAME,
ADDRESS.CONTACT_TITLE,
ADDRESS.CONTACT_FORENAME,
ADDRESS.CONTACT_SURNAME,
ADDRESS.POSITION,
ADDRESS.ADDRESS_LINE_1,
ADDRESS.ADDRESS_LINE_2,
ADDRESS.TOWN,
ADDRESS.COUNTY,
ADDRESS.POSTCODE,
ADDRESS.EMAIL,
ADDRESS.TELEPHONE,
ADDRESS.FAX,
from
(
SELECT ADDR.CONTACT_TITLE,
ADDR.CONTACT_FORENAME,
ADDR.CONTACT_SURNAME,
ADDR.POSITION,
ADDR.ADDRESS_LINE_1,
ADDR.ADDRESS_LINE_2,
ADDR.TOWN,
ADDR.COUNTY,
ADDR.POSTCODE,
ADDR.EMAIL,
ADDR.TELEPHONE,
ADDR.FAX,
ADDR.LOCATION_ID,
ADDR.ADDRESS_TYPE_ID
FROM ADDRESS ADDR
where ADDR.ADDRESS_TYPE_ID =
3
) ADDRESS, LOCATION, BID,
USERS
where
BID.LOCATION =
LOCATION.LOCATION_ID
and
BID.USER_ID = USERS.USER_ID
and
LOCATION.LOCATION_ID =
ADDRESS.LOCATION_ID (+)
and
BID.BID_ID = 'Q0011DJR'
The inline view is in red.
What's happening here is
that, instead of referencing
the ADDRESS table in the
FROM clause as normal, and
then trying to limit down
the resultset later on with
the ADDRESS.ADDRESS_TYPE_ID
= 3, the inline view
retrieves all addresses of
address_type_id = 3, which
is then left outer joined to
the LOCATION table later on.
Therefore, if there isn't an
address in the inline view
that has the correct
LOCATION_ID, the left out
join comes into play and
returns all LOCATIONs as
required. The
ADDRESS.ADDRESS_TYPE_ID = 3
predicate can then be
removed from the end of the
query, and then, if an
ADDRESS_TYPE_ID of 3 is not
present for a bid, the
columns from the LOCATION,
BID and USER tables are
still returned in the output
row.
Having thought this through,
I thought it was a good
example of an inline view in
action, as as doing a
google search on inline
views didn't come up
with much useful
information, I thought it
worth jotting this one down.
Let me know if there's a
better way to achieve this
result.
UPDATE:
Yves Bergeron mentions
another solution that
adds another left outer join
and avoids the need for the
inline view. I'll try it
out tomorrow and report
back.
FURTHER UPDATE: This
article has been linked to
on OTN and I've had a few
useful emails with
additional advice. This one
comes from Natti Bar-On from
Oracle...
Hello Mark,
This is an email I've sent
to a customer of mine a few
weeks ago. Attaching it as a
whole and hope it might
help. Nice seeing you on
OTN...
:)
BTW, there were problems
with outer joins and
inline-view-optimization in
8i (mostly VPD related), but
9i more or less solved
those.
Regards,
Natti Bar-On,
Sr. Technology Specialist,
Oracle Israel.
--------------------------------------------------------------
Outer joins have two ways of
being written in Oracle.
let's assume we have two
tables:
A
a1 num a2 num
1
1
2
2
3
3
B
b1 num b2 num
1
1
2
2
4
4
B_TAG
b1 num b2 varchar2
1
hello
2
goodbye
we can write an outer join
like this
--FORM 1
select a1,a2,b1,b2
from a,b
where a1 = b1 (+)
A1
A2
B1
B2
---------- ----------
---------- ----------
1
1
1
1
2
2
2
2
3
3
or we can write it like this
--FORM 2
select a1,a2,b1,b2
from a left outer join b on
a1=b1
A1
A2
B1
B2
---------- ----------
---------- ----------
1
1
1
1
2
2
2
2
3
3
the latter was introduced in
9i and has better
functionality.
How do we perform filtering
on rows?
There are two kinds of
filters -
A) we want to reduce the
number of rows before the
outer join
this won't ruin the outer
join
B) we want to reduce the
number of rows after the
outer join -
this will probably ruin the
outer join...
this is the problem you
encounter...
What does the syntax looks
like when we want to filter
without losing the outer
join?
form 1: Syntax for operators
:
= Equals
select a1,a2,b1,b2
from a,b
where a1 = b1 (+)
and b2 (+)= 3;
A1
A2
B1
B2
---------- ----------
---------- ----------
1
1
2
2
3
3
> < greater than / less
than
select a1,a2,b1,b2
from a,b
where a1 = b1 (+)
and b2 (+)> 1
A1
A2
B1
B2
---------- ----------
---------- ----------
1
1
2
2
2
2
3
3
between
select a1,a2,b1,b2
from a,b
where a1 = b1 (+)
and b2 (+) between 1 and 5
A1
A2
B1
B2
---------- ----------
---------- ----------
1
1
1
1
2
2
2
2
3
3
is null / is not null
select a1,a2,b1,b2
from a,b
where a1 = b1 (+)
and b2 (+) is not null
A1
A2
B1
B2
---------- ----------
---------- ----------
1
1
1
1
2
2
2
2
3
3
like
select a1,a2,b1,b2
from a,b_tag
where a1 = b1 (+)
and b2 (+) like 'hel%'
A1
A2
B1
B2
---------- ----------
---------- ----------
1
1
1 hello
2
2
3
3
in
no way of represnting IN in
form 1 outer joins.
FORM 2
Equals
select a1,a2,b1,b2
from a left outer join b on
a1=b1
and b2 = 2
-- No where clause! the
condition is an attribute of
the outer join!
< >
select a1,a2,b1,b2
from a left outer join b on
a1=b1
and b2 > 2
between
select a1,a2,b1,b2
from a left outer join b on
a1=b1
and b2 between 0 and 2
like
select a1,a2,b1,b2
from a left outer join
b_tag on a1=b1
and b2 like 'hell%'
is null
select a1,a2,b1,b2
from a left outer join
b_tag on a1=b1
and b2 is null
in
select a1,a2,b1,b2
from a left outer join b on
a1=b1
and b2 in (0,1)
A1
A2
B1
B2
---------- ----------
---------- ----------
1
1
1
1
2
2
3
3
So far for the outer join.
Both forms are ANSI STANDARD
FOR SQL, and Oracle supports
both (not all vendors
support both)."