Lire ensemble deux tables reliées par une clé étrangère. On commence par la requête naïve, qui donne 36 lignes pour 12 élèves, puis on écrit la bonne : la jointure.
Dans une base bien découpée, la classe d'un élève n'est pas écrite en toutes lettres : la table eleve contient seulement un numéro. Pour afficher « Lucas Adam, 4TTR », il faut lire deux tables en même temps. Cet article commence par la requête la plus simple, qui donne un résultat absurde, puis construit la bonne.
💡 Base de travail
Télécharge
mon-ecole-classes.dbdans la carte « Téléchargements » de la sidebar et ouvre-la avec DB Browser for SQLite. Dans l'onglet Éditer les pragmas, vérifie que la case Foreign Keys est cochée.
À la fin de cet article, tu seras capable de :
JOIN … ON.JOIN et LEFT JOIN selon la question posée.La base contient une table classe et une table eleve. Voici leur description, au format du modèle logique de données (MLD) : le nom de la table, puis ses colonnes, avec la clé primaire soulignée.
La colonne classe_id est une clé étrangère : elle contient l'id de la classe de l'élève. L'écriture #classe_id → classe.id rappelle vers quelle colonne elle mène. Regarde les trois premiers élèves :
SELECT id, nom, prenom, classe_id FROM eleve WHERE id <= 3;
id | nom | prenom | classe_id
---+----------+--------+----------
1 | Adam | Lucas | 1
2 | Bastin | Emma | 1
3 | Charlier | Noah | 1
Personne ne veut lire un bulletin où il est écrit « classe 1 ». Pour afficher 4TTR, il faut aller chercher le nom de la classe dans l'autre table.
FROMEssayons le plus simple : écrire les deux tables dans le FROM, séparées par une virgule. Comme les deux tables ont une colonne nom, on précise à chaque fois de quelle table elle vient : eleve.nom et classe.nom.
SELECT eleve.nom, classe.nom
FROM eleve, classe;
Le résultat compte 36 lignes. Voici les neuf premières :
nom | nom
---------+-----
Adam | 4TTR
Adam | 5TTR
Adam | 6TTR
Bastin | 4TTR
Bastin | 5TTR
Bastin | 6TTR
Charlier | 4TTR
Charlier | 5TTR
Charlier | 6TTR
36 lignes pour 12 élèves, et Lucas Adam apparaît dans les trois classes à la fois.
12 élèves × 3 classes = 36. SQLite a fait exactement ce qu'on lui a demandé : il a associé chaque élève à chaque classe, sans rien trier. Sur ces 36 combinaisons, seules 11 sont vraies : celles où le classe_id de l'élève est égal à l'id de la classe.
📖 Nouvelle notion : le produit cartésien
Le produit cartésien de deux tables est l'ensemble de toutes les combinaisons possibles entre une ligne de la première table et une ligne de la seconde. Son nombre de lignes est le produit des deux nombres de lignes : 12 élèves × 3 classes = 36 lignes.
En anglais : 🇬🇧 cartesian product.
⚠️ Aucun message d'erreur
Le produit cartésien est dangereux parce que la requête s'exécute sans erreur. Avec 12 élèves et 3 classes, l'absurdité saute aux yeux. Avec 800 élèves et 40 classes, on obtient 32 000 lignes qui ont l'air plausibles, et quelqu'un finira par en tirer une statistique fausse.
Le symptôme à reconnaître : un résultat beaucoup plus gros que prévu, où chaque ligne se répète. Réflexe : vérifie la condition qui relie les deux tables.
Il manque une phrase à notre requête : « garde seulement les combinaisons où l'élève appartient vraiment à la classe ». Autrement dit, les lignes où classe.id est égal à eleve.classe_id.
SELECT eleve.prenom, eleve.nom, classe.nom AS classe
FROM eleve
JOIN classe ON classe.id = eleve.classe_id
ORDER BY classe.nom, eleve.nom;
prenom | nom | classe
--------+-----------+-------
Lucas | Adam | 4TTR
Emma | Bastin | 4TTR
Noah | Charlier | 4TTR
Léa | Delvaux | 4TTR
Hugo | Englebert | 4TTR
Chloé | Fontaine | 5TTR
Nathan | Gérard | 5TTR
Manon | Hubert | 5TTR
Louis | Istas | 5TTR
Camille | Jacques | 6TTR
Arthur | Kevers | 6TTR
Onze lignes, et chacune est vraie. Pour rappel, AS veut dire comme : la colonne classe.nom s'affiche sous le nom classe.
📖 Nouvelle notion : la jointure
Une jointure assemble les lignes de deux tables qui correspondent l'une à l'autre.
JOINveut dire joindre etONveut dire sur : on joint la tableclassesur la conditionclasse.id = eleve.classe_id. Seules les combinaisons qui respectent cette condition sont gardées.En anglais : 🇬🇧 join.
| Morceau | Rôle |
|---|---|
FROM eleve |
la première table |
JOIN classe |
la table à joindre |
ON classe.id = eleve.classe_id |
la condition de liaison : la clé primaire d'un côté, la clé étrangère de l'autre |
🧠 Le
ONs'écrit toujours pareilClé étrangère d'un côté, clé primaire de l'autre. Le MLD te donne donc la condition sans rien inventer :
#classe_id → classe.iddevientON classe.id = eleve.classe_id.
Écrire eleve. et classe. devant chaque colonne devient vite long. On peut donner un surnom court à chaque table :
SELECT e.prenom, e.nom, c.nom AS classe
FROM eleve e
JOIN classe c ON c.id = e.classe_id
ORDER BY c.nom, e.nom;
Le résultat est exactement le même que plus haut. FROM eleve e donne à la table eleve le surnom e ; on écrit ensuite e.nom au lieu de eleve.nom.
📖 Nouvelle notion : l'alias de table
Un alias de table est un surnom donné à une table dans une requête, écrit juste après son nom :
FROM eleve e. Il sert à préfixer les colonnes (e.nom,c.nom) pour dire de quelle table chacune vient. Il n'existe que le temps de la requête.En anglais : 🇬🇧 table alias.
Préfixer les colonnes n'est pas qu'une question de confort. Les deux tables ont une colonne nom, et sans préfixe, SQLite ne sait pas de laquelle tu parles :
SELECT nom FROM eleve JOIN classe ON classe.id = eleve.classe_id;
ambiguous column name: nom
Le message veut dire « nom de colonne ambigu : nom ». La requête est refusée.
💡 Préfixe toutes les colonnes
Dès qu'une requête contient une jointure, préfixe toutes les colonnes, même celles qui ne sont pas ambiguës. On voit alors d'un coup d'œil d'où vient chaque information.
WHERE (où) fonctionne comme d'habitude, et il peut porter sur les colonnes des deux tables :
SELECT e.prenom, e.nom, c.local
FROM eleve e
JOIN classe c ON c.id = e.classe_id
WHERE c.nom = '5TTR'
ORDER BY e.nom;
prenom | nom | local
-------+----------+------
Chloé | Fontaine | A21
Nathan | Gérard | A21
Manon | Hubert | A21
Louis | Istas | A21
Cette requête traverse deux tables : le critère porte sur la classe, et le résultat parle des élèves. C'est tout l'intérêt d'avoir séparé les données puis de les avoir reliées.
JOIN ou LEFT JOIN ?Regarde encore le résultat de la première jointure : onze lignes pour douze élèves. Sarah Lambert a disparu.
Elle n'a pas encore de classe : son classe_id est vide (NULL). Aucune ligne de classe ne correspond, donc JOIN l'écarte. Selon la question posée, c'est le bon comportement… ou une perte silencieuse.
Pour garder tous les élèves, on écrit LEFT JOIN :
SELECT e.prenom, e.nom, c.nom AS classe
FROM eleve e
LEFT JOIN classe c ON c.id = e.classe_id
ORDER BY e.nom;
prenom | nom | classe
--------+-----------+-------
Lucas | Adam | 4TTR
Emma | Bastin | 4TTR
Noah | Charlier | 4TTR
Léa | Delvaux | 4TTR
Hugo | Englebert | 4TTR
Chloé | Fontaine | 5TTR
Nathan | Gérard | 5TTR
Manon | Hubert | 5TTR
Louis | Istas | 5TTR
Camille | Jacques | 6TTR
Arthur | Kevers | 6TTR
Sarah | Lambert | NULL
Sarah Lambert est là, avec une classe vide.
📖 Nouvelle notion :
LEFT JOIN
LEFTveut dire gauche.LEFT JOINgarde toutes les lignes de la table de gauche, celle écrite dans leFROM, même celles qui n'ont aucune correspondance dans l'autre table. Pour ces lignes-là, les colonnes de la table de droite valentNULL.En anglais : 🇬🇧 left join.
| Tu demandes… | Tu écris |
|---|---|
| « Tous les élèves, avec leur classe quand elle est connue » | LEFT JOIN |
| « Les élèves qui ont une classe, avec cette classe » | JOIN |
🧠 La question à se poser
Est-ce que je veux voir les lignes qui n'ont pas de correspondance ?
Choisir
JOINpar habitude fait disparaître, sans bruit, exactement les cas qui méritaient ton attention. Un élève sans classe en octobre, c'est probablement un oubli d'encodage : c'est précisément la ligne qu'il fallait voir.
Pour rappel, IS NULL veut dire est vide. On l'écrit toujours ainsi, jamais = NULL. Combiné à LEFT JOIN, il isole les élèves qui n'ont pas de classe :
SELECT e.prenom, e.nom
FROM eleve e
LEFT JOIN classe c ON c.id = e.classe_id
WHERE c.id IS NULL;
prenom | nom
-------+--------
Sarah | Lambert
💡 Une requête de contrôle qualité
LEFT JOIN … WHERE … IS NULLliste exactement les cas incomplets. Garde cette requête sous la main et relance-la régulièrement : si elle renvoie des lignes, un encodage est à compléter.
WHERE sur la table de droiteNouvelle question : « tous les élèves, avec leur classe, sauf ceux de 4TTR ». Sarah Lambert n'est pas en 4TTR : elle doit donc apparaître. <> veut dire différent de.
SELECT e.prenom, e.nom, c.nom AS classe
FROM eleve e
LEFT JOIN classe c ON c.id = e.classe_id
WHERE c.nom <> '4TTR'
ORDER BY e.nom;
prenom | nom | classe
--------+----------+-------
Chloé | Fontaine | 5TTR
Nathan | Gérard | 5TTR
Manon | Hubert | 5TTR
Louis | Istas | 5TTR
Camille | Jacques | 6TTR
Arthur | Kevers | 6TTR
Sarah Lambert a disparu, malgré le LEFT JOIN. Pour sa ligne, c.nom vaut NULL. Or une comparaison avec NULL n'est jamais vraie : SQLite ne sait pas si « rien » est différent de 4TTR, et le WHERE écarte la ligne.
Pour la garder, il faut le demander explicitement : OR veut dire ou.
SELECT e.prenom, e.nom, c.nom AS classe
FROM eleve e
LEFT JOIN classe c ON c.id = e.classe_id
WHERE c.nom <> '4TTR' OR c.id IS NULL
ORDER BY e.nom;
prenom | nom | classe
--------+----------+-------
Chloé | Fontaine | 5TTR
Nathan | Gérard | 5TTR
Manon | Hubert | 5TTR
Louis | Istas | 5TTR
Camille | Jacques | 6TTR
Arthur | Kevers | 6TTR
Sarah | Lambert | NULL
⚠️ Un
WHEREsur la table de droite annule leLEFTUne condition du
WHEREsur une colonne de la table de droite élimine toutes les lignes où cette colonne vautNULL, c'est-à-dire justement celles que leLEFT JOINdevait garder. Le résultat devient celui d'un simpleJOIN, sans aucun message d'erreur.
GROUP BYPour rappel, COUNT veut dire compter et GROUP BY veut dire regrouper par : GROUP BY c.nom range les lignes par classe, et COUNT compte les lignes de chaque groupe. Combien d'élèves compte chaque classe ?
SELECT c.nom AS classe, COUNT(*) AS nb_eleves
FROM eleve e
JOIN classe c ON c.id = e.classe_id
GROUP BY c.nom
ORDER BY c.nom;
classe | nb_eleves
-------+----------
4TTR | 5
5TTR | 4
6TTR | 2
L'école ouvre maintenant une classe de 3TTR, dans le local B10. Aucun élève n'y est encore inscrit. Ajoute-la dans ta base :
INSERT INTO classe (nom, annee, local) VALUES ('3TTR', 3, 'B10');
Relance la requête précédente : le résultat ne change pas, la 3TTR n'apparaît pas. Aucun élève ne la désigne, donc JOIN n'a rien à assembler pour elle. Pour voir toutes les classes, il faut partir de la table classe et écrire un LEFT JOIN vers eleve :
SELECT c.nom AS classe, COUNT(e.id) AS nb_eleves
FROM classe c
LEFT JOIN eleve e ON e.classe_id = c.id
GROUP BY c.nom
ORDER BY c.nom;
classe | nb_eleves
-------+----------
3TTR | 0
4TTR | 5
5TTR | 4
6TTR | 2
La classe vide apparaît, avec 0 élève. Le sens de la jointure compte : la table dont on veut voir toutes les lignes s'écrit dans le FROM, à gauche du LEFT JOIN.
⚠️
COUNT(*)ouCOUNT(e.id)?
COUNT(*)compte les lignes, même celles remplies deNULL.COUNT(e.id)ne compte que les lignes oùe.idn'est pas vide. AvecCOUNT(*), la 3TTR afficherait 1 élève au lieu de 0 :classe | nb_eleves -------+---------- 3TTR | 1 4TTR | 5 5TTR | 4 6TTR | 2Avec un
LEFT JOIN, compte toujours une colonne de la table de droite.
Remets ta base dans son état de départ en supprimant la 3TTR :
DELETE FROM classe WHERE nom = '3TTR';
Cette fois, la suppression est acceptée, même avec le pragma activé : aucun élève ne désigne cette classe, donc aucune ligne ne devient orpheline.
Pour rappel, le module sqlite3 permet à un programme Python d'ouvrir une base SQLite : connect ouvre la base, execute envoie une requête et close ferme la connexion. Place ce programme dans le même dossier que mon-ecole-classes.db :
import sqlite3
conn = sqlite3.connect("mon-ecole-classes.db")
conn.execute("PRAGMA foreign_keys = ON")
conn.row_factory = sqlite3.Row
requete = """
SELECT e.prenom, e.nom, c.nom AS classe
FROM eleve e
JOIN classe c ON c.id = e.classe_id
ORDER BY c.nom, e.nom
"""
for ligne in conn.execute(requete):
print(f"{ligne['prenom']:8} {ligne['nom']:10} {ligne['classe']}")
conn.close()
Lucas Adam 4TTR
Emma Bastin 4TTR
Noah Charlier 4TTR
Léa Delvaux 4TTR
Hugo Englebert 4TTR
Chloé Fontaine 5TTR
Nathan Gérard 5TTR
Manon Hubert 5TTR
Louis Istas 5TTR
Camille Jacques 6TTR
Arthur Kevers 6TTR
Quatre points méritent ton attention.
PRAGMA foreign_keys = ON vient juste après l'ouverture. Un programme Python part toujours d'un pragma désactivé. Avec le pragma activé, une écriture incohérente, comme UPDATE eleve SET classe_id = 99 WHERE id = 12, arrête le programme avec l'erreur sqlite3.IntegrityError: FOREIGN KEY constraint failed.conn.row_factory = sqlite3.Row permet de lire chaque colonne par son nom : ligne['prenom'] plutôt que ligne[0].""" permettent d'écrire la requête sur plusieurs lignes, avec la mise en page qui la rend lisible. Utilise-les dès que la requête dépasse une ligne.AS classe est indispensable. Sans lui, les deux colonnes s'appelleraient nom.Voici ce qui se passe sans AS, avec la requête SELECT e.nom, c.nom FROM eleve e JOIN classe c ON c.id = e.classe_id : ligne.keys() renvoie deux noms identiques, et ligne["nom"] donne seulement le nom de l'élève.
['nom', 'nom']
Adam
Le nom de la classe est dans le résultat, mais impossible de l'atteindre par son nom. Aucune erreur ne te prévient.
Tous les exercices portent sur mon-ecole-classes.db, avec le pragma activé.
COUNT(*) compte les lignes d'un résultat : SELECT COUNT(*) FROM eleve; renvoie le nombre d'élèves.
eleve, puis celles de classe.SELECT * FROM eleve, classe;. Vérifie avec SELECT COUNT(*) FROM eleve, classe;.SELECT * FROM eleve, classe, professeur;, puis vérifie.Utilise des alias et préfixe toutes les colonnes.
A21.JOIN ou LEFT JOIN ? ★★☆☆☆Pour chaque demande, dis quelle jointure tu utilises et pourquoi, puis écris la requête.
| Lettre | Demande |
|---|---|
| a | La liste complète des élèves pour l'appel, avec leur classe quand elle est connue |
| b | Les élèves à convoquer pour la photo de classe |
| c | Les élèves qui ne sont rattachés à aucune classe |
| d | Le nombre d'élèves de chaque classe, classes vides comprises |
| e | Les classes qui n'ont encore aucun élève (attention au sens de la jointure) |
Ces quatre requêtes ont chacune un problème. Exécute-les, note ce qui se passe, explique la cause et corrige.
-- a)
SELECT nom, prenom FROM eleve JOIN classe ON classe.id = eleve.classe_id;
-- b)
SELECT e.nom, c.nom FROM eleve e, classe c WHERE c.nom = '4TTR';
-- c)
SELECT e.nom, c.nom AS classe FROM eleve e JOIN classe c ON c.id = e.id;
-- d)
SELECT e.nom, c.nom AS classe FROM eleve e LEFT JOIN classe c ON c.id = e.classe_id WHERE c.nom = '4TTR';
LEFT JOIN et un WHERE sur la table de droite. Que devient Sarah Lambert ? Le LEFT JOIN sert-il encore à quelque chose ?Écris un programme Python qui affiche ce rapport de cohérence de la base :
Rapport de cohérence
Élèves ................ 12
Élèves sans classe .... 1 → Sarah Lambert
Classes ............... 3
Classes sans élève .... 0
Contraintes :
PRAGMA foreign_keys = ON et utilise row_factory ;Vérifie ensuite ton programme : ajoute une classe vide dans une copie de la base, relance le rapport et contrôle que la dernière ligne passe à 1.
FROM sans condition donnent le produit cartésien : toutes les combinaisons, sans aucun message d'erreur. Symptôme : un résultat beaucoup trop gros, où chaque ligne se répète.JOIN autre_table ON cle_primaire = cle_etrangere ne garde que les combinaisons réelles ; la condition se lit directement dans le MLD.FROM eleve e) raccourcissent les requêtes ; préfixer les colonnes évite l'erreur ambiguous column name.JOIN écarte les lignes sans correspondance ; LEFT JOIN les garde, avec des NULL. La question à se poser : est-ce que je veux voir ceux qui n'ont pas de correspondance ?LEFT JOIN … WHERE … IS NULL isole les cas incomplets ; un WHERE sur la table de droite annule le LEFT.LEFT JOIN, écris COUNT(colonne_de_droite), pas COUNT(*).AS dès que deux colonnes portent le même nom.Tu sais maintenant découper des données en deux entités, les relier par une clé étrangère et les interroger ensemble. Pour réviser tout le niveau, passe par la fiche à étudier.