SQL Quest
Sign in Français 0 XP Lvl 1
Intermediate

SQL INNER JOIN: link two tables

5 min read · 4 runnable examples · updated October 9, 2026

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

commandes
idclient_id
11
22
39
clients
idprenom
1Léa
2Hugo
3Inès
result
commandeprenom
1Léa
2Hugo
commandes JOIN clients ON clients.id = commandes.client_id: each order gets the details of its customer. Order 3 (customer 9, who does not exist) disappears.

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
Result
commandedate_commandeprenomnom
12023-06-02ChloéMercier
22025-10-17OmarLambert
32024-05-10SarahLambert
42025-02-08LéaLefebvre
52025-09-06ManonGarcia
62024-01-11InèsHaddad

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
Result
idprenomnomdate_commande
13InèsGarcia2024-06-02
138ThéoMartin2023-12-01
153ThéoDubois2023-06-01
174ThéoMichel2024-03-10
210ThéoMartin2023-10-20
348ChloéFontaine2025-04-15
358SarahMichel2025-08-15
368ThéoMichel2024-05-13
430SamirFaure2023-07-22
500ThéoMichel2023-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
Result
categorieca
Informatique151,080
Maison31,502.56
Sport26,397.45
Alimentation20,224.32
Livres12,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
Result
produitfournisseurpays
Souris sans filElectroPlusAllemagne
SSD 1ToElectroPlusAllemagne
Roman policierBookWorldBelgique
Haltères 5kgAsiaPartsChine
Corde à sauterAsiaPartsChine
Manga tome 1AsiaPartsChine
Écran 27"SportZoneEspagne
Hub USB-CSportZoneEspagne

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.

Avoid
SELECT * FROM commandes o JOIN clients c;
Correct
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.

Avoid
SELECT * FROM commandes o JOIN clients c ON c.id = o.id;
Correct
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.

Avoid
SELECT id, nom FROM commandes o JOIN clients c ON c.id = o.client_id;
Correct
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_idnomquantite
1BD aventure5
1Gourde inox5
2Vase céramique2
3Haltères 5kg2
4Écran 27"4
4Thé vert4
4Corde à sauter4
4Cafetière4

10 rows in total · 8 shown

Try it in the sandbox

Challenge 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;
prenomnomnb
SamirRoux49
InèsMercier37
KarimBenali35
LéaGarcia33
LouisChevalier33
LucasMoreau32
ChloéRoux31
EmmaLambert31

21 rows in total · 8 shown

Try it in the sandbox
Want more practice?

The interactive exercises are checked automatically, with a tutor who explains your mistakes. A short lesson stays next to the editor.

Do the interactive exercises

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.

Read next