JOINs: All Types
INNER, LEFT, RIGHT, FULL OUTER, SELF JOIN, CROSS JOIN, and UNION โ complete join reference.
๐ฏLearning Objectives
- Explain the difference between INNER, LEFT, RIGHT, and FULL OUTER JOIN
- Write multi-table JOIN queries using the DVD Rental schema
- Use SELF JOIN to relate rows within the same table
- Combine result sets with UNION and UNION ALL
- Choose the correct join type for a given problem
๐Theory
1. Why JOINs Exist
Relational databases store data across multiple tables to avoid duplication. A film row does not embed category or actor data โ it holds foreign keys pointing to those tables. JOINs reassemble that data at query time.
Every JOIN follows the same basic pattern:
SQL
SELECT columns
FROM table_a
JOIN table_b ON table_a.key = table_b.key;The ON clause defines the match condition. Rows that satisfy the condition are combined into a single output row.
2. INNER JOIN
INNER JOIN returns only rows where the condition matches in both tables. Rows with no counterpart in the other table are dropped entirely.
SQL
-- All films that have at least one actor assigned
SELECT f.title, a.first_name, a.last_name
FROM film f
INNER JOIN film_actor fa ON f.film_id = fa.film_id
INNER JOIN actor a ON fa.actor_id = a.actor_id
ORDER BY f.title
LIMIT 10;Think of INNER JOIN as the intersection of two sets: only the overlap survives.
3. LEFT JOIN (LEFT OUTER JOIN)
LEFT JOIN keeps all rows from the left table. For each left row, PostgreSQL looks for a matching right row. If a match is found, the right columns are filled in. If no match exists, the right columns are NULL.
SQL
-- All customers, plus their rental if one exists (NULL if not)
SELECT c.first_name, c.last_name, r.rental_id
FROM customer c
LEFT JOIN rental r ON c.customer_id = r.customer_id
LIMIT 10;The classic use case is finding rows with no counterpart in another table โ by filtering for WHERE right_table.key IS NULL.
4. RIGHT JOIN (RIGHT OUTER JOIN)
RIGHT JOIN is the mirror of LEFT JOIN โ all rows from the right table are preserved, and left-side columns go NULL when there is no match. In practice, most developers rewrite RIGHT JOINs as LEFT JOINs by swapping table order, because LEFT JOIN is more natural to read.
SQL
-- All staff, plus their store (even if store record is missing)
SELECT st.first_name, st.last_name, s.address_id
FROM store s
RIGHT JOIN staff st ON s.store_id = st.store_id;5. FULL OUTER JOIN
FULL OUTER JOIN returns every row from both tables. Where a match exists, the columns are combined. Where no match exists on either side, the missing columns are NULL. It is the union of LEFT JOIN and RIGHT JOIN.
SQL
-- Every film and every inventory item; NULLs show gaps on either side
SELECT f.title, i.inventory_id, i.store_id
FROM film f
FULL OUTER JOIN inventory i ON f.film_id = i.film_id
ORDER BY f.film_id
LIMIT 20;FULL OUTER JOIN is relatively rare but useful for reconciliation โ finding rows in table A that have no match in B, and vice versa, all in one query.
6. SELF JOIN
A SELF JOIN joins a table to itself. PostgreSQL has no special syntax for this โ you just reference the same table twice with different aliases. Self joins are used when rows within a table have relationships to other rows in the same table.
SQL
-- Pairs of customers who live in the same city
SELECT c1.first_name AS customer1,
c2.first_name AS customer2,
ci.city
FROM customer c1
JOIN address a1 ON c1.address_id = a1.address_id
JOIN customer c2 ON c1.customer_id < c2.customer_id
JOIN address a2 ON c2.address_id = a2.address_id
JOIN city ci ON a1.city_id = ci.city_id
WHERE a1.city_id = a2.city_id
LIMIT 10;The condition c1.customer_id < c2.customer_id prevents duplicate pairs (A+B and B+A) and self-matches (A+A).
7. UNION and UNION ALL
UNION combines the result sets of two or more SELECT statements into a single list. The rules are:
- Both queries must return the same number of columns
- Corresponding columns must have compatible data types
UNIONremoves duplicate rows (likeSELECT DISTINCT)UNION ALLkeeps all rows including duplicates โ and is faster
SQL
-- Combined name list from actors and customers
SELECT first_name, last_name, 'actor' AS source FROM actor
UNION
SELECT first_name, last_name, 'customer' AS source FROM customer
ORDER BY last_name;Tip: Column names in the result come from the first SELECT. Aliases in subsequent SELECTs are ignored.
8. Choosing the Right Join
| Situation | Use |
|---|---|
| Only rows that match in both tables | INNER JOIN |
| All rows from left, optional match from right | LEFT JOIN |
| Find rows in left with no match in right | LEFT JOIN + WHERE right.key IS NULL |
| All rows from both, show gaps on either side | FULL OUTER JOIN |
| Rows within one table relate to each other | SELF JOIN |
| Combine two result lists | UNION / UNION ALL |
| Combine without deduplication (fast) | UNION ALL |
๐ปCode Examples
Example A โ Actors and Their Films (INNER JOIN, three tables)
SQL
SELECT a.first_name,
a.last_name,
f.title,
f.release_year
FROM actor a
INNER JOIN film_actor fa ON a.actor_id = fa.actor_id
INNER JOIN film f ON fa.film_id = f.film_id
ORDER BY a.last_name, a.first_name
LIMIT 20;This chains two INNER JOINs through the film_actor bridge table. Every actor-film combination is returned; actors with no films and films with no actors are excluded.
Example B โ Customers With No Rentals (LEFT JOIN + NULL filter)
SQL
SELECT c.customer_id,
c.first_name,
c.last_name,
c.email
FROM customer c
LEFT JOIN rental r ON c.customer_id = r.customer_id
WHERE r.rental_id IS NULL
ORDER BY c.last_name;LEFT JOIN ensures every customer row survives. The WHERE r.rental_id IS NULL filter then keeps only those rows where no rental matched โ i.e., customers who have never rented anything.
Example C โ Combined Name Directory (UNION)
SQL
SELECT first_name,
last_name,
'Actor' AS role
FROM actor
UNION
SELECT first_name,
last_name,
'Customer' AS role
FROM customer
UNION
SELECT first_name,
last_name,
'Staff' AS role
FROM staff
ORDER BY last_name, first_name;Three tables are combined into one alphabetical directory. UNION removes any rows where the same name appears as both, for example, an actor and a customer with identical names.
โ๏ธPractice Tasks
Write a query that lists every film title together with its language name. Use an INNER JOIN between film and language.
Expected columns: title, language_name (alias the language.name column).
Hint
Join film to language on film.language_id = language.language_id. Alias language.name AS language_name to avoid ambiguity.
SQL
SELECT f.title, l.name AS language_name
FROM film f
INNER JOIN language l ON f.language_id = l.language_id
ORDER BY f.title;Find all films that have never been rented. Return the film title and rental rate, ordered by rental rate descending.
Tip: You will need to trace the path: film โ inventory โ rental. A film with no inventory record can never have been rented. A film that has inventory but no rental rows also qualifies.
Hint
Use a LEFT JOIN from film through inventory to rental. Filter for rental.rental_id IS NULL to isolate films with no rental records.
SQL
SELECT f.title, f.rental_rate
FROM film f
LEFT JOIN inventory i ON f.film_id = i.film_id
LEFT JOIN rental r ON i.inventory_id = r.inventory_id
WHERE r.rental_id IS NULL
ORDER BY f.rental_rate DESC;Write a two-part query:
Part A: For each staff member, show their full name and the total payment amount they processed. Include staff members with zero payments (use LEFT JOIN).
Part B: Add a summary row at the bottom showing the label 'TOTAL' in the first_name column, an empty string in last_name, and the grand total amount. Combine Part A and Part B using UNION ALL.
Hint
Part A joins staff LEFT JOIN payment on staff_id, groups by staff, and uses COALESCE(SUM(amount), 0).
Part B is a separate SELECT that does SUM(amount) from payment with no grouping, and literal strings for the name columns.
SQL
SELECT s.first_name,
s.last_name,
COALESCE(SUM(p.amount), 0) AS total_amount
FROM staff s
LEFT JOIN payment p ON s.staff_id = p.staff_id
GROUP BY s.staff_id, s.first_name, s.last_name
UNION ALL
SELECT 'TOTAL', '', SUM(amount)
FROM payment
ORDER BY total_amount;โ ๏ธCommon Mistakes
Forgetting the ON clause condition entirely. Writing FROM film JOIN inventory without ON film.film_id = inventory.film_id produces a CROSS JOIN โ every film row combined with every inventory row. On large tables this can generate millions of rows.
Using INNER JOIN when LEFT JOIN is needed. If the goal is to find customers with no rentals, INNER JOIN silently drops those customers from the result. Always ask: "Do I need rows even when there is no match?"
NULL comparisons after LEFT JOIN. After a LEFT JOIN, unmatched rows have NULL in the right-side columns. You must use IS NULL or IS NOT NULL โ comparing with = NULL always returns false in SQL.
Ambiguous column names. When two joined tables have a column with the same name (e.g., both have name), PostgreSQL raises an error. Always qualify columns with a table alias.
UNION column count mismatch. Both SELECT statements in a UNION must have exactly the same number of columns. Add literal values or NULL placeholders to balance them.
Duplicate pairs in SELF JOIN. Without table1.id < table2.id in the condition, a self join returns both (A, B) and (B, A) as separate rows, and (A, A) as a self-match.
๐Instructor Notes
Timing breakdown:
- 0โ5 min: Recap relational model and why data is split across tables
- 5โ20 min: INNER JOIN live demo โ actor + film_actor + film
- 20โ30 min: LEFT JOIN demo โ customers with no rentals; draw the Venn diagram
- 30โ40 min: RIGHT JOIN (brief), FULL OUTER JOIN โ show the gaps
- 40โ50 min: SELF JOIN โ customers in same city; emphasize the alias trick and
<condition - 50โ60 min: UNION vs UNION ALL โ column count rule, deduplication cost
- 60โ80 min: Students work on Tasks 1โ3
Live-coding cues:
- For INNER JOIN: start with two tables, then add the bridge table. Show row counts at each step with
SELECT COUNT(*). - For LEFT JOIN: first run without the WHERE filter to show NULL columns, then add the filter.
- For SELF JOIN: draw two copies of the customer table on the whiteboard before writing SQL.
- For UNION: deliberately write different column counts to trigger the error, then fix it live.
Discussion questions:
- "If I LEFT JOIN and never filter for NULL, how does the result differ from INNER JOIN?"
- "What would happen if we removed the
c1.customer_id < c2.customer_idcondition from the self join?" - "Why is UNION ALL generally faster than UNION?"