SQL LEFT JOIN: keep the rows without a match
With an INNER JOIN, a customer who never ordered disappears from the result. Yet sometimes that is exactly who you are looking for: inactive customers, products that never sold, suppliers without products… LEFT JOIN solves this problem: it keeps every row of the left table, whether it has a match or not.
Syntax
SELECT a.column, b.column
FROM table_a a
LEFT JOIN table_b b ON b.key = a.key;How it works
Left and right
The “left” table is the one written after FROM; the “right” table is the one in the LEFT JOIN. Every row on the left is kept. When a left row has no match on the right, the right columns are NULL.
LEFT JOIN or INNER JOIN?
| INNER JOIN | LEFT JOIN | |
|---|---|---|
| Left row with a match | kept | kept |
| Left row without a match | removed | kept, with NULLs on the right |
Finding what is missing
The most useful LEFT JOIN pattern: LEFT JOIN … WHERE right.id IS NULL. It only keeps the left rows without any match: customers without orders, products never sold, and so on.
RIGHT JOIN and FULL JOIN
RIGHT JOIN does the same thing while keeping the right table; you can always write it as a LEFT JOIN by swapping the tables. FULL OUTER JOIN keeps the rows of both sides. SQLite accepts them since version 3.39.
Diagram
| id | prenom |
|---|---|
| 1 | Léa |
| 2 | Hugo |
| 3 | Inès |
| id | client_id |
|---|---|
| 10 | 1 |
| 11 | 1 |
| 12 | 2 |
| prenom | commande |
|---|---|
| Léa | 10 |
| Léa | 11 |
| Hugo | 12 |
| Inès | NULL |
Simplified diagram to show the idea. The examples below use the real SQL Quest practice database (a fictional online shop: see the full schema).
Glossary: the sample database uses French names
clients- customers
commandes- orders
lignes_commande- order lines
produits- products
fournisseurs- suppliers
employes- employees
prenom / nom- first name / last name (or name)
ville- city
pays- country
prix- price
prix_unitaire- unit price
quantite- quantity
categorie- category
date_commande- order date
poste- job title
departement- department
salaire- salary
statut- status: livrée = delivered, expédiée = shipped, en cours = in progress, annulée = cancelled
Examples with their results
1. Keep every customer
Each customer with their number of orders, including those who have none (0).
SELECT c.id, c.prenom, c.nom, COUNT(o.id) AS nb_commandes
FROM clients c
LEFT JOIN commandes o ON o.client_id = c.id
GROUP BY c.id
ORDER BY nb_commandes
LIMIT 8; Try it in the sandbox| id | prenom | nom | nb_commandes |
|---|---|---|---|
| 111 | Manon | Roux | 0 |
| 112 | Alice | Bernard | 0 |
| 113 | Emma | Moreau | 0 |
| 114 | Nathan | Lefebvre | 0 |
| 115 | Sarah | Simon | 0 |
| 116 | Karim | Lefebvre | 0 |
| 117 | Théo | Chevalier | 0 |
| 118 | Chloé | Lambert | 0 |
8 rows in total
2. Find the rows without a match
The customers who never placed an order.
SELECT c.id, c.prenom, c.nom, c.ville
FROM clients c
LEFT JOIN commandes o ON o.client_id = c.id
WHERE o.id IS NULL; Try it in the sandbox| id | prenom | nom | ville |
|---|---|---|---|
| 111 | Manon | Roux | Strasbourg |
| 112 | Alice | Bernard | Nantes |
| 113 | Emma | Moreau | Bordeaux |
| 114 | Nathan | Lefebvre | Marseille |
| 115 | Sarah | Simon | Lyon |
| 116 | Karim | Lefebvre | Nantes |
| 117 | Théo | Chevalier | Montréal |
| 118 | Chloé | Lambert | Nantes |
| 119 | Lucas | Benali | Bordeaux |
| 120 | Inès | Lambert | Paris |
10 rows in total
3. Compare with INNER JOIN
The number of customers obtained with each kind of join: the difference is the customers without orders.
SELECT
(SELECT COUNT(DISTINCT c.id) FROM clients c JOIN commandes o ON o.client_id = c.id) AS avec_inner_join,
(SELECT COUNT(DISTINCT c.id) FROM clients c LEFT JOIN commandes o ON o.client_id = c.id) AS avec_left_join; Try it in the sandbox| avec_inner_join | avec_left_join |
|---|---|
| 110 | 120 |
1 row in total
4. Employees who handled no orders
Management does not process orders: these employees have no match.
SELECT e.prenom, e.nom, e.poste
FROM employes e
LEFT JOIN commandes o ON o.employe_id = e.id
WHERE o.id IS NULL; Try it in the sandbox| prenom | nom | poste |
|---|---|---|
| Chloé | Dubois | PDG |
| Jade | Lefebvre | Directeur commercial |
| Nathan | Roux | Directrice technique |
| Samir | Simon | Responsable RH |
4 rows in total
Common mistakes
Filtering the right table in the WHERE
A condition on the right table in the WHERE removes the rows where it is NULL: the LEFT JOIN then behaves like an INNER JOIN. Put that condition in the ON.
SELECT c.prenom, o.id FROM clients c LEFT JOIN commandes o ON o.client_id = c.id WHERE o.statut = 'annulée';SELECT c.prenom, o.id FROM clients c LEFT JOIN commandes o ON o.client_id = c.id AND o.statut = 'annulée';Counting with COUNT(*)
For a customer without orders, the row still exists (with NULLs): COUNT(*) gives 1 instead of 0. Count a column from the right table.
SELECT c.id, COUNT(*) FROM clients c LEFT JOIN commandes o ON o.client_id = c.id GROUP BY c.id;SELECT c.id, COUNT(o.id) FROM clients c LEFT JOIN commandes o ON o.client_id = c.id GROUP BY c.id;Swapping the tables
The table whose rows you want to keep must be on the left, right after FROM.
SELECT c.nom FROM commandes o LEFT JOIN clients c ON c.id = o.client_id WHERE o.id IS NULL;SELECT c.nom FROM clients c LEFT JOIN commandes o ON o.client_id = c.id WHERE o.id IS NULL;Exercises
Try writing the query yourself before opening the solution.
Challenge 1. Show every supplier with their number of products, including those who have none.
Show a solution
SELECT f.nom, COUNT(p.id) AS nb_produits FROM fournisseurs f LEFT JOIN produits p ON p.fournisseur_id = f.id GROUP BY f.id;| nom | nb_produits |
|---|---|
| TechNova | 1 |
| ElectroPlus | 2 |
| MaisonDeco | 4 |
| SportZone | 13 |
| BookWorld | 1 |
| GreenFood | 5 |
| AsiaParts | 3 |
7 rows in total
Try it in the sandboxChallenge 2. Show every employee with the number of orders they handled, including those who handled none (0), from the smallest count to the largest.
Show a solution
SELECT e.prenom, e.nom, COUNT(o.id) AS nb_commandes FROM employes e LEFT JOIN commandes o ON o.employe_id = e.id GROUP BY e.id ORDER BY nb_commandes LIMIT 8;| prenom | nom | nb_commandes |
|---|---|---|
| Chloé | Dubois | 0 |
| Jade | Lefebvre | 0 |
| Nathan | Roux | 0 |
| Samir | Simon | 0 |
| Louis | Garcia | 18 |
| Louis | Lambert | 20 |
| Inès | Moreau | 21 |
| Chloé | Benali | 22 |
8 rows in total
Try it in the sandboxThe interactive exercises are checked automatically, with a tutor who explains your mistakes. A short lesson stays next to the editor.
Frequently asked questions
What is the difference between LEFT JOIN and INNER JOIN?
INNER JOIN only keeps the rows that have a match in both tables. LEFT JOIN keeps every row of the left table, and puts NULL in the right columns when there is no match.
Are LEFT JOIN and LEFT OUTER JOIN the same?
Yes, the word OUTER is optional.
Why does my LEFT JOIN return more rows than the left table?
Because a left row is repeated as many times as it has matches on the right. A customer with 5 orders appears 5 times.
How do I find customers without orders?
With LEFT JOIN commandes … WHERE commandes.id IS NULL, or with WHERE NOT EXISTS (SELECT 1 FROM commandes WHERE client_id = clients.id).