Leçon 5/7 9 min

Les jointures

Comprenez pourquoi les tables sont séparées, les clés étrangères, et reliez-les avec INNER JOIN, LEFT JOIN et la clause ON.

FR EN

Le problème : le nom du client n'est pas dans la commande

Vous voulez afficher « Marie Dupont a commandé pour 89,90 € ». Mais la table commandes ne contient pas le nom de Marie. Elle contient seulement un client_id = 1. Le nom, lui, est dans la table clients.

C'est volontaire. Si on stockait « Marie Dupont » dans chaque commande, et qu'elle se marie et change de nom, il faudrait modifier des milliers de lignes. En séparant, on écrit le nom une seule fois. C'est le principe des relations. Pour rassembler les morceaux au moment de l'affichage, on utilise une jointure.

clients                      commandes
id | nom            | ville   id | client_id | montant
---+----------------+------   ---+-----------+--------
1  | Marie Dupont   | Besac.  1  | 1         | 89.90
2  | Karim Benali   | Dijon   2  | 2         | 42.00
3  | Léa Martin     | Lyon    3  | 1         | 120.50

La clé étrangère : le pont entre deux tables

Dans commandes, la colonne client_id est une clé étrangère (foreign key) : elle pointe vers la clé primaire id de la table clients. C'est le fil qui relie une commande à son client.

Une clé primaire identifie une ligne dans SA table. Une clé étrangère est une copie de cet identifiant rangée dans une AUTRE table pour créer le lien. commandes.client_id référence clients.id.

INNER JOIN : ne garder que les correspondances

Un INNER JOIN assemble deux tables sur une condition de correspondance définie par ON. Il ne garde que les lignes qui matchent des deux côtés.

SELECT c.nom, cmd.montant
FROM clients AS c
INNER JOIN commandes AS cmd
    ON cmd.client_id = c.id
ORDER BY c.nom;

On lit : « pour chaque commande, va chercher dans clients la ligne dont l'id égale le client_id, et colle les colonnes ensemble ». Marie apparaît deux fois (elle a deux commandes), Léa n'apparaît pas (elle n'a aucune commande).

Toujours qualifier les colonnes avec des alias de table. Écrivez c.nom et cmd.montant plutôt que nom et montant. Si les deux tables ont une colonne du même nom (comme id), une requête non qualifiée est ambiguë et échoue. Les alias rendent aussi la requête bien plus lisible.

LEFT JOIN : garder tout le monde, même sans correspondance

Et si on veut tous les clients, y compris ceux qui n'ont jamais commandé ? L'INNER JOIN les oublie. Le LEFT JOIN garde toutes les lignes de la table de gauche (clients), et met NULL dans les colonnes de droite quand il n'y a pas de correspondance.

Diagrammes de Venn comparant INNER JOIN (seule l'intersection clients-commandes est gardée) et LEFT JOIN (tous les clients plus l'intersection). INNER JOIN clients commandes seulement les lignes qui correspondent des deux côtés LEFT JOIN clients commandes tous les clients, même sans commande
En vert, les lignes conservées : l'INNER JOIN ne garde que l'intersection, le LEFT JOIN garde tous les clients.
SELECT c.nom, cmd.montant
FROM clients AS c
LEFT JOIN commandes AS cmd
    ON cmd.client_id = c.id
ORDER BY c.nom;

Cette fois, Léa apparaît avec un montant à NULL. Combiné à une agrégation, le LEFT JOIN répond à « combien chaque client a-t-il dépensé, zéro inclus » :

SELECT c.nom, COALESCE(SUM(cmd.montant), 0) AS total
FROM clients AS c
LEFT JOIN commandes AS cmd
    ON cmd.client_id = c.id
GROUP BY c.id, c.nom
ORDER BY total DESC;

COALESCE(valeur, 0) remplace un NULL par 0. Pratique pour qu'un client sans commande affiche un total de 0 au lieu de NULL.

The problem: the customer name is not in the order

You want to display "Marie Dupont ordered for 89.90 EUR". But the commandes table does not contain Marie's name. It only contains a client_id = 1. The name lives in the clients table.

This is on purpose. If we stored "Marie Dupont" in every order, and she married and changed her name, we would have to update thousands of rows. By splitting, we write the name only once. This is the principle of relations. To reassemble the pieces at display time, we use a join.

clients                      commandes
id | nom            | ville   id | client_id | montant
---+----------------+------   ---+-----------+--------
1  | Marie Dupont   | Besac.  1  | 1         | 89.90
2  | Karim Benali   | Dijon   2  | 2         | 42.00
3  | Lea Martin     | Lyon    3  | 1         | 120.50

The foreign key: the bridge between two tables

In commandes, the client_id column is a foreign key: it points to the primary key id of the clients table. It is the thread linking an order to its customer.

A primary key identifies a row in ITS table. A foreign key is a copy of that identifier stored in ANOTHER table to create the link. commandes.client_id references clients.id.

INNER JOIN: keep only matches

An INNER JOIN assembles two tables on a match condition defined by ON. It keeps only the rows that match on both sides.

SELECT c.nom, cmd.montant
FROM clients AS c
INNER JOIN commandes AS cmd
    ON cmd.client_id = c.id
ORDER BY c.nom;

Read it as: "for each order, fetch from clients the row whose id equals the client_id, and stick the columns together". Marie appears twice (she has two orders), Lea does not appear (she has no order).

Always qualify columns with table aliases. Write c.nom and cmd.montant rather than nom and montant. If both tables have a column with the same name (like id), an unqualified query is ambiguous and fails. Aliases also make the query far more readable.

LEFT JOIN: keep everyone, even without a match

What if we want all customers, including those who never ordered? The INNER JOIN forgets them. The LEFT JOIN keeps every row of the left table (clients), and puts NULL in the right columns when there is no match.

Venn diagrams comparing INNER JOIN (only the customers-orders intersection is kept) and LEFT JOIN (all customers plus the intersection). INNER JOIN customers orders only the rows that match on both sides LEFT JOIN customers orders all customers, even without an order
In green, the rows kept: INNER JOIN keeps only the intersection, LEFT JOIN keeps every customer.
SELECT c.nom, cmd.montant
FROM clients AS c
LEFT JOIN commandes AS cmd
    ON cmd.client_id = c.id
ORDER BY c.nom;

This time, Lea appears with a NULL montant. Combined with aggregation, the LEFT JOIN answers "how much did each customer spend, zero included":

SELECT c.nom, COALESCE(SUM(cmd.montant), 0) AS total
FROM clients AS c
LEFT JOIN commandes AS cmd
    ON cmd.client_id = c.id
GROUP BY c.id, c.nom
ORDER BY total DESC;

COALESCE(value, 0) replaces a NULL with 0. Handy so a customer with no order shows a total of 0 instead of NULL.

À vous d'essayer — la base est déjà remplie :

CREATE TABLE clients (id INTEGER, nom TEXT);
CREATE TABLE commandes (id INTEGER, client_id INTEGER, montant REAL);
INSERT INTO clients VALUES (1, 'Alice Durand');
INSERT INTO clients VALUES (2, 'Marc Petit');
INSERT INTO commandes VALUES (1, 1, 120.50);
INSERT INTO commandes VALUES (2, 1, 80.00);
INSERT INTO commandes VALUES (3, 2, 45.00);

SELECT clients.nom, commandes.montant
FROM clients
INNER JOIN commandes ON commandes.client_id = clients.id
ORDER BY clients.nom;
Avec l'IA

Demandez la jointure à l'IA et vérifiez le type (INNER vs LEFT) et les alias de table :

J'ai deux tables : clients (id, nom, ville) et commandes (id, client_id, montant), où commandes.client_id référence clients.id. Écris une requête SQL qui liste TOUS les clients avec le total de leurs achats, y compris ceux qui n'ont jamais commandé (total 0). Utilise des alias de table et qualifie chaque colonne. Précise pourquoi tu choisis LEFT JOIN plutôt qu'INNER JOIN.
Ré-explique sans regarder

Sans relire la réponse de l'IA : avec tes mots, pourquoi a-t-elle choisi LEFT JOIN plutôt qu'INNER JOIN pour lister TOUS les clients, même ceux à 0 € ?

Une bonne explication dit : un INNER JOIN ne garde que les clients qui ont au moins une commande, donc les clients sans commande disparaissent du résultat. Le LEFT JOIN garde toutes les lignes de la table de gauche (clients) et met NULL à droite quand il n'y a pas de correspondance ; combiné à COALESCE(SUM(...), 0), le client sans achat affiche bien un total de 0 au lieu de disparaître.
Exercice : Joignez les tables

Écrivez une requête qui affiche le nom du client et le montant de chaque commande, uniquement pour les clients ayant au moins une commande. Utilisez un INNER JOIN, la clause ON (cmd.client_id = c.id) et des alias de table.

Accepter ou rejeter le code de l'IA

Tu lui as demandé : « liste TOUS les clients avec leur total dépensé, ceux sans commande inclus ». L'IA renvoie ceci. Tu l'acceptes tel quel ou tu le rejettes ?

SELECT c.nom, SUM(cmd.montant) AS total
FROM clients AS c
INNER JOIN commandes AS cmd
    ON cmd.client_id = c.id
GROUP BY c.id, c.nom
ORDER BY total DESC;
À rejeter. La demande disait « ceux sans commande inclus », mais l'INNER JOIN ne garde que les clients ayant au moins une commande : un client à 0 € disparaît silencieusement du résultat. C'est le piège classique de la jointure, et le plus sournois car la requête tourne sans erreur. Deux corrections : passer en LEFT JOIN pour garder tous les clients, et envelopper la somme dans COALESCE(SUM(cmd.montant), 0) pour afficher 0 au lieu de NULL.
Rappel libre

Sans remonter dans la leçon : quelle est la différence entre INNER JOIN et LEFT JOIN, et à quoi sert la clause ON ?

L'INNER JOIN ne garde que les lignes qui ont une correspondance des deux côtés (un client SANS commande disparaît). Le LEFT JOIN garde toutes les lignes de la table de gauche et met NULL à droite quand il n'y a pas de correspondance. La clause ON définit la condition de rapprochement, ici cmd.client_id = c.id : c'est elle qui dit quelles lignes des deux tables vont ensemble.
Qu'est-ce qu'une clé étrangère ?
Quelle jointure garde les clients sans aucune commande ?
Pourquoi qualifier les colonnes (c.nom, cmd.montant) ?
Prochaine étape

Jusqu'ici vous n'avez fait que lire. La prochaine leçon vous donne les pleins pouvoirs : ajouter, modifier et supprimer des lignes avec INSERT, UPDATE et DELETE, sans vider toute la table par accident.

Leçon 6 : Modifier les données →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement