Dans cet article
- Un WHERE sur la table droite après un LEFT JOIN transforme silencieusement la requête en INNER JOIN dans la majorité des cas
- La solution consiste à déplacer la condition dans la clause ON pour conserver les lignes sans correspondance
- La différence de résultats entre ON et WHERE peut atteindre 30 à 60 % de lignes perdues sur des jeux de données réels
- L’ordre d’évaluation SQL suit la séquence FROM → ON → JOIN → WHERE → SELECT, ce qui explique le piège
- Les cas d’usage légitimes du WHERE après un LEFT JOIN incluent la recherche de lignes orphelines avec IS NULL
- Les performances varient selon le SGBD : MySQL, PostgreSQL et SQL Server optimisent différemment ces combinaisons
Sommaire
- Comprendre le LEFT JOIN et la clause WHERE
- L’ordre d’évaluation d’une requête SQL
- Le piège classique : WHERE sur la table droite
- Condition dans ON vs condition dans WHERE
- Cas d’usage légitimes du WHERE avec LEFT JOIN
- Exemples pratiques avec MySQL et PostgreSQL
- Performances et optimisation des requêtes
- Erreurs fréquentes et bonnes pratiques
Je vois régulièrement mes étudiants en BTS SIO tomber dans le même piège : ils écrivent un LEFT JOIN impeccable, ajoutent une clause WHERE sur la table de droite, et perdent sans comprendre toutes les lignes qui n’ont pas de correspondance. Cette confusion entre ON et WHERE est probablement l’erreur SQL la plus répandue, y compris chez des développeurs expérimentés. Je vous propose de décortiquer ce mécanisme pour que vous ne fassiez plus jamais cette erreur.
Comprendre le LEFT JOIN et la clause WHERE
Le LEFT JOIN (ou LEFT OUTER JOIN, les deux écritures sont synonymes) est une jointure qui retourne toutes les lignes de la table de gauche, même si aucune correspondance n’existe dans la table de droite. Quand il n’y a pas de correspondance, les colonnes de la table droite sont remplies avec des valeurs NULL.
Prenons un exemple concret avec deux tables. Je travaille sur une base de données d’une école :
-- Table des étudiants
CREATE TABLE etudiants (
id INT PRIMARY KEY,
nom VARCHAR(100),
prenom VARCHAR(100)
);
-- Table des notes
CREATE TABLE notes (
id INT PRIMARY KEY,
etudiant_id INT,
matiere VARCHAR(50),
note DECIMAL(4,2)
);
Avec les données suivantes :
INSERT INTO etudiants VALUES (1, 'Dupont', 'Marie');
INSERT INTO etudiants VALUES (2, 'Martin', 'Lucas');
INSERT INTO etudiants VALUES (3, 'Bernard', 'Sophie');
INSERT INTO notes VALUES (1, 1, 'SQL', 15.5);
INSERT INTO notes VALUES (2, 1, 'PHP', 12.0);
INSERT INTO notes VALUES (3, 2, 'SQL', 18.0);
Sophie Bernard n’a aucune note. Un LEFT JOIN classique me permet de la voir quand même :
SELECT e.nom, e.prenom, n.matiere, n.note
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id;
Résultat :
| nom | prenom | matiere | note |
|---------|--------|---------|-------|
| Dupont | Marie | SQL | 15.50 |
| Dupont | Marie | PHP | 12.00 |
| Martin | Lucas | SQL | 18.00 |
| Bernard | Sophie | NULL | NULL |
La clause WHERE, quant à elle, filtre les résultats après que la jointure a été effectuée. C’est précisément cet ordre d’exécution qui crée le piège que nous allons analyser.

L’ordre d’évaluation d’une requête SQL
Pour comprendre le problème, il faut connaître l’ordre logique d’évaluation d’une requête SQL. Contrairement à ce que l’écriture syntaxique suggère, le SELECT n’est pas traité en premier. Voici l’ordre réel, tel que décrit dans la spécification SQL de la norme ISO/IEC 9075 :
- FROM : identification des tables sources
- ON : évaluation de la condition de jointure
- JOIN : exécution de la jointure (ajout des NULL pour le LEFT JOIN)
- WHERE : filtrage des lignes résultantes
- GROUP BY : regroupement
- HAVING : filtrage des groupes
- SELECT : sélection des colonnes
- ORDER BY : tri du résultat final
Le point crucial se situe entre les étapes 3 et 4. Le LEFT JOIN produit d’abord son résultat complet, avec les lignes NULL incluses. Ensuite seulement, le WHERE filtre ce résultat. Si votre clause WHERE teste une colonne de la table droite (qui contient NULL pour les non-correspondances), ces lignes sont automatiquement éliminées, car NULL ne satisfait aucune comparaison classique.
C’est un concept fondamental que je recommande de bien maîtriser avant de passer aux exercices SQL pratiques.
Le piège classique : WHERE sur la table droite
Reprenons notre exemple. Je veux afficher tous les étudiants avec leurs notes en SQL uniquement. Voici la requête que beaucoup écrivent instinctivement :
-- PIÈGE : cette requête élimine Sophie
SELECT e.nom, e.prenom, n.matiere, n.note
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id
WHERE n.matiere = 'SQL';
Résultat obtenu :
| nom | prenom | matiere | note |
|--------|--------|---------|-------|
| Dupont | Marie | SQL | 15.50 |
| Martin | Lucas | SQL | 18.00 |
Sophie a disparu. Le LEFT JOIN a bien fonctionné, il a généré une ligne avec NULL pour Sophie. Mais la clause WHERE n.matiere = 'SQL' a ensuite éliminé cette ligne, car NULL = ‘SQL’ renvoie UNKNOWN, qui est traité comme FALSE par le WHERE.
En pratique, votre LEFT JOIN s’est comporté exactement comme un INNER JOIN. Vous avez perdu tout l’intérêt de la jointure gauche. Sur des jeux de données de production, j’ai vu des développeurs perdre entre 30 et 60 % des lignes attendues à cause de ce piège, sans s’en rendre compte pendant des mois.
| Requête | Comportement réel | Sophie Bernard visible ? | Nombre de lignes |
|---|---|---|---|
| LEFT JOIN sans WHERE | LEFT JOIN authentique | Oui (avec NULL) | 4 |
| LEFT JOIN + WHERE n.matiere = ‘SQL’ | Équivalent à un INNER JOIN | Non | 2 |
| LEFT JOIN + ON n.matiere = ‘SQL’ | LEFT JOIN avec filtre intégré | Oui (avec NULL) | 3 |
| LEFT JOIN + WHERE n.matiere = ‘SQL’ OR n.matiere IS NULL | LEFT JOIN avec contournement | Oui (avec NULL) | 3 |
Condition dans ON vs condition dans WHERE
La solution au problème précédent est simple : déplacer la condition de filtrage dans la clause ON. Voici la requête corrigée :
-- CORRECT : Sophie reste visible
SELECT e.nom, e.prenom, n.matiere, n.note
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id AND n.matiere = 'SQL';
Résultat :
| nom | prenom | matiere | note |
|---------|--------|---------|-------|
| Dupont | Marie | SQL | 15.50 |
| Martin | Lucas | SQL | 18.00 |
| Bernard | Sophie | NULL | NULL |
Cette fois, la condition n.matiere = 'SQL' est évaluée pendant la jointure (étape 2), pas après. Le moteur SQL cherche dans la table notes les lignes qui correspondent à l’étudiant ET dont la matière est ‘SQL’. S’il ne trouve pas de correspondance, il génère quand même la ligne avec des NULL, car c’est le comportement garanti du LEFT JOIN.
Voici la règle que je donne à mes étudiants, et qui fonctionne dans 100 % des cas :
- Condition sur la table de gauche → mettre dans le WHERE (on veut filtrer les lignes principales)
- Condition sur la table de droite → mettre dans le ON (on veut restreindre la correspondance sans perdre les lignes de gauche)
Avec un INNER JOIN, la distinction n’a pas d’importance : mettre la condition dans le ON ou dans le WHERE produit le même résultat. C’est uniquement avec les jointures externes (LEFT JOIN, RIGHT JOIN, FULL JOIN) que la position de la condition change le résultat.

Cas d’usage légitimes du WHERE avec LEFT JOIN
Il existe cependant des situations où utiliser WHERE après un LEFT JOIN est non seulement correct, mais souhaitable. Le cas le plus courant est la recherche de lignes orphelines :
Trouver les enregistrements sans correspondance (IS NULL)
-- Étudiants qui n'ont AUCUNE note
SELECT e.nom, e.prenom
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id
WHERE n.id IS NULL;
Ici, le WHERE n.id IS NULL est intentionnel. Je veux précisément les lignes où la jointure n’a trouvé aucune correspondance. Ce pattern est extrêmement courant et souvent plus performant qu’un NOT EXISTS ou NOT IN selon le SGBD utilisé.
Filtrer sur la table de gauche
-- Tous les étudiants dont le nom commence par 'D', avec leurs notes
SELECT e.nom, e.prenom, n.matiere, n.note
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id
WHERE e.nom LIKE 'D%';
Ce WHERE filtre la table de gauche. Il ne compromet pas le LEFT JOIN car il ne teste pas une colonne de la table droite. Les lignes sans correspondance dans notes restent visibles avec des NULL.
Combiner filtre gauche et filtre droit
Quand vous devez filtrer les deux tables, la stratégie mixte est la bonne approche :
-- Étudiants actifs, avec leurs notes en SQL (s'ils en ont)
SELECT e.nom, e.prenom, n.matiere, n.note
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id AND n.matiere = 'SQL'
WHERE e.prenom LIKE 'M%';
La condition sur la table droite (n.matiere = 'SQL') va dans le ON. La condition sur la table gauche (e.prenom LIKE 'M%') va dans le WHERE. Ce pattern est la combinaison correcte qui préserve la sémantique du LEFT JOIN tout en appliquant les filtres voulus.
Exemples pratiques avec MySQL et PostgreSQL
Passons à des scénarios plus réalistes. Je vais utiliser un schéma de commerce en ligne, typique des projets que je supervise en BTS SIO.
CREATE TABLE clients (
id INT PRIMARY KEY,
nom VARCHAR(100),
ville VARCHAR(50),
date_inscription DATE
);
CREATE TABLE commandes (
id INT PRIMARY KEY,
client_id INT,
montant DECIMAL(10,2),
statut VARCHAR(20),
date_commande DATE
);
Exemple 1 : clients avec et sans commandes récentes
-- Tous les clients, avec leurs commandes de 2026 (si elles existent)
SELECT c.nom, c.ville,
COUNT(co.id) AS nb_commandes,
COALESCE(SUM(co.montant), 0) AS total_achats
FROM clients c
LEFT JOIN commandes co ON c.id = co.client_id
AND co.date_commande >= '2026-01-01'
GROUP BY c.id, c.nom, c.ville
ORDER BY total_achats DESC;
La condition de date est placée dans le ON, pas dans le WHERE. Tous les clients apparaissent, même ceux qui n’ont pas commandé en 2026. La fonction COALESCE remplace les NULL par 0 pour l’affichage.
Exemple 2 : identifier les clients inactifs
-- Clients inscrits depuis plus d'un an qui n'ont jamais commandé
SELECT c.nom, c.ville, c.date_inscription
FROM clients c
LEFT JOIN commandes co ON c.id = co.client_id
WHERE co.id IS NULL
AND c.date_inscription < '2025-06-01'
ORDER BY c.date_inscription;
Ici le WHERE est parfaitement légitime : co.id IS NULL exploite volontairement l'absence de correspondance, et c.date_inscription filtre la table de gauche.
Exemple 3 : LEFT JOIN multiple avec filtres mixtes
-- Clients avec commandes livrées et paiements associés
SELECT c.nom,
co.id AS commande_id,
co.montant,
p.methode_paiement
FROM clients c
LEFT JOIN commandes co ON c.id = co.client_id
AND co.statut = 'livree'
LEFT JOIN paiements p ON co.id = p.commande_id
AND p.valide = 1
WHERE c.ville = 'Paris'
ORDER BY c.nom;
Chaque LEFT JOIN a ses propres conditions dans le ON. Le WHERE ne filtre que la table principale clients. Ce pattern se généralise à autant de jointures que nécessaire.
Si vous souhaitez combiner ces requêtes avec des opérations ensemblistes, consultez mon guide sur SQL UNION qui complète parfaitement ce sujet.

Performances et optimisation des requêtes
La position de la condition (ON vs WHERE) peut avoir un impact significatif sur les performances, selon le moteur SQL utilisé. Voici ce que j'observe en pratique.
Plan d'exécution
Utilisez EXPLAIN (ou EXPLAIN ANALYZE sous PostgreSQL) pour vérifier comment le moteur interprète votre requête. Selon la documentation officielle de PostgreSQL sur EXPLAIN, l'optimiseur peut parfois réorganiser les conditions, mais pas toujours de manière optimale avec un LEFT JOIN.
-- PostgreSQL
EXPLAIN ANALYZE
SELECT e.nom, n.note
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id AND n.matiere = 'SQL';
Indexation
Pour optimiser un LEFT JOIN avec condition, créez des index composites qui couvrent la clé de jointure et la colonne filtrée :
-- Index optimisé pour le LEFT JOIN avec filtre sur matière
CREATE INDEX idx_notes_etudiant_matiere
ON notes (etudiant_id, matiere);
Cet index permet au moteur de résoudre la jointure et le filtre en une seule opération de lecture d'index, sans accéder à la table notes elle-même (index-only scan).
Comparaison des performances par SGBD
| SGBD | Condition dans ON | Condition dans WHERE | Réécriture automatique ? |
|---|---|---|---|
| MySQL 8.x | Filtre pendant le scan de jointure | Filtre post-jointure, transforme en INNER JOIN | Non pour LEFT JOIN |
| PostgreSQL 15+ | Pushdown dans le nœud de jointure | Nœud Filter séparé | Non pour LEFT JOIN |
| SQL Server 2022 | Intégré au Hash/Merge Join | Filter operator séparé | Parfois (simplex queries) |
| SQLite | Filtre inline | Filtre post-jointure | Non |
Les informations spécifiques à SQL Server 2022 montrent que cet optimiseur est le seul à tenter parfois une réécriture, mais uniquement dans des cas simples. En règle générale, ne comptez pas sur l'optimiseur pour corriger une condition mal placée dans un LEFT JOIN.
Sous-requête vs LEFT JOIN filtré
Une alternative au LEFT JOIN avec condition dans le ON est d'utiliser une sous-requête pré-filtrée :
-- Alternative avec sous-requête
SELECT e.nom, e.prenom, sub.note
FROM etudiants e
LEFT JOIN (
SELECT etudiant_id, note
FROM notes
WHERE matiere = 'SQL'
) sub ON e.id = sub.etudiant_id;
Cette approche produit le même résultat et peut être plus lisible quand les conditions de filtrage sont complexes. En termes de performances, les deux approches sont généralement équivalentes grâce à l'inlining des sous-requêtes par l'optimiseur.
Erreurs fréquentes et bonnes pratiques
Après des années de corrections de projets étudiants et d'audits de code en entreprise, voici les erreurs que je rencontre le plus souvent et les bonnes pratiques que je recommande.
Erreur 1 : OR dans le WHERE sur la table droite
-- INCORRECT : résultat imprévisible
SELECT e.nom, n.matiere, n.note
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id
WHERE n.matiere = 'SQL' OR n.matiere = 'PHP';
-- CORRECT
SELECT e.nom, n.matiere, n.note
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id
AND n.matiere IN ('SQL', 'PHP');
Erreur 2 : oublier le IS NULL dans le contournement
-- INCORRECT : oubli du cas NULL
SELECT e.nom, n.note
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id
WHERE n.matiere = 'SQL' OR n.note > 10;
-- CORRECT : inclure explicitement les NULL
SELECT e.nom, n.note
FROM etudiants e
LEFT JOIN notes n ON e.id = n.etudiant_id
AND (n.matiere = 'SQL' OR n.note > 10);
Erreur 3 : mélanger INNER JOIN et LEFT JOIN sans réflexion
Quand vous chaînez un INNER JOIN après un LEFT JOIN, l'INNER JOIN peut éliminer les lignes NULL produites par le LEFT JOIN. Pensez à utiliser des LEFT JOIN en cascade si vous devez conserver toutes les lignes de la table principale.
Checklist avant de valider une requête LEFT JOIN + WHERE
- Vérifiez que chaque condition WHERE porte sur la table de gauche ou utilise IS NULL
- Déplacez dans le ON toute condition portant sur une colonne de la table de droite
- Testez avec un jeu de données contenant des lignes sans correspondance
- Utilisez
EXPLAINpour vérifier que l'optimiseur n'a pas transformé votre LEFT JOIN en INNER JOIN - Nommez vos alias de table de façon explicite pour éviter toute ambiguïté
Pour mettre en pratique ces concepts, je vous recommande de travailler sur les exercices SQL que j'ai préparés. Ils couvrent spécifiquement les jointures avec filtres, et vous pouvez vérifier vos résultats pas à pas.
Le sujet des jointures ouvre naturellement la porte à d'autres opérations ensemblistes comme le SQL UNION, qui permet de combiner les résultats de plusieurs requêtes SELECT. Maîtriser ces deux concepts ensemble vous donne une boîte à outils solide pour interroger n'importe quelle base relationnelle.
À retenir
- Placez toute condition sur la table droite dans le ON, jamais dans le WHERE, pour préserver le LEFT JOIN
- Utilisez
WHERE table_droite.colonne IS NULLuniquement pour trouver les lignes orphelines - Créez un index composite sur (clé_jointure, colonne_filtrée) pour optimiser les performances
- Testez systématiquement avec des données contenant des lignes sans correspondance pour valider votre requête
- Utilisez
EXPLAIN ANALYZEpour vérifier que le moteur n'a pas transformé votre LEFT JOIN en INNER JOIN
Questions fréquentes
Quelle est la différence entre LEFT JOIN et LEFT OUTER JOIN ?
Il n'y a aucune différence. LEFT JOIN et LEFT OUTER JOIN sont deux écritures strictement synonymes dans la norme SQL. Le mot OUTER est optionnel. J'utilise personnellement LEFT JOIN par souci de concision, mais les deux formes produisent exactement le même résultat sur tous les SGBD (MySQL, PostgreSQL, SQL Server, Oracle, SQLite).
Dans 95 % des cas, c'est parce que votre clause WHERE contient une condition sur une colonne de la table de droite (autre que IS NULL). Cette condition élimine les lignes où la table droite contient des NULL, ce qui revient à supprimer les lignes sans correspondance. La solution est de déplacer cette condition dans la clause ON de la jointure.Pourquoi mon LEFT JOIN se comporte comme un INNER JOIN ?
Oui, vous pouvez combiner autant de conditions que nécessaire dans le ON avec les opérateurs AND et OR. Par exemple : Peut-on utiliser plusieurs conditions dans la clause ON d'un LEFT JOIN ?
LEFT JOIN notes n ON e.id = n.etudiant_id AND n.matiere = 'SQL' AND n.note >= 10. Toutes ces conditions sont évaluées pendant la jointure, avant la génération des lignes NULL.
En théorie, un LEFT JOIN peut être légèrement plus lent car le moteur doit conserver les lignes sans correspondance et générer des NULL. En pratique, la différence est souvent négligeable si les index sont correctement configurés. Le choix entre LEFT JOIN et INNER JOIN doit être dicté par la logique métier (voulez-vous voir les lignes sans correspondance ?), pas par les performances.Le LEFT JOIN est-il plus lent que l'INNER JOIN ?
Deux approches fonctionnent. La première consiste à placer la condition de filtrage dans la clause ON : Comment filtrer sur la table droite d'un LEFT JOIN sans perdre les lignes NULL ?
LEFT JOIN notes n ON e.id = n.etudiant_id AND n.matiere = 'SQL'. La seconde utilise une sous-requête pré-filtrée : LEFT JOIN (SELECT * FROM notes WHERE matiere = 'SQL') sub ON e.id = sub.etudiant_id. Les deux approches produisent le même résultat et ont des performances comparables.
Le LEFT JOIN conserve toutes les lignes de la table de gauche (celle écrite avant le mot JOIN). Le RIGHT JOIN conserve toutes les lignes de la table de droite (celle écrite après le mot JOIN). En pratique, tout RIGHT JOIN peut être réécrit en LEFT JOIN en inversant l'ordre des tables. La majorité des développeurs n'utilisent que le LEFT JOIN par convention de lisibilité.Quelle est la différence entre LEFT JOIN et RIGHT JOIN ?
Formatrice IT indépendante depuis 2016, ancienne étudiante BTS SIO SLAM. 6 ans d'expérience en entreprise.