16. INNER JOIN: combining rows across tables
Why joins exist
Recall the relational model from Module 0: relationships are expressed by
matching values, not by nesting or pointers. A JOIN is the operation
that acts on that match — it combines rows from two tables where a
condition you specify holds true. Everything from here through Module 7
builds on this one idea.
INNER JOIN syntax
SELECT c.first_name, c.last_name, r.rental_date
FROM customer c
INNER JOIN rental r ON r.customer_id = c.customer_id
LIMIT 5;
INNER JOIN (often written just JOIN — they're identical, INNER is
implied) returns only rows where the ON condition matches on both
sides. A customer with zero rentals produces zero rows in this result — a
critical fact that motivates the entire next lesson (OUTER JOIN, for
when you need unmatched rows too).
Table aliases
c and r above are table aliases — short names standing in for
customer and rental for the rest of the query. Not optional in
practice once you're joining: without them, SELECT customer_id is
ambiguous the instant two joined tables both have a customer_id column,
and Postgres will reject the query outright rather than guess:
-- Ambiguous — both tables have customer_id:
-- SELECT customer_id FROM customer JOIN rental ON ...
-- ERROR: column reference "customer_id" is ambiguous
-- Disambiguated with aliases:
SELECT c.customer_id FROM customer c JOIN rental r ON r.customer_id = c.customer_id LIMIT 3;
This course's convention (and a common industry one): alias to the
table's initial letter(s), lowercase, no AS required for table aliases
(unlike column aliases, though AS is allowed here too — FROM customer
AS c works identically to FROM customer c).
Joining on multiple keys
ON isn't limited to one equality. Two situations call for more than one
condition, and it's worth telling them apart:
Composite-key relationships — when a table's primary/foreign key is
made of more than one column together, every part must match. pagila's
film_actor is a good example of the key itself being composite (its
primary key is (actor_id, film_id) together — neither column alone is
unique), even though nothing in pagila needs to join against both parts
at once. The general shape, joined with AND, looks like this:
-- Hypothetical: if a table stored bookings keyed by (customer_id, visit_date)
-- together rather than a single surrogate id, a join enforcing both parts
-- of that key would read:
-- ON b.customer_id = c.customer_id AND b.visit_date = c.signup_date
Extra filtering baked into the join itself — more common in practice
than true composite keys: adding a second AND condition to ON that
isn't part of any key at all, to narrow which matching rows join, before
WHERE ever runs:
-- Only join in rentals that were also returned same-day, as part of the
-- join condition itself (not a separate WHERE filter):
SELECT c.first_name, r.rental_date, r.return_date
FROM customer c
JOIN rental r
ON r.customer_id = c.customer_id
AND r.return_date::date = r.rental_date::date
LIMIT 5;
The distinction matters most with outer joins (next lesson) — putting a
condition in ON vs. WHERE changes the result in that case. For inner
joins, the two are equivalent — try both in the boxes above.
The USING shorthand
When the join columns have the identical name on both sides (as
customer_id does here), USING is a shorter, equivalent spelling:
SELECT c.first_name, r.rental_date
FROM customer c
JOIN rental r USING (customer_id)
LIMIT 5;
One behavioral difference worth knowing: with USING, the joined column
appears once in SELECT *, not twice — with ON, SELECT * would
include both customer.customer_id and rental.customer_id as separate
output columns. Minor, but it explains an otherwise-confusing column
count difference if you ever compare the two forms directly.
Chaining joins
Nothing stops you from joining more than two tables — each JOIN adds
one more table to the working row set, and later joins can reference
columns from any table joined so far:
SELECT c.first_name, c.last_name, f.title, r.rental_date
FROM customer c
JOIN rental r ON r.customer_id = c.customer_id
JOIN inventory i ON i.inventory_id = r.inventory_id
JOIN film f ON f.film_id = i.film_id
LIMIT 5;
This four-table chain is a completely normal, everyday query shape — not an advanced technique. Real schemas are normalized (Module 8) precisely so that answering a real question routinely means walking through several tables like this.
Check yourself
- What's the difference in behavior between
INNER JOINand how a plainJOINkeyword behaves in Postgres? - Why does
SELECT customer_idbecome ambiguous once youJOIN customertorental, and what's the fix? - When can you use
USING (col)instead ofON a.col = b.col, and what changes aboutSELECT *output when you do?