Combiner des tables : JOIN

Clients, commandes et produits sont généralement stockés dans des tables distinctes. JOIN permet de lire les lignes liées à partir de leurs clés.

La jointure de base

Les commandes stockent customer_id. Les clients stockent le nom. Pour les voir ensemble, indiquez à la base comment les deux tables se correspondent.

1

SELECT o.order_number,
       o.ordered_at,
       o.total,
       c.first_name || ' ' || c.last_name AS customer,
       c.country
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY o.ordered_at DESC
LIMIT 20;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 20

order_numberordered_attotalcustomercountry
ORD-2025-0024802025-12-28 23:52:146333.77Elena MeyerCA
ORD-2025-0005052025-12-28 11:16:392276.51Diego RossiIT
ORD-2025-0009402025-12-28 07:28:4611000.68Freya ChoiBR
ORD-2025-0015812025-12-28 06:12:382645.06Jonas LeeJP
ORD-2025-0009952025-12-28 05:47:532584.02Leo HaddadUS

Planche s’ouvre dans un onglet séparé, la base d’exemple déjà chargée et cette requête prête dans l’éditeur. Tous les exemples de la page réutilisent cet onglet, et rien ne s’exécute tant que vous n’appuyez pas sur F9.

Trois éléments méritent d’être nommés. JOIN seul signifie INNER JOIN. ON c.id = o.customer_id est la condition de jointure — comment une ligne de gauche est appariée à une ligne de droite. Et orders o donne à la table un alias court, ce qui permet d’écrire o.total et c.country sans répéter le nom complet.

Les alias cessent d’être optionnels dès que deux tables ont une colonne du même nom. Ces deux tables ont chacune un id ; non qualifié, id serait ambigu et la base refuserait la requête.

Plus de deux tables

Les jointures s’enchaînent. Les produits d’une commande sont à deux pas : orders → order_items → products.

2

SELECT o.order_number,
       p.name AS product,
       oi.quantity,
       oi.unit_price
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'delivered'
ORDER BY o.ordered_at DESC
LIMIT 25;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 25

order_numberproductquantityunit_price
ORD-2025-000505Zen Boots1700.65
ORD-2025-000505Echo Backpack 40L Plus11570.96
ORD-2025-001581Atlas Phone Pro Max11546.38
ORD-2025-001581Pulse Headphones2548.09
ORD-2025-000995Aria Chrono21289.56

Chaque JOIN accroche une table de plus à ce que vous avez déjà : une jointure sur quatre tables, c’est la même chose avec une ligne en plus. Lisez de haut en bas : « les commandes, avec leurs lignes de commande, avec le produit de chaque ligne ».

Garder les lignes sans correspondance

Une jointure interne écarte tout ce qui n’a pas de partenaire. Un client qui n’a jamais commandé disparaît simplement du résultat. LEFT JOIN garde chaque ligne de la table de gauche et remplit la droite avec des NULL là où il n’y avait rien à apparier.

3

SELECT c.id,
       c.first_name,
       c.last_name,
       o.order_number,
       o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.id
LIMIT 25;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 25

idfirst_namelast_nameorder_numbertotal
1LiamFischerORD-2022-0015961317.56
1LiamFischerORD-2025-0017232664.56
1LiamFischerORD-2025-0031424609.77
2MateoCostaORD-2025-0024733760.6
3YukiJangORD-2025-0035277569.3

C’est ce qui rend possible la requête « trouver ce qui manque » : tout garder, puis ne conserver que les lignes dont l’appariement a échoué.

4

-- The 95 customers who have never ordered anything.
SELECT c.id, c.first_name, c.last_name, c.country
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL
ORDER BY c.id
LIMIT 25;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 25

idfirst_namelast_namecountry
14MayaMoreauUS
17NoraMüllerIT
20SanaParkKR
30RaviParkKR
33OliverParkIN

WHERE o.id IS NULL se lit bizarrement au début — orders.id est une clé primaire et ne peut jamais être nulle dans la table. Mais elle peut l’être dans ce résultat, parce que le LEFT JOIN a inventé ces NULL pour les lignes non appariées. Ces lignes sont les 95 clients qui n’ont jamais commandé.

ON ou WHERE : ce n’est pas la même question

Voici le piège. Les deux requêtes mentionnent status = 'cancelled'. Une seule est réellement un LEFT JOIN.

5

-- The condition is part of the join: every customer is kept,
-- and only cancelled orders are attached to them.
SELECT c.id, c.first_name, o.order_number, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'cancelled'
ORDER BY c.id
LIMIT 25;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 25

idfirst_nameorder_numberstatus
1LiamNULLNULL
2MateoNULLNULL
3YukiORD-2025-003530cancelled
4InesNULLNULL
5LeoNULLNULL

La condition est dans ON, elle fait donc partie de l’appariement : chaque client est conservé, et une commande annulée est rattachée quand il en existe une. Les clients sans commande annulée apparaissent quand même, avec des NULL.

6

-- The same condition in WHERE runs after the join and throws away
-- every row where it is not true — including the NULLs the LEFT JOIN made.
SELECT c.id, c.first_name, o.order_number, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'cancelled'
ORDER BY c.id
LIMIT 25;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 25

idfirst_nameorder_numberstatus
3YukiORD-2025-003530cancelled
6NoraORD-2023-001551cancelled
6NoraORD-2022-002224cancelled
7PriyaORD-2022-003262cancelled
10ElenaORD-2025-000182cancelled

Ici la condition est dans WHERE, qui s’exécute après la jointure et jette toutes les lignes pour lesquelles elle n’est pas vraie. Les lignes NULL que le LEFT JOIN vient de créer échouent au test — NULL = 'cancelled' n’est pas vrai — et sont donc toutes écartées. Vous avez écrit un LEFT JOIN et obtenu un INNER JOIN.

La règle est courte : une condition sur la table de droite d’un LEFT JOIN va dans ON. Une condition sur la table de gauche va dans WHERE. Pour une jointure interne, cela ne change réellement rien, et c’est pour cela que la mauvaise habitude s’attrape facilement.

Joindre une table à elle-même

Rien n’impose que les deux côtés soient des tables différentes. Les employés ont un manager_id qui pointe vers un autre employé : la table se joint donc à elle-même sous deux alias différents.

7

SELECT e.first_name || ' ' || e.last_name AS employee,
       e.position,
       m.first_name || ' ' || m.last_name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id
ORDER BY manager, employee
LIMIT 25;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 25

employeepositionmanager
Ava MüllerDirectorNULL
Hana TanakaDirectorNULL
Hugo AhmedDirectorNULL
Lucas RomeroDirectorNULL
Maya MüllerDirectorNULL

e et m sont la même table vue deux fois, et ce sont les alias qui rendent la chose possible. C’est un LEFT JOIN parce que les dix personnes au sommet n’ont pas de responsable, et qu’une jointure interne les laisserait discrètement de côté.

Ce que vous savez faire maintenant

Vous pouvez poser des questions sur tout le schéma. Ce que vous ne savez pas encore faire, c’est résumer la réponse : compter les commandes, totaliser le chiffre d’affaires et calculer des moyennes, c’est la prochaine leçon.

À vous d’essayer

  1. Les 15 avis les plus récents, avec la date, le produit concerné, l’auteur et la note. Cela fait trois tables.

    Voir la réponse

    Voir la réponse

    SELECT r.created_at,
           p.name AS product,
           c.first_name || ' ' || c.last_name AS reviewer,
           r.rating
    FROM reviews r
    JOIN products p ON p.id = r.product_id
    JOIN customers c ON c.id = r.customer_id
    ORDER BY r.created_at DESC
    LIMIT 15;
    Ouvrir dans Planche

    Résultat — 5 premières lignes sur 15

    created_atproductreviewerrating
    2025-12-28 21:12:12Sol ChronoHugo Park2
    2025-12-28 16:46:22Ember Catan 2Minjun Novak4
    2025-12-28 14:00:00Quill SpeakerOmar Smith3
    2025-12-28 03:41:42Learning React (Deluxe ed.)Ines Romero4
    2025-12-28 03:40:06Zen Sheet Set 2Kenji Ahmed4

    Deux jointures enchaînées : les avis vers les produits pour le nom, puis les avis vers les clients pour l’auteur. Les deux sont des jointures internes, car un avis a toujours les deux.

  2. Quelles commandes n’ont aucune expédition enregistrée ?

    Voir la réponse

    Voir la réponse

    SELECT o.order_number, o.status, o.ordered_at
    FROM orders o
    LEFT JOIN shipments s ON s.order_id = o.id
    WHERE s.id IS NULL
    ORDER BY o.ordered_at DESC
    LIMIT 20;
    Ouvrir dans Planche

    Résultat — 5 premières lignes sur 20

    order_numberstatusordered_at
    ORD-2025-000940cancelled2025-12-28 07:28:46
    ORD-2025-003297processing2025-12-28 01:40:38
    ORD-2025-002729cancelled2025-12-27 23:10:11
    ORD-2025-002019paid2025-12-27 09:26:48
    ORD-2025-000337pending2025-12-27 05:29:40

    Un LEFT JOIN garde toutes les commandes, et WHERE s.id IS NULL ne conserve que celles sans correspondance. 1 681 des 4 000 commandes remplissent la condition — logique, puisque les commandes en attente ou annulées ne sont jamais expédiées.