Left Outer join with same table as part of the outer join - sql

I was wondering how I could go by writing a the outer join query to get the required outputs (described below)
where the tables I am outer joining are part of the other join conditions in the statement
given the following datastructure where
- Table A is the main table containing some arbitrary objects
- Table B is referenced by A where A.TYPE_ID = B.ID
- Table C defininfs relations between the objects in Table A where C.SOURCE_ID references A.ID and C.TARGET_ID references A.ID
This is how the schema is defined and I can't do anything about it (it's a legacy system)
TABLE_A
---------------------------
| ID | TYPE_ID | Name |
|-------------------------|
| 1 | 1 | Name 1 |
| 2 | 2 | Name 2 |
| 3 | 1 | Name 3 |
| 4 | 1 | Name 4 |
| 5 | 3 | Name 5 |
|-------------------------|
TABLE_B
----------------------
| ID | TYPE_NAME |
|--------------------|
| 1 | Type 1 |
| 2 | Type 2 |
| 3 | Type 3 |
| 4 | Type 4 |
|--------------------|
TABLE_C
-------------------------------
| PK | SOURCE_ID | TARGET_ID |
|-----------------------------|
| 11 | 2 | 1 |
| 12 | 2 | 3 |
| 13 | 5 | 1 |
| 13 | 5 | 4 |
-------------------------------
What I would like to get is all the objects in Table A of "Type 1" with the name of the object they are associated to (null otherwise) which are of Type 2,
i.e an outer join to get all the objects of Type 1 regardless if they have an association, but if they do then I need the name of the object.
Note that objects of Type 1 will always been in the TARGET in the relstionship.
The output for the above example would be
-------------------------------
| Target Name | Source Name |
|-----------------------------|
| Name 1 | Name 2 |
| Name 3 | Name 2 |
| Name 4 | (NULL) |
|-----------------------------|
My original join query (couldn't get the outer join to work) this is the normal join not showing objects with no associations.
select atrgt.NAME, asrc.NAME
from TABLE_A atrgt
JOIN TABLE_B trgttype on atrgt.TYPE_ID = trgttype.ID
and trgttype.TYPE_NAME = 'Type 1'
JOIN TABLE_C assoc on atrgt.ID = assoc.TARGET_ID
JOIN TABLE_A asrc on asrc.ID = assoc.SOURCE_ID
JOIN TABLE_B srctype on asrc.TYPE_ID = srctype.ID
and srctype.TYPE_NAME = 'Type 2'

Basically in these situations I think the best approach is to subdivide the query into two normal joins, then do the outer join between those results sets. If you think of SQL as procedural code, you may think it looks inefficient, but the query optimizer will not necessarily run the two subjoins independently.
You didn't say what RDBMS you are using. In Oracle I would probably write it like this:
with
src_type_2 as (
select c.target_id, a.name
from table_c c
join table_a on a.id = c.source_id
join table_b on b.id = a.type_id
where b.type_name = 'Type 2'
),
all_type_1 as (
select a.id, a.name
from table_a a
join table_b on b.id = a.type_id
where b.type_name = 'Type 1'
)
select tgt.name, src.name
from all_type_1 tgt
left join src_type_2 src on src.target_id = tgt.id

Try
select atrgt.NAME, baseview.NAME
from TABLE_A atrgt
JOIN TABLE_B trgttype on atrgt.TYPE_ID = trgttype.ID
and trgttype.TYPE_NAME = 'Type 1'
JOIN TABLE_C assoc on atrgt.ID = assoc.TARGET_ID
LEFT JOIN (
TABLE_A asrc on asrc.ID = assoc.SOURCE_ID
JOIN TABLE_B srctype on asrc.TYPE_ID = srctype.ID
and srctype.TYPE_NAME = 'Type 2'
) as baseview

I think this should work:
SELECT
TGT.NAME, SRC_TYPE.TYPE_NAME
FROM TABLE_A TGT
JOIN TABLE_B TGT_TYPE ON TGT.TYPE_ID = TGT_TYPE.ID
LEFT JOIN TABLE_C REL ON TGT.ID = REL.TARGET_ID
LEFT JOIN TABLE_A SRC ON REL.SOURCE_ID = SRC.ID
LEFT JOIN TABLE_B SRC_TYPE ON SRC_TYPE.ID = SRC.TYPE_ID
WHERE TGT_TYPE.TYPE_NAME = 'Type 1' AND COALESCE(SRC_TYPE.TYPE_NAME, 'Type 2') = 'Type 2'
If you're using Oracle, you could replace the COALESCE with NVL(SRC_TYPE.TYPE_NAME, 'Type 2').

Related

Get left table data completely even when there is no reference in right joined table

Database used: SQL Server
I have three tables A,B,C.
TABLE A:
------------------
| ID | Name |
------------------
| 1 | X |
------------------
| 2 | Y |
------------------
TABLE B:
----------------------
| ID | Date |
----------------------
| 1 | 2019-11-06 |
----------------------
| 2 | 2019-11-05 |
----------------------
TABLE C:
----------------------------------
| ID | B.ID | A.ID | Amount |
----------------------------------
| 1 | 1 | 1 | 500 |
----------------------------------
| 2 | 2 | 2 | 1000 |
----------------------------------
The result I would like to get is all entries of table A.Name with their amount in table C.amount where table B.Date = 2019-11-06. The result set should include all A.name entries even it have no reference in Table C.
Required result is:
-----------------------
| A.Name | C.Amount |
-----------------------
| X | 500 |
-----------------------
| Y | NULL |
-----------------------
Code I tried with :
SELECT A.Name,C.Amount
FROM A
LEFT OUTER JOIN C ON C.A_ID=A.ID
LEFT OUTER JOIN B ON B.ID = C.B_ID ON
WHERE B.Date='2019-11-06'
The result I obtained with above code is :
------------------
| Name | Amount |
------------------
| X | 500 |
------------------
There is no Y in the result, its because there is no entry for Y on that particular date. I just want to show Y and amount as null or zero.
SQL Fiddle with my query
Please help me with this.
There's is no relationship between your A and B, so we need to group B and C using a subquery to filter with date before doing the left join.
SELECT A.Name, t1.Amount
FROM A
LEFT JOIN
(SELECT C.A_ID, C.Amount FROM C
INNER JOIN B ON B.ID = C.B_ID
WHERE B.Date='2019-11-06') t1
ON t1.A_ID=A.ID
see dbfiddle
Try this-
Fiddle Here
SELECT A.Name,C.Amount
FROM A
LEFT JOIN B ON A.ID = B.ID AND B.Date = '2019-11-06'
LEFT JOIN C ON B.ID = C.ID
Output is-
Name Amount
X 500
Y (null)

Transform Oracle sql syntax

I would like to change the syntax of an Oracl SQL Query,
The original query contains a join:
WHERE (A.ID = B.ID OR A.ID is null)
I would like to change this to LEFT OUTER JOIN or INNER JOIN syntax,
I tried the following but the query doesn't return the same results:
LEFT OUTER JOIN TABLEA A ON A.ID = B.ID
As an outsider, I can't say for sure, but I strongly suspect that the person who wrote
WHERE (A.ID = B.ID OR A.ID is null)
was trying to do an outer join and just got it completely wrong. That condition is not logically equivalent to an outer join.
Consider:
with a ( id, val ) as (
SELECT 1, 'a one' FROM DUAL UNION ALL
SELECT 2, 'a two' FROM DUAL UNION ALL
SELECT 3, 'a three' FROM DUAL UNION ALL
SELECT null, 'a null' FROM DUAL ),
b ( id, val ) as (
SELECT 2, 'b two' FROM DUAL UNION ALL
SELECT 3, 'b three' FROM DUAL UNION ALL
SELECT 4, 'b four' FROM DUAL UNION ALL
SELECT null, 'a null' FROM DUAL )
select a.id aid, a.val aval, b.id bid, b.val bval
from a, b
where ( a.id = b.id or a.id is null )
+-----+---------+-----+---------+
| AID | AVAL | BID | BVAL |
+-----+---------+-----+---------+
| 2 | a two | 2 | b two |
| 3 | a three | 3 | b three |
| | a null | 2 | b two |
| | a null | 3 | b three |
| | a null | 4 | b four |
| | a null | | a null |
+-----+---------+-----+---------+
... you can see the null value for a.id got joined to every row in table b. If there were no row in table a with a null id, there would have been no row having b.id = 4 (only the 1st two records above would have been returned).
An outer join of the same data should look like this:
select a.id aid, a.val aval, b.id bid, b.val bval from b left join a
on a.id = b.id order by a.id, b.id
+-----+---------+-----+---------+
| AID | AVAL | BID | BVAL |
+-----+---------+-----+---------+
| 2 | a two | 2 | b two |
| 3 | a three | 3 | b three |
| | | 4 | b four |
| | | | a null |
+-----+---------+-----+---------+
My advice: understand what the query is supposed to do. If the requirements of the query are to outer join, fix the query to be an outer join and ignore what it used to do -- it was wrong.

Showing the full list IDs when OUTER JOIN two tables

How to display not only the overlapped IDs from two tables (table A and table B) but also the unique IDs from two tables?
Here is the example code:
SELECT A.ID AS ID
FROM A
FULL OUTER JOIN B ON A.ID = B.ID
I think there's something I can do with the SELECT line but I don't know how.
coalesce() returns the first non-null value from a set of parameters. Is that what you are looking for?
rextester demo: http://rextester.com/QYAZV8300
create table a (id int)
insert into a values (1),(3),(5)
create table b (id int)
insert into b values (2),(3),(4)
select
a.id as A_Id
, b.id as B_Id
, coalesce(a.id,b.id) as Id
from a
full join b
on a.id = b.id
returns:
+------+------+----+
| A_Id | B_Id | Id |
+------+------+----+
| 1 | NULL | 1 |
| 3 | 3 | 3 |
| 5 | NULL | 5 |
| NULL | 2 | 2 |
| NULL | 4 | 4 |
+------+------+----+

Join table 1 to either column 1 or 2 from table 2 without duplicates

[MS SQL 2008]
I have tables (all columns are string names):
A: two columns relating some datafield to an owning entity
B: three columns defining a hierarchy of entities
I need to create a singe table of the whole hierarchy (including all rows not existing in both tables), but the key column in table A (shown as Acol2) can be in either column 1 or 2 of table B...
A: B:
Acol1 | Acol2 Bcol1 | Bcol2 | Bcol3
-------+------ --------+-------+------
A | B B | X | Y
C | D Q | X | Y
E | F H | D | Z
G | H W | V | U
The output should be
Hierarchy:
Acol1 | Bcol1 | Bcol2 | Bcol3
-------+-------+-------+------
A | B | X | Y
Null | Q | X | Y
C | Null | D | Z
G | H | D | Z
E | Null | Null | Null
Null | W | V | U
Logic (also added to original):
If A has no record in B, show A with all Null
If A has record in Bcol1, show A with full row B
If A has record in Bcol2, show A with Null, Bcol2, Bcol3
If B has no record in A, show B with Null for Acol1
I have tried all sorts of UNIONs of two separate JOINs, but can't seem to get rid of extraneous rows...
B LEFT JOIN A ON Acol2=Bcol1 UNION B LEFT JOIN A ON Acol2=Bcol2;
gives duplicate rows, as the second part of the union has to set Bcol1 to NULL
(perhaps one solution is a way to remove this duplicate NULL row?)
B INNER JOIN A ON Acol2=Bcol1 UNION B INNER JOIN A ON Acol2=Bcol2;
Obviously removes all the rows from A and B that have no shared keys
(solution as to easy way to regain just those rows?)
Any idea appreciated!
To play:
[SQL removed - see fiddle in reply comments]
SELECT
Table1.ACol1,
CASE WHEN Table1.ACol1 = Table2.BCol1 THEN Table2.BCol1 ELSE NULL END AS BCol1
Table2.BCol2,
Table2.BCol3
FROM
Table1
FULL OUTER JOIN
Table2
ON Table1.ACol2 IN (Table2.BCol1, Table2.BCol2)
When you say no duplicates, this is only possible if ACol2 only ever appears in one field of one row in Table2. If it appears in multiple places, you'll get duplication.
- If that's possible, how would you want to chose which record from Table2?
Also, in general, however, this is a SQL-Anti-Pattern.
This is because the join would prefer an index on Table2. But, since you never know which field you're joining on, no single index will ever satsify the join condition.
EDIT:
What would make this significantly faster is to create a normalised TableB...
B_ID | B_Col | B_Val
------+-------+-------
1 | 1 | B
1 | 2 | X
1 | 3 | Y
2 | 1 | Q
2 | 2 | X
2 | 3 | Y
3 | 1 | H
3 | 2 | D
3 | 3 | Z
4 | 1 | W
4 | 2 | V
4 | 3 | U
Then index that table with (B_ID) and on (B_Val)...
Then include the B_ID field in the non_normalised table...
ID | Bcol1 | Bcol2 | Bcol3
------+-------+-------+-------
1 | B | X | Y
2 | Q | X | Y
3 | H | D | Z
4 | W | V | U
Then use the following query...
SELECT
Table1.ACol1,
CASE WHEN Table1.ACol1 = Table2.BCol1 THEN Table2.BCol1 ELSE NULL END AS BCol1
Table2.BCol2,
Table2.BCol3
FROM
(
Table1
LEFT JOIN
Table2Normalised
ON Table2Normalised.B_Val = Table1.ACol2
AND Table2Normalised.B_Col IN (1,2)
)
FULL OUTER JOIN
Table2
ON Table2Normalised.B_ID = Table2.ID
EDIT:
Without changing the schema, and instead having one index on BCol1 and a second index on Bcol2...
SELECT ACol1, BCol1, BCol2, BCol3 FROM Table1 a INNER JOIN Table2 b ON a.ACol2 = b.BCol1
UNION ALL
SELECT ACol1, NULL, BCol2, BCol3 FROM Table1 a INNER JOIN Table2 b ON a.ACol2 = b.BCol2
UNION ALL
SELECT ACol1, NULL, NULL, NULL FROM Table1 a WHERE NOT EXISTS (SELECT * FROM Table2 WHERE BCol1 = a.ACol2)
AND NOT EXISTS (SELECT * FROM Table2 WHERE BCol2 = a.ACol2)
UNION ALL
SELECT NULL, BCol1, BCol2, BCol3 FROM Table2 b WHERE NOT EXISTS (SELECT * FROM Table1 WHERE ACol2 = b.BCol1)
AND NOT EXISTS (SELECT * FROM Table1 WHERE ACol2 = b.BCol2)
But that's pretty messy...

Single SQL query to check if either table contains a row with column=x

I have 2 unrelated tables A and B which both have foreign key constraints on C. I need to run an sql query that determines if either A or B contain a given id of C. My first approach was to use the union all but A and B are not related thus it will not work.
Any ideas?
Select 1
From DUAL
Where Exists ( Select null From Table_A Where a.fk = :id ) OR
Exists ( Select null From Table_B Where b.fk = :id );
You could indeed use union, why not? But why use UNION ALL, and not just UNION? Just pick the one common column:
SELECT 1
FROM
(select A.fk from A inner join C on A.FK = C.pk
UNION
select B.fk from B inner join C on B.FK = C.pk) AS bothTables
WHERE fk = 'desiredValue';
This would work just nicely.
Tested it on the following tables in MySQL, with myValue = 1, just to verify.
mysql> select * from A;
+------+--------+------+
| pk | value | fk |
+------+--------+------+
| 1 | ape | 2 |
| 2 | fjfjfj | 3 |
+------+--------+------+
2 rows in set (0.00 sec)
mysql> select * from B;
+------+--------+------+
| pk | value | fk |
+------+--------+------+
| 1 | katt | 1 |
| 2 | fjfjfj | 3 |
+------+--------+------+
2 rows in set (0.00 sec)
mysql> select * from C;
+------+-------+
| pk | value |
+------+-------+
| 1 | hei |
| 2 | nei |
| 3 | jeg |
+------+-------+
3 rows in set (0.00 sec)
Not 100% sure what you're asking but if you mean to return the elements of A and B that match the id in C then this will do it.
Select c.*, a.*, b.*
From c.id
Left Outer Join a On a.id = c.id
Left Outer Join b On b.id = c.id
Where c.id = #somevalue and (a.id Is Not Null or b.id Is Not Null)
Where #somevalue is the value you're looking for.