SQL Join Types
Inner joins, multi-table joins, how a join is computed, and LEFT / RIGHT / FULL outer joins.
Contents
- Use a simple join to retrieve information from more than one table
- Understand the different join types: INNER JOIN, LEFT OUTER JOIN, RIGHT OUTER JOIN and FULL OUTER JOIN
Topics: simple join · three-table join · multiple grouping columns · left / right / full outer join. Reference: Connolly & Begg, Ch. 5.
Join types at a glance
| Join | Keeps | Unmatched columns |
|---|---|---|
| INNER (simple/equi-join) | Only rows that match in both tables | — |
| LEFT OUTER | All rows of the first (left) table + matches | Right-table columns filled with NULL |
| RIGHT OUTER | All rows of the second (right) table + matches | Left-table columns filled with NULL |
| FULL OUTER | All rows from both tables | Either side filled with NULL |
| CROSS | Every combination (Cartesian product) | — |
Simple (inner) join
List names of all clients who have viewed a property along with any comment supplied.
SELECT c.clientNo, fName, lName, propertyNo, comment
FROM Client c, Viewing v
WHERE c.clientNo = v.clientNo;
- Only rows from both tables that have identical values in the clientNo columns (
c.clientNo = v.clientNo) are included. - Equivalent to the equi-join in relational algebra.
| clientNo | fName | lName | propertyNo | comment |
|---|---|---|---|---|
| CR56 | Aline | Stewart | PG36 | |
| CR56 | Aline | Stewart | PA14 | too small |
| CR56 | Aline | Stewart | PG4 | |
| CR62 | Mary | Tregear | PA14 | no dining room |
| CR76 | John | Kay | PG4 | too remote |
Client CR74 (Mike Ritchie) has viewed nothing, so he does not appear — that’s the key limitation that outer joins solve.
Alternative JOIN constructs
FROM Client c JOIN Viewing v ON c.clientNo = v.clientNo -- explicit INNER JOIN
FROM Client JOIN Viewing USING (clientNo)
FROM Client NATURAL JOIN Viewing
In each case, the FROM replaces the original FROM and WHERE. The first (ON) produces a table with two identical clientNo columns.
ON— any condition; most flexible and the only one supported by SQL Server.USING (col)— join on same-named column(s), shown once.NATURAL JOIN— automatically joins on all same-named columns. Risky: adding a column with a common name (e.g.comment) silently changes the join.
JOIN on its own means INNER JOIN.
Sorting a join
For each branch, list numbers and names of staff who manage properties, and the properties they manage.
SELECT s.branchNo, s.staffNo, fName, lName, propertyNo
FROM Staff s, PropertyForRent p
WHERE s.staffNo = p.staffNo
ORDER BY s.branchNo, s.staffNo, propertyNo;
| branchNo | staffNo | fName | lName | propertyNo |
|---|---|---|---|---|
| B003 | SG14 | David | Ford | PG16 |
| B003 | SG37 | Ann | Beech | PG21 |
| B003 | SG37 | Ann | Beech | PG36 |
| B005 | SL41 | Julie | Lee | PL94 |
| B007 | SA9 | Mary | Howe | PA14 |
Three-table join
For each branch, list staff who manage properties, including the city in which the branch is located and the properties they manage.
SELECT b.branchNo, b.city, s.staffNo, fName, lName, propertyNo
FROM Branch b, Staff s, PropertyForRent p
WHERE b.branchNo = s.branchNo
AND s.staffNo = p.staffNo
ORDER BY b.branchNo, s.staffNo, propertyNo;
Alternative formulation for FROM and WHERE:
FROM (Branch b JOIN Staff s USING (branchNo)) AS bs
JOIN PropertyForRent p USING (staffNo)
| branchNo | city | staffNo | fName | lName | propertyNo |
|---|---|---|---|---|---|
| B003 | Glasgow | SG14 | David | Ford | PG16 |
| B003 | Glasgow | SG37 | Ann | Beech | PG21 |
| B003 | Glasgow | SG37 | Ann | Beech | PG36 |
| B005 | London | SL41 | Julie | Lee | PL94 |
| B007 | Aberdeen | SA9 | Mary | Howe | PA14 |
Joining n tables needs at least n − 1 join conditions. Missing one gives a partial Cartesian product (far too many rows).
Multiple grouping columns
Find the number of properties handled by each staff member.
SELECT s.branchNo, s.staffNo, COUNT(*) AS myCount
FROM Staff s, PropertyForRent p
WHERE s.staffNo = p.staffNo
GROUP BY s.branchNo, s.staffNo
ORDER BY s.branchNo, s.staffNo;
| branchNo | staffNo | myCount |
|---|---|---|
| B003 | SG14 | 1 |
| B003 | SG37 | 2 |
| B005 | SL41 | 1 |
| B007 | SA9 | 1 |
Computing a join
The conceptual procedure for generating the result of a join:
- Form the Cartesian product of the tables named in the FROM clause.
- If there is a WHERE clause, apply the search condition to each row of the product table, retaining rows that satisfy it.
- For each remaining row, determine the value of each item in the SELECT list to produce a single row in the result.
- If
DISTINCThas been specified, eliminate duplicate rows. - If there is an
ORDER BYclause, sort the result as required.
SELECT [DISTINCT | ALL] {* | columnList}
FROM Table1 CROSS JOIN Table2; -- explicit Cartesian product
This is only the logical definition. Real query optimizers never build the full Cartesian product — they use nested-loop, hash or merge joins and indexes — but the result must be the same.
Outer joins — the example tables
With an inner join, if a row of one table is unmatched it is omitted from the result. Outer joins retain rows that do not satisfy the join condition.
| Branch1 | |
|---|---|
| branchNo | bCity |
| B003 | Glasgow |
| B004 | Bristol |
| B002 | London |
| PropertyForRent1 | |
|---|---|
| propertyNo | pCity |
| PA14 | Aberdeen |
| PL94 | London |
| PG4 | Glasgow |
Inner join of these tables
SELECT b.*, p.*
FROM Branch1 b, PropertyForRent1 p
WHERE b.bCity = p.pCity;
| branchNo | bCity | propertyNo | pCity |
|---|---|---|---|
| B003 | Glasgow | PG4 | Glasgow |
| B002 | London | PL94 | London |
The result has two rows where the cities are the same. There are no rows for the branch in Bristol or the property in Aberdeen. To include unmatched rows, use an outer join.
LEFT OUTER JOIN
List branches and properties that are in the same city, along with any unmatched branches.
SELECT b.*, p.*
FROM Branch1 b LEFT JOIN PropertyForRent1 p
ON b.bCity = p.pCity;
| branchNo | bCity | propertyNo | pCity |
|---|---|---|---|
| B003 | Glasgow | PG4 | Glasgow |
| B004 | Bristol | NULL | NULL |
| B002 | London | PL94 | London |
- Includes rows of the first (left) table that are unmatched with rows from the second (right) table.
- Columns from the second table are filled with NULLs.
RIGHT OUTER JOIN
List branches and properties in the same city and any unmatched properties.
SELECT b.*, p.*
FROM Branch1 b RIGHT JOIN PropertyForRent1 p
ON b.bCity = p.pCity;
| branchNo | bCity | propertyNo | pCity |
|---|---|---|---|
| NULL | NULL | PA14 | Aberdeen |
| B003 | Glasgow | PG4 | Glasgow |
| B002 | London | PL94 | London |
- Includes rows of the second (right) table that are unmatched with rows from the first (left) table.
- Columns from the first table are filled with NULLs.
A RIGHT JOIN B gives the same rows as B LEFT JOIN A (only column order differs). Many developers stick to LEFT JOIN for readability.
FULL OUTER JOIN
List branches and properties in the same city and any unmatched branches or properties.
SELECT b.*, p.*
FROM Branch1 b FULL JOIN PropertyForRent1 p
ON b.bCity = p.pCity;
| branchNo | bCity | propertyNo | pCity |
|---|---|---|---|
| NULL | NULL | PA14 | Aberdeen |
| B003 | Glasgow | PG4 | Glasgow |
| B004 | Bristol | NULL | NULL |
| B002 | London | PL94 | London |
- Includes rows that are unmatched in both tables.
- Unmatched columns are filled with NULLs.
Inner = 2 rows. Left = 2 + 1 unmatched branch = 3. Right = 2 + 1 unmatched property = 3. Full = 2 + 1 + 1 = 4. Being able to predict these counts is a quick way to check your answer.
Practical patterns
A LEFT JOIN plus an IS NULL test finds rows with no partner — e.g. clients who have never viewed a property:
SELECT c.clientNo, c.fName, c.lName
FROM Client c LEFT JOIN Viewing v ON c.clientNo = v.clientNo
WHERE v.clientNo IS NULL; -- CR74 Mike Ritchie
Counting with outer joins: use COUNT(v.propertyNo) not COUNT(*), so unmatched rows count as 0 rather than 1.
Quick review
What happens to unmatched rows in an inner join?
In a LEFT JOIN, which table’s columns get NULLs?
Write a query that lists every branch and every property, matched by city where possible.
SELECT b.*, p.* FROM Branch1 b FULL JOIN PropertyForRent1 p ON b.bCity = p.pCity;