Dans ce carnet
- Que doit conserver votre LEFT JOIN ?
- Comment reproduire le cas dans une base vide ?
- Pourquoi WHERE fait-il disparaître deux équipes ?
- Où mettre le filtre pour conserver les quatre équipes ?
- Pourquoi compter t.id plutôt que COUNT(*) ?
- Exercice : ajouter OR IS NULL suffit-il à réparer WHERE ?
- Quels contrôles faire avant de partager le rapport ?
- Questions fréquentes sur LEFT JOIN et WHERE
Un LEFT JOIN conserve les lignes de la table de gauche, mais un filtre placé ensuite dans WHERE peut les supprimer. Pour afficher toutes les équipes avec leur nombre de tickets résolus, placez le critère de résolution dans ON, puis comptez les identifiants des tickets. Une équipe sans ticket résolu pourra alors apparaître avec zéro.
Mon conseil : avant de corriger la requête, écrivez ce que doit représenter une ligne du rapport. Ici : « une équipe, même si elle n’a rien résolu ». Nous allons vérifier cette promesse sur un petit jeu de données, puis tenter de la casser.
Que doit conserver votre LEFT JOIN ?
Il doit conserver les quatre équipes de notre liste, même celles qui n’ont que des tickets ouverts ou aucun ticket. Le besoin porte sur les équipes ; elles seront donc à gauche de la jointure. Une jointure relie les lignes de deux tables à l’aide d’une condition.
| id | nom |
|---|---|
| 1 | Nord |
| 2 | Sud |
| 3 | Ouest |
| 4 | Est |
| id | equipe_id | statut |
|---|---|---|
| 101 | 1 | resolu |
| 102 | 1 | resolu |
| 103 | 1 | ouvert |
| 104 | 2 | ouvert |
| 105 | 4 | resolu |
Avant d’écrire une formule, on peut prévoir le résultat : Nord 2, Sud 0, Ouest 0, Est 1. Trois tickets sont résolus ; le rapport doit afficher quatre équipes. Cette attente sera notre contrôle.
Si ces notions sont nouvelles, commencez par notre guide des jointures SQL. Ici, nous nous concentrons sur une erreur précise, pas sur toutes les jointures possibles.
Comment reproduire le cas dans une base vide ?
Créez les deux tables ci-dessous, puis insérez les données. Les identifiants sont uniques et non nuls grâce aux clés primaires. Le champ equipe_id indique à quelle équipe appartient chaque ticket. Dans cet exercice, chaque equipe_id correspond à une équipe existante. Le script ne contient toutefois aucune contrainte imposant cette correspondance.
CREATE TABLE equipes (
id INTEGER PRIMARY KEY,
nom TEXT NOT NULL
);
CREATE TABLE tickets (
id INTEGER PRIMARY KEY,
equipe_id INTEGER NOT NULL,
statut TEXT NOT NULL
);
INSERT INTO equipes VALUES
(1, 'Nord'), (2, 'Sud'),
(3, 'Ouest'), (4, 'Est');
INSERT INTO tickets VALUES
(101, 1, 'resolu'),
(102, 1, 'resolu'),
(103, 1, 'ouvert'),
(104, 2, 'ouvert'),
(105, 4, 'resolu');Ces requêtes ont été exécutées pour ce carnet avec SQLite. Nous citons également la documentation PostgreSQL pour expliquer les règles. Les exemples restent simples ; adaptez les types et les noms à votre propre base après le test.

Pourquoi WHERE fait-il disparaître deux équipes ?
La requête suivante ne garde que les lignes associées à un ticket résolu. Elle retourne Nord et Est ; Sud et Ouest disparaissent. Elle répond donc à une autre question : « quelles équipes ont au moins un ticket résolu ? ».
SELECT e.id, e.nom, COUNT(t.id) AS tickets_resolus
FROM equipes AS e
LEFT JOIN tickets AS t ON t.equipe_id = e.id
WHERE t.statut = 'resolu'
GROUP BY e.id, e.nom
ORDER BY e.id;Sud a bien une correspondance : le ticket 104, mais il est ouvert. Cette ligne ne passe pas le filtre. Ouest n’a aucune correspondance ; les colonnes de ticket sont remplies avec NULL, qui représente ici une valeur absente. Le test de statut ne vaut pas vrai pour cette ligne.
La documentation PostgreSQL sur les expressions de table distingue la condition de jointure de la sélection par WHERE. Cette explication décrit la logique du résultat, pas une promesse sur l’ordre physique des opérations du moteur.
Dans ce rapport, l’équipe Sud devrait avoir zéro ticket résolu. Si elle disparaît, le lecteur ne sait plus si elle a été oubliée, exclue ou supprimée du fichier.
Où mettre le filtre pour conserver les quatre équipes ?
Placez le critère de résolution dans ON : il définit quels tickets peuvent être associés à une équipe. Si aucun ticket ne convient, l’équipe reste présente sans ticket associé. Le comptage donne alors zéro.
SELECT e.id, e.nom, COUNT(t.id) AS tickets_resolus
FROM equipes AS e
LEFT JOIN tickets AS t
ON t.equipe_id = e.id
AND t.statut = 'resolu'
GROUP BY e.id, e.nom
ORDER BY e.id;| Équipe | Tickets résolus |
|---|---|
| Nord | 2 |
| Sud | 0 |
| Ouest | 0 |
| Est | 1 |
La documentation officielle SELECT de SQLite explique cette différence pour une jointure externe. Notre exemple permet de la vérifier : le changement de clause restaure Sud et Ouest, sans ajouter de ticket résolu.
Ne déplacez pas tous les filtres automatiquement. Si vous souhaitez seulement certaines équipes, un filtre sur leurs colonnes dans WHERE peut être voulu. Et si vous voulez uniquement les équipes ayant résolu un ticket, leur disparition est légitime : nommez ce périmètre clairement.
Pourquoi compter t.id plutôt que COUNT(*) ?
COUNT(t.id) compte les valeurs non nulles de t.id dans chaque groupe. COUNT(*) compte toutes les lignes du résultat de la jointure, y compris la ligne conservée pour une équipe sans ticket admissible.
Sur la requête corrigée, remplacer COUNT(t.id) par COUNT(*) donne Nord 2, Sud 1, Ouest 1, Est 1. Sud et Ouest ont chacun une ligne, mais aucun ticket résolu. Ce « 1 » ne mesure donc pas une résolution.
Les documentations COUNT de SQLite et fonctions d’agrégation de PostgreSQL précisent ce que les deux formes comptent. Ici, t.id convient parce que chaque ticket réel possède cet identifiant non nul.
Ne choisissez pas une colonne facultative, comme une note laissée par l’utilisateur : vous pourriez ne pas compter certains tickets réels. Pour comprendre le regroupement, consultez notre guide GROUP BY.
Exercice : ajouter OR IS NULL suffit-il à réparer WHERE ?
Non, dans ce jeu de données. Avant de lire la correction, prédisez les équipes qui resteront avec cette tentative :
SELECT e.id, e.nom, COUNT(t.id) AS tickets_resolus
FROM equipes AS e
LEFT JOIN tickets AS t ON t.equipe_id = e.id
WHERE t.statut = 'resolu' OR t.id IS NULL
GROUP BY e.id, e.nom
ORDER BY e.id;Afficher la correction du contre-exemple
Vous obtenez Nord 2, Ouest 0, Est 1. Sud manque encore. Son ticket ouvert possède un identifiant non nul : ni le statut demandé ni le test IS NULL ne sont satisfaits.
Une correspondance rejetée par WHERE ne devient pas ensuite une nouvelle ligne sans correspondance. Pour notre besoin, gardez le critère de résolution dans ON.
Deuxième test : ajoutez maintenant un premier ticket résolu à Sud, puis relancez la requête avec le filtre dans ON et COUNT(t.id), présentée dans la section de correction.
INSERT INTO tickets VALUES (106, 2, 'resolu');Afficher le résultat après ajout du ticket 106
Le résultat devient Nord 2, Sud 1, Ouest 0, Est 1. Il reste quatre équipes, et la somme passe de trois à quatre tickets résolus. Ouest conserve son zéro.
Ce test contrôle deux propriétés : ajouter une résolution modifie le bon compteur, sans changer la liste des équipes. Les résultats sont vérifiés sur ce jeu ; ils ne dispensent pas de contrôler vos données réelles.
Quels contrôles faire avant de partager le rapport ?
Contrôlez les noms des équipes, le nombre de lignes attendu et la somme des tickets résolus. Sur le jeu initial, attendez quatre équipes et trois résolutions ; après l’ajout du ticket 106, quatre équipes et quatre résolutions.
Ces cases sont une aide locale et se réinitialisent au rechargement. Vérifiez aussi l’unicité des identifiants et les liens entre équipes et tickets. Une autre jointure qui multiplie les lignes peut gonfler les comptes ; déplacer le filtre ne résout pas tous les problèmes de données.
Un zéro signifie ici « aucun ticket marqué résolu dans les données retenues ». Il ne signifie pas « aucune activité » et ne permet pas de juger une équipe. Si l’export est incomplet, signalez-le. Reprenez nos contrôles avant de présenter un chiffre pour examiner le périmètre et les exclusions.
Questions fréquentes sur LEFT JOIN et WHERE
LEFT JOIN et LEFT OUTER JOIN sont-ils différents ?
Dans SQLite et PostgreSQL, OUTER est facultatif : les deux écritures désignent cette même jointure externe à gauche.
Faut-il toujours éviter WHERE après LEFT JOIN ?
Non. Demandez-vous quelles lignes le filtre doit retirer. Le problème apparaît ici parce que le filtre sur les tickets contredit la promesse de conserver toutes les équipes.
Dois-je remplacer tous les NULL par zéro ?
Non. Le compteur donne déjà zéro pour les identifiants absents. Dans d’autres colonnes, une valeur inconnue et un vrai zéro peuvent avoir des sens différents. Expliquez ce que mesure votre résultat avant de changer l’affichage.
Jeu de données original entièrement fictif. Requêtes vérifiées avec SQLite 3.51.2 ; documentations SQLite et PostgreSQL consultées le 5 octobre 2026.





