LEFT JOIN and missing matches
An inner join drops rows that have no partner. Often those are exactly the rows you care about: the customers who have not ordered, the products that have not sold. A LEFT JOIN keeps them.
The idea
A LEFT JOIN B returns every row of A. Where a row of A has a match in B, the columns of B are filled in. Where it has none, the columns of B are NULL.
"Left" simply means the table written first, on the left of the word JOIN.
SELECT c.name, o.id AS order_id, o.order_date
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.name;Result
| name | order_id | order_date |
|---|---|---|
| Arjun Mehta | NULL | NULL |
| Asha Verma | 1 | 2026-01-05 |
| Asha Verma | 4 | 2026-02-02 |
| Asha Verma | 12 | 2026-05-06 |
| Asha Verma | 20 | 2026-10-04 |
| Imran Ali | 8 | 2026-03-15 |
| Imran Ali | 15 | 2026-06-21 |
| Joy Thomas | 14 | 2026-06-02 |
| Kabir Khan | 3 | 2026-01-20 |
| Kabir Khan | 10 | 2026-04-01 |
| Kabir Khan | 18 | 2026-09-01 |
| Meena Gill | NULL | NULL |
23 rows
Showing the first 12 of 23 rows.
Every customer is here. A customer with three orders appears three times. A customer with no orders appears once, with NULL in the order columns.
Compare the row counts of the two kinds of join:
SELECT
(SELECT COUNT(*) FROM customers c JOIN orders o ON o.customer_id = c.id) AS inner_rows,
(SELECT COUNT(*) FROM customers c LEFT JOIN orders o ON o.customer_id = c.id) AS left_rows;Result
| inner_rows | left_rows |
|---|---|
| 20 | 23 |
1 row
The difference is the number of customers without orders.
Finding rows with no match
This is the most valuable use of a left join. Keep everything from the left, then keep only the rows where the right side came back empty.
SELECT c.name, c.city
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;Result
| name | city |
|---|---|
| Meena Gill | Amritsar |
| Arjun Mehta | Pune |
| Tara Singh | Ludhiana |
3 rows
These are the customers who have never ordered. The test must be IS NULL on a column of the right-hand table that can never be NULL in a real match. The primary key, o.id, is the safe choice.
Which product has nobody ever bought?
SELECT p.name, p.category
FROM products p
LEFT JOIN order_items oi ON oi.product_id = p.id
WHERE oi.product_id IS NULL;Result
| name | category |
|---|---|
| Wall Clock | Home |
1 row
Counting, including the zeros
In lesson 12, a count of orders per customer left out the customers with none. A left join puts them back. There is one thing to get right: count a column from the right table, not *.
SELECT c.name, COUNT(o.id) AS orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY orders DESC, c.name;Result
| name | orders |
|---|---|
| Asha Verma | 4 |
| Kabir Khan | 3 |
| Ravi Kumar | 3 |
| Imran Ali | 2 |
| Neha Joshi | 2 |
| Priya Nair | 2 |
| Sunita Rao | 2 |
| Joy Thomas | 1 |
| Vikram Das | 1 |
| Arjun Mehta | 0 |
| Meena Gill | 0 |
| Tara Singh | 0 |
12 rows
COUNT(o.id) skips the NULLs, so a customer with no orders gets 0. COUNT(*) would count the single row that the left join keeps for that customer and report 1, which is wrong. Try it and see.
Totals work the same way, with COALESCE to turn a missing total into zero.
SELECT p.name, COALESCE(SUM(oi.quantity), 0) AS units_sold
FROM products p
LEFT JOIN order_items oi ON oi.product_id = p.id
GROUP BY p.id, p.name
ORDER BY units_sold
LIMIT 5;Result
| name | units_sold |
|---|---|
| Wall Clock | 0 |
| Earphones | 2 |
| Power Bank | 2 |
| Pressure Cooker 3 L | 2 |
| Tea 500 g | 3 |
5 rows
The trap: a WHERE that undoes the left join
Suppose you want every customer, with their delivered orders if they have any. This looks right and is not.
SELECT c.name, o.id AS order_id, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'delivered'
ORDER BY c.name;Result
| name | order_id | status |
|---|---|---|
| Asha Verma | 1 | delivered |
| Asha Verma | 4 | delivered |
| Asha Verma | 12 | delivered |
| Imran Ali | 8 | delivered |
| Imran Ali | 15 | delivered |
| Joy Thomas | 14 | delivered |
| Kabir Khan | 10 | delivered |
| Priya Nair | 6 | delivered |
| Ravi Kumar | 2 | delivered |
| Ravi Kumar | 7 | delivered |
| Ravi Kumar | 16 | delivered |
| Sunita Rao | 5 | delivered |
13 rows
Showing the first 12 of 13 rows.
The customers with no orders have vanished. For them o.status is NULL, the test NULL = 'delivered' is not true, and WHERE throws the row out. The left join has quietly become an inner join.
The cure is to put the condition on the right-hand table into the ON, so that it is part of the matching and not a filter afterwards.
SELECT c.name, o.id AS order_id, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'delivered'
ORDER BY c.name;Result
| name | order_id | status |
|---|---|---|
| Arjun Mehta | NULL | NULL |
| Asha Verma | 1 | delivered |
| Asha Verma | 4 | delivered |
| Asha Verma | 12 | delivered |
| Imran Ali | 8 | delivered |
| Imran Ali | 15 | delivered |
| Joy Thomas | 14 | delivered |
| Kabir Khan | 10 | delivered |
| Meena Gill | NULL | NULL |
| Neha Joshi | NULL | NULL |
| Priya Nair | 6 | delivered |
| Ravi Kumar | 2 | delivered |
17 rows
Showing the first 12 of 17 rows.
Now every customer appears. The rule to remember:
- A condition in
ONdecides which rows of the right table count as a match. - A condition in
WHEREdecides which rows of the final result survive.
For an inner join the two give the same result. For a left join they do not.
RIGHT JOIN
A RIGHT JOIN B keeps every row of B. It is a left join written the other way round, and you can always get the same result by swapping the tables and using LEFT JOIN. Most people use only left joins, so that "the table I am keeping everything from" is always the first one. Here is the "never ordered" query from above, written with a right join. It returns the same customers:
SELECT c.name, c.city
FROM orders o
RIGHT JOIN customers c ON o.customer_id = c.id
WHERE o.id IS NULL;Result
| name | city |
|---|---|
| Meena Gill | Amritsar |
| Arjun Mehta | Pune |
| Tara Singh | Ludhiana |
3 rows
FULL JOIN
A FULL JOIN, also written FULL OUTER JOIN, keeps the unmatched rows of both tables. It is useful for comparing two lists, such as this month's customers against last month's. PostgreSQL, SQL Server, Oracle and recent versions of SQLite have it. MySQL does not; there, people combine a left join and a right join with UNION.
The join family
| Join | Keeps |
|---|---|
INNER JOIN | only rows with a match on both sides |
LEFT JOIN | all rows of the first table |
RIGHT JOIN | all rows of the second table |
FULL JOIN | all rows of both tables |
CROSS JOIN | every row paired with every row |
The word OUTER may be written in the middle three, as in LEFT OUTER JOIN. It changes nothing.
Which join should I use?
Ask what should happen to a row that has no partner. If it should disappear, use an inner join. If it should stay, with blanks, use a left join starting from the table whose rows must all be kept. When in doubt, compare the row counts of both and look at the rows that differ.
Try it yourself
Exercise 1. Show every product's name with the number of order lines it appears in, including products with zero. Name the count lines.
Show one answer
SELECT p.name, COUNT(oi.product_id) AS lines FROM products p LEFT JOIN order_items oi ON oi.product_id = p.id GROUP BY p.id, p.name;| name | lines |
|---|---|
| Basmati Rice 5 kg | 3 |
| Toor Dal 1 kg | 3 |
| Sunflower Oil 1 L | 3 |
| Tea 500 g | 2 |
| Notebook 200 pages | 4 |
| Ball Pen pack of 10 | 4 |
| Geometry Box | 2 |
| School Bag | 2 |
| LED Bulb 9 W | 3 |
| Extension Board | 2 |
| Earphones | 2 |
| Power Bank | 2 |
16 rows
Showing the first 12 of 16 rows.
Exercise 2. Show the names of the customers who have no orders, in alphabetical order.
Show one answer
SELECT c.name FROM customers c LEFT JOIN orders o ON o.customer_id = c.id WHERE o.id IS NULL ORDER BY c.name;| name |
|---|
| Arjun Mehta |
| Meena Gill |
| Tara Singh |
3 rows
Exercise 3. Show every customer's name and the date of their most recent order, with NULL for customers who have none. Sort by name.
Show one answer
SELECT c.name, MAX(o.order_date) FROM customers c LEFT JOIN orders o ON o.customer_id = c.id GROUP BY c.id, c.name ORDER BY c.name;| name | MAX(o.order_date) |
|---|---|
| Arjun Mehta | NULL |
| Asha Verma | 2026-10-04 |
| Imran Ali | 2026-06-21 |
| Joy Thomas | 2026-06-02 |
| Kabir Khan | 2026-09-01 |
| Meena Gill | NULL |
| Neha Joshi | 2026-09-17 |
| Priya Nair | 2026-08-11 |
| Ravi Kumar | 2026-07-09 |
| Sunita Rao | 2026-05-19 |
| Tara Singh | NULL |
| Vikram Das | 2026-04-18 |
12 rows
Examples on this page were run with SQLite 3.50.4 on 12 November 2026. The results shown are the real results.
This is an independent learning site. It is not connected with the makers of any database product. MySQL, PostgreSQL, SQL Server, Oracle and SQLite are named only so that you know where a piece of SQL works. The shop, its customers and its staff in the practice database are made up.