LEFT JOIN: rows without a match too
In short: LEFT JOIN keeps every row of the left table, even those with no match in the right table, and fills the missing columns with NULL.
Problem
Who has not ordered?
Marketing wants to write to the customers who signed up but have not ordered anything yet. In the last lesson JOIN returned 60 rows, and Teodora and Yavor were not among them.
JOIN keeps only the pairs. A row without a match disappears without a warning. Here those are exactly the rows we need.
The idea
LEFT JOIN
Compare the two queries. The first uses JOIN:
The second uses LEFT JOIN:
62 rows instead of 60. LEFT JOIN keeps every row of the left table (the one in FROM), even if there is no match in the right one. Where there is no match, the columns of the right table are NULL. Scroll to the end: Teodora and Yavor are there, with NULL for the number and the date.
From here the customers without orders are one step away:
WHERE o.id IS NULL keeps only the rows where no order was found. We check o.id because the primary key of an order that was found is never empty.
See it
An empty answer is an answer too
Are there products that nobody has ever ordered? The same approach, with order_items:
Not a single row. Every one of the 25 products has been ordered at least once. A table with no rows is not an error: the query has answered “there are none”.
Try it
The empty categories
Which categories have no product directly in them? Show the category name.
Solution
SELECT cat.name
FROM categories cat
LEFT JOIN products p ON p.category_id = cat.id
WHERE p.id IS NULL;FROM categories cat LEFT JOIN products p ON p.category_id = cat.id WHERE p.id IS NULL. “Food” and “Home”. These are the top categories: the products are in their subcategories. With a plain JOIN they do not come out at all.
See it
Counting with the zeros
How many orders does each customer have, including those with none? A first attempt with COUNT(*):
Teodora and Yavor have 1 order each? No. COUNT(*) counts rows, and LEFT JOIN gave each of them one row full of NULLs. Count what has a value, COUNT(o.id):
COUNT(column) skips the NULLs, so they come out with 0. With LEFT JOIN, count a column of the right table, preferably its primary key.
Try it
Reviews per product
Show every product and how many reviews it has, including the products without a review (with 0).
Solution
SELECT p.name, COUNT(r.id) AS reviews
FROM products p
LEFT JOIN reviews r ON r.product_id = p.id
GROUP BY p.id, p.name;FROM products p LEFT JOIN reviews r ON r.product_id = p.id GROUP BY p.id, p.name with COUNT(r.id). 25 rows. With COUNT(*) the products without a review would show 1, and with JOIN they would be missing.
See it
The WHERE trap
A report for October: all customers and how many orders they have placed since 1 October 2026. The date condition goes in WHERE:
Five rows, but there are 20 customers. A condition on the right table in WHERE turns a LEFT JOIN into a plain JOIN. Its place is in ON: there it says which orders to join, and all the customers stay.
20 customers, most of them with 0. The rule: with LEFT JOIN, a condition on the right table goes in ON. The exception is IS NULL, when you are looking for the missing ones.
Exercise
Two tasks
1. The shop will invite the customers who have not written a review yet to write one. Who are they? Show the name.
The customers without a single review:
Solution
SELECT c.name
FROM customers c
LEFT JOIN reviews r ON r.customer_id = c.id
WHERE r.id IS NULL;FROM customers c LEFT JOIN reviews r ON r.customer_id = c.id WHERE r.id IS NULL. Six: Petya, Martin, Simona, Hristo, Teodora and Yavor.
2. The “We miss you” campaign is for the customers who have not placed an order since 1 July 2026 (including those who have never ordered). Show the name and email.
The customers without an order since 2026-07-01:
Solution
SELECT c.name, c.email
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.ordered_at >= '2026-07-01'
WHERE o.id IS NULL;The date goes in ON, and IS NULL in WHERE: LEFT JOIN orders o ON o.customer_id = c.id AND o.ordered_at >= '2026-07-01' WHERE o.id IS NULL. 10 customers. If the date is in WHERE, nobody comes out: the rows with NULL are dropped because of the date, and the rows with a date are dropped because of IS NULL.
More precisely, if you are curious
There are also RIGHT JOIN (keeps the rows of the right table) and FULL JOIN (keeps both sides). SQLite understands them since version 3.39. RIGHT JOIN is rarely written: it is the same as LEFT JOIN with the tables swapped. MySQL has no FULL JOIN.
Only the customers without orders come out here, because every order has a customer.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Aggregate functions: COUNT, SUM, AVG · GROUP BY: a report by groups · Primary key and foreign key · JOIN: two tables in one query
- Continues in: Many tables, and a table joined to itself · UNION, INTERSECT, EXCEPT and CROSS JOIN · A report in steps: WITH · Trees and sequences: WITH RECURSIVE
- Tasks on this topic: Joining tables