SQL INNER JOIN: link two tables
In a well-designed database, information is spread over several tables: orders on one side, customers on the other. To show “order no. 12 was placed by Léa Martin”, you need to link these tables. That is what a join does, and INNER JOIN (or just JOIN) is the most common one.
Syntax
SELECT a.column, b.column
FROM table_a a
JOIN table_b b ON b.key = a.foreign_key;How it works
Primary key and foreign key
Each customer has a unique identifier, its primary key: clients.id. In the commandes (orders) table, the client_id column holds the identifier of the customer who placed the order: it is a foreign key. The join pairs the rows where commandes.client_id = clients.id.
How INNER JOIN works
For each row of the first table, SQL looks for the rows of the second one that meet the ON condition, and builds one result row per match. A row without a match disappears from the result: that is the difference with LEFT JOIN.
Table aliases
FROM commandes o JOIN clients c gives the tables the nicknames o and c. You then write c.nom or o.id: it is shorter, and essential when two tables have a column with the same name.
Chaining joins
You can add as many JOINs as needed. For example: order lines → products → suppliers.
Diagram
| id | client_id |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 9 |
| id | prenom |
|---|---|
| 1 | Léa |
| 2 | Hugo |
| 3 | Inès |
| commande | prenom |
|---|---|
| 1 | Léa |
| 2 | Hugo |
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. A simple join
Each order with the first and last name of the customer.
SELECT o.id AS commande, o.date_commande, c.prenom, c.nom
FROM commandes o
JOIN clients c ON c.id = o.client_id
LIMIT 6; Try it in the sandbox| commande | date_commande | prenom | nom |
|---|---|---|---|
| 1 | 2023-06-02 | Chloé | Mercier |
| 2 | 2025-10-17 | Omar | Lambert |
| 3 | 2024-05-10 | Sarah | Lambert |
| 4 | 2025-02-08 | Léa | Lefebvre |
| 5 | 2025-09-06 | Manon | Garcia |
| 6 | 2024-01-11 | Inès | Haddad |
6 rows in total
2. Join to filter
The cancelled orders of customers from Lyon.
SELECT o.id, c.prenom, c.nom, o.date_commande
FROM commandes o
JOIN clients c ON c.id = o.client_id
WHERE o.statut = 'annulée' AND c.ville = 'Lyon'; Try it in the sandbox| id | prenom | nom | date_commande |
|---|---|---|---|
| 13 | Inès | Garcia | 2024-06-02 |
| 138 | Théo | Martin | 2023-12-01 |
| 153 | Théo | Dubois | 2023-06-01 |
| 174 | Théo | Michel | 2024-03-10 |
| 210 | Théo | Martin | 2023-10-20 |
| 348 | Chloé | Fontaine | 2025-04-15 |
| 358 | Sarah | Michel | 2025-08-15 |
| 368 | Théo | Michel | 2024-05-13 |
| 430 | Samir | Faure | 2023-07-22 |
| 500 | Théo | Michel | 2023-07-03 |
14 rows in total · 10 shown
3. Three tables and a GROUP BY
The revenue per product category.
SELECT p.categorie, ROUND(SUM(l.quantite * l.prix_unitaire), 2) AS ca
FROM lignes_commande l
JOIN produits p ON p.id = l.produit_id
GROUP BY p.categorie
ORDER BY ca DESC; Try it in the sandbox| categorie | ca |
|---|---|
| Informatique | 151,080 |
| Maison | 31,502.56 |
| Sport | 26,397.45 |
| Alimentation | 20,224.32 |
| Livres | 12,755.42 |
5 rows in total
4. A chain of joins
The products with the country of their supplier.
SELECT p.nom AS produit, f.nom AS fournisseur, f.pays
FROM produits p
JOIN fournisseurs f ON f.id = p.fournisseur_id
ORDER BY f.pays
LIMIT 8; Try it in the sandbox| produit | fournisseur | pays |
|---|---|---|
| Souris sans fil | ElectroPlus | Allemagne |
| SSD 1To | ElectroPlus | Allemagne |
| Roman policier | BookWorld | Belgique |
| Haltères 5kg | AsiaParts | Chine |
| Corde à sauter | AsiaParts | Chine |
| Manga tome 1 | AsiaParts | Chine |
| Écran 27" | SportZone | Espagne |
| Hub USB-C | SportZone | Espagne |
8 rows in total
Common mistakes
Forgetting the ON condition
Without ON, every order is paired with every customer (Cartesian product): 600 × 120 = 72,000 rows.
SELECT * FROM commandes o JOIN clients c;SELECT * FROM commandes o JOIN clients c ON c.id = o.client_id;Linking the wrong columns
c.id = o.id pairs order no. 5 with customer no. 5, which has nothing to do with it. The query runs but the result is wrong.
SELECT * FROM commandes o JOIN clients c ON c.id = o.id;SELECT * FROM commandes o JOIN clients c ON c.id = o.client_id;Ambiguous column
Both tables have an id column: SQL does not know which one to show (ambiguous column name). Prefix it with the alias.
SELECT id, nom FROM commandes o JOIN clients c ON c.id = o.client_id;SELECT o.id, c.nom FROM commandes o JOIN clients c ON c.id = o.client_id;Exercises
Try writing the query yourself before opening the solution.
Challenge 1. Show each order line with the product name and the quantity.
Show a solution
SELECT l.commande_id, p.nom, l.quantite FROM lignes_commande l JOIN produits p ON p.id = l.produit_id LIMIT 10;| commande_id | nom | quantite |
|---|---|---|
| 1 | BD aventure | 5 |
| 1 | Gourde inox | 5 |
| 2 | Vase céramique | 2 |
| 3 | Haltères 5kg | 2 |
| 4 | Écran 27" | 4 |
| 4 | Thé vert | 4 |
| 4 | Corde à sauter | 4 |
| 4 | Cafetière | 4 |
10 rows in total · 8 shown
Try it in the sandboxChallenge 2. Show the number of orders handled by each employee (first name, last name, count).
Show a solution
SELECT e.prenom, e.nom, COUNT(*) AS nb FROM commandes o JOIN employes e ON e.id = o.employe_id GROUP BY e.id ORDER BY nb DESC;| prenom | nom | nb |
|---|---|---|
| Samir | Roux | 49 |
| Inès | Mercier | 37 |
| Karim | Benali | 35 |
| Léa | Garcia | 33 |
| Louis | Chevalier | 33 |
| Lucas | Moreau | 32 |
| Chloé | Roux | 31 |
| Emma | Lambert | 31 |
21 rows in total · 8 shown
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
Are JOIN and INNER JOIN the same?
Yes. INNER is implied: a plain JOIN is an inner join.
Can I put the condition in the WHERE instead of ON?
For an INNER JOIN, the result is the same (it is the old FROM a, b WHERE a.x = b.y syntax). With ON, the query is easier to read, and it is essential for LEFT JOINs.
Why does my join return more rows than expected?
Because a row can have several matches: an order contains several order lines, so it appears several times. That is normal for a “one-to-many” relationship.
How many tables can I join?
As many as needed. Each JOIN adds a table with its own ON condition.