After this lesson you can
- Choose between an INNER JOIN and a LEFT JOIN deliberately
- Predict which rows survive each one
- Explain why a filter on the outer table belongs in ON, not WHERE
A join matches rows in one table against rows in another using a condition you supply.
INNER JOIN keeps only rows that matched on both sides.
LEFT JOIN keeps every row from the left table, and fills the right-hand
columns with NULL where nothing matched.
SELECT c.name, o.total_cents
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;
A customer with no orders still appears, once, with total_cents null.
Try it
CREATE TABLE customers ( id int PRIMARY KEY, name text NOT NULL);CREATE TABLE orders ( id int PRIMARY KEY, customer_id int NOT NULL, status text NOT NULL, total_cents int NOT NULL);INSERT INTO customers VALUES (1, 'Nino'), (2, 'Ana'), (3, 'Luka');INSERT INTO orders VALUES (10, 1, 'paid', 4500), (11, 1, 'cancelled', 900), (12, 2, 'paid', 12000);SELECT c.name, COALESCE(SUM(o.total_cents), 0) AS paid_centsFROM customers cLEFT JOIN orders o ON o.customer_id = c.id AND o.status <> 'cancelled'GROUP BY c.nameORDER BY c.name;The trap
This is the mistake that turns up in real code more than any other join question:
SELECT c.name, SUM(o.total_cents)
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status <> 'cancelled' -- the bug
GROUP BY c.name;
Customers with no orders survive the join with every o.* column null. The
WHERE clause then evaluates NULL <> 'cancelled', which is unknown rather
than true, so those rows are thrown away. The query is now an inner join and
nobody changed the word LEFT.
The fix is to move the condition into the ON clause, where it decides
which orders are allowed to match rather than which customers
survive:
LEFT JOIN orders o
ON o.customer_id = c.id
AND o.status <> 'cancelled'
The rule worth carrying out of this lesson: a filter on the outer side of a
LEFT JOIN belongs in ON; a filter on the preserved side belongs in
WHERE.
Try it yourself
2 visible tests · 2 hidden testsTables customers(id, name) and orders(id, customer_id). Return every
customer's name and order_count — the number of orders they have
placed, 0 if none — ordered by name. Every customer must appear
exactly once, including one who has never ordered.
CREATE TABLE customers ( id int PRIMARY KEY, name text NOT NULL);CREATE TABLE orders ( id int PRIMARY KEY, customer_id int NOT NULL);INSERT INTO customers VALUES (1, 'Nino'), (2, 'Ana'), (3, 'Luka');INSERT INTO orders VALUES (10, 1), (11, 1), (12, 2);Sign up to check the hidden tests and save your progress. Sign up