LEFT JOIN
The JOIN from the last lesson keeps only the rows that match on both sides. But plenty of questions are about the rows that don't match: which customers have never ordered? Which product has nobody bought? This lesson is about LEFT JOIN, which keeps every row of the left table.
The unmatched rows are gone
First, join customers and orders the way the last lesson did:
1SELECT c.id, c.name, o.id AS order_id2FROM customers AS c3JOIN orders AS o ON o.customer_id = c.id4ORDER BY c.id, o.id;
| id | name | order_id |
|---|---|---|
| 1 | Ana Silva | 1 |
| 1 | Ana Silva | 3 |
| 1 | Ana Silva | 9 |
| 2 | Ben Okafor | 2 |
| 2 | Ben Okafor | 7 |
| 3 | Chloe Martin | 6 |
| 4 | David Kim | 4 |
| 4 | David Kim | 12 |
| 5 | Emma Novak | 5 |
| 5 | Emma Novak | 11 |
| 7 | George Brown | 8 |
| 8 | Hana Sato | 10 |
The result has 12 rows, one per order. There are 10 customers, but customers 6, 9 and 10 are missing: they have never ordered, so nothing in orders matches them, and JOIN drops them.
LEFT JOIN
Change JOIN to LEFT JOIN and nothing else:
1SELECT c.id, c.name, o.id AS order_id2FROM customers AS c3LEFT JOIN orders AS o ON o.customer_id = c.id4ORDER BY c.id, o.id;
| id | name | order_id |
|---|---|---|
| 1 | Ana Silva | 1 |
| 1 | Ana Silva | 3 |
| 1 | Ana Silva | 9 |
| 2 | Ben Okafor | 2 |
| 2 | Ben Okafor | 7 |
| 3 | Chloe Martin | 6 |
| 4 | David Kim | 4 |
| 4 | David Kim | 12 |
| 5 | Emma Novak | 5 |
| 5 | Emma Novak | 11 |
| 6 | Farah Khan | NULL |
| 7 | George Brown | 8 |
| 8 | Hana Sato | 10 |
| 9 | Ivan Petrov | NULL |
| 10 | Julia Costa | NULL |
Now there are 15 rows. Customers 6, 9 and 10 are back, and their order_id is NULL.
LEFT JOIN means: keep every row of the table written after FROM (the left table). A row that matches is joined to the right table's rows just as in the last lesson. A row that doesn't match still gets one row, with every column from the right table filled with NULL.
| This left row | JOIN | LEFT JOIN |
|---|---|---|
| matches 1 row | 1 row | 1 row |
| matches 3 rows | 3 rows | 3 rows |
| matches nothing | gone | 1 row, NULL on the right |
Step through the NULLs
Press Step on the query above and look at the JOIN stage: each customer looks in orders for the orders whose customer_id equals their id. Customer 1, Ana Silva, finds 3 and becomes 3 rows; when it gets to customer 6, Farah Khan, nothing matches, yet she still keeps one row, and every cell on the right is NULL.
The right side has more columns than id. Pick every column of the order to see it more clearly (o.* means all of o's columns):
1SELECT c.name, o.*2FROM customers AS c3LEFT JOIN orders AS o ON o.customer_id = c.id4WHERE c.id >= 85ORDER BY c.id;
| name | id | customer_id | order_date | status |
|---|---|---|---|---|
| Hana Sato | 10 | 8 | 2025-10-20 | shipped |
| Ivan Petrov | NULL | NULL | NULL | NULL |
| Julia Costa | NULL | NULL | NULL | NULL |
Hana Sato has one order, and all four columns have values; Ivan Petrov and Julia Costa have no orders, so all four orders columns are NULL.
Customers who never ordered
Since an unmatched row has NULL on the right, keeping only those rows gives "customers who never ordered":
1SELECT c.id, c.name2FROM customers AS c3LEFT JOIN orders AS o ON o.customer_id = c.id4WHERE o.id IS NULL5ORDER BY c.id;
| id | name |
|---|---|
| 6 | Farah Khan |
| 9 | Ivan Petrov |
| 10 | Julia Costa |
WHERE runs after the JOIN. By then, the rows for customers 6, 9 and 10 have NULL in o.id, and every other row has an order number. Remember to write IS NULL: = NULL finds nothing at all (lesson 5).
What does this query give?
1SELECT p.name2FROM products AS p3LEFT JOIN order_items AS oi ON oi.product_id = p.id4WHERE oi.order_id IS NULL;
order_items lists what each order bought. A product nobody bought matches no row there, gets NULL on the right, and WHERE keeps only those rows. In this shop only product 6, the desk lamp, has never been bought.
Orders per customer
Add GROUP BY to a LEFT JOIN and you can count each customer's orders, zeros included:
1SELECT c.id, c.name, COUNT(o.id) AS order_count2FROM customers AS c3LEFT JOIN orders AS o ON o.customer_id = c.id4GROUP BY c.id, c.name5ORDER BY c.id;
| id | name | order_count |
|---|---|---|
| 1 | Ana Silva | 3 |
| 2 | Ben Okafor | 2 |
| 3 | Chloe Martin | 1 |
| 4 | David Kim | 2 |
| 5 | Emma Novak | 2 |
| 6 | Farah Khan | 0 |
| 7 | George Brown | 1 |
| 8 | Hana Sato | 1 |
| 9 | Ivan Petrov | 0 |
| 10 | Julia Costa | 0 |
It has to be COUNT(o.id), not COUNT(*). Farah Khan's group has a single row, the one padded with NULL: COUNT(*) counts rows and gives 1; COUNT(o.id) skips NULL and gives 0. Change it to COUNT(*) and run it to see.
Put the condition in ON
Say you want each customer's pending orders, with customers who have none still listed. The obvious move is a condition in WHERE, but that loses the customers again:
1SELECT c.name, o.id AS order_id2FROM customers AS c3LEFT JOIN orders AS o ON o.customer_id = c.id4WHERE o.status = 'pending'5ORDER BY c.id;
| name | order_id |
|---|---|
| Ana Silva | 9 |
| Ben Okafor | 7 |
| David Kim | 12 |
Only 3 rows are left. In a padded row, o.status is NULL too, so it fails the WHERE. Move the condition into ON and it becomes part of the matching: only pending orders are matched, and customers with no match still stay:
1SELECT c.name, o.id AS order_id2FROM customers AS c3LEFT JOIN orders AS o ON o.customer_id = c.id AND o.status = 'pending'4ORDER BY c.id;
| name | order_id |
|---|---|
| Ana Silva | 9 |
| Ben Okafor | 7 |
| Chloe Martin | NULL |
| David Kim | 12 |
| Emma Novak | NULL |
| Farah Khan | NULL |
| George Brown | NULL |
| Hana Sato | NULL |
| Ivan Petrov | NULL |
| Julia Costa | NULL |
Tip: With LEFT JOIN, put conditions on the right table in ON. Put them in WHERE and your LEFT JOIN behaves much like a plain JOIN.
RIGHT JOIN
RIGHT JOIN is the other way round: every row of the right table stays. orders RIGHT JOIN customers gives the same rows as customers LEFT JOIN orders. Most people only use LEFT JOIN and write the table they want to keep whole right after FROM; it reads more naturally.
Practice
Write the query, then press Check. A hint costs nothing; the answer is there when you need it.
Never sold
✓Use LEFT JOIN to find the products that never appear in order_items. Show two columns: id and name.
| id | name |
|---|---|
| 6 | Desk lamp |
1SELECT p.id, p.name2FROM products AS p3JOIN order_items AS oi ON oi.product_id = p.id;
Units sold, zeros included
✓Find how many units of each product were sold: two columns, name and sold, with 0 for products never sold, ordered by product id.
| name | sold |
|---|---|
| Notebook | 10 |
| Pens (10) | 3 |
| Backpack | 2 |
| Water bottle | 3 |
| Headphones | 2 |
| Desk lamp | 0 |
| USB cable | 3 |
| Coffee mug | 6 |
| Phone case | 2 |
| Tote bag | 2 |
1SELECT p.name, SUM(oi.quantity) AS sold2FROM products AS p3LEFT JOIN order_items AS oi ON oi.product_id = p.id4GROUP BY p.id, p.name5ORDER BY p.id;
Fix it: a missing customer
✓This should list every customer in Paris with their order numbers, and a customer with no orders should still get a row (with NULL as the order number). One customer is missing. Fix it.
| name | order_id |
|---|---|
| Ben Okafor | 2 |
| Ben Okafor | 7 |
| Farah Khan | NULL |
1SELECT c.name, o.id AS order_id2FROM customers AS c3JOIN orders AS o ON o.customer_id = c.id4WHERE c.city = 'Paris'5ORDER BY c.id, o.id;
Here the condition is fine in WHERE: c.city is a column of the left table, and only the right side gets padded with NULL.
What you learned
JOINkeeps only matching rows;LEFT JOINkeeps the whole left table and pads unmatched rows withNULLon the right.LEFT JOIN … WHERE right.id IS NULLfinds rows with no match: customers who never ordered, products nobody bought.- When counting with
GROUP BY, useCOUNT(a right column);COUNT(*)counts the padded row as 1. - Conditions on the right table go in
ON; inWHEREthey delete the padded rows again. RIGHT JOINkeeps the right table; usually you swap the order and writeLEFT JOIN.