Guide pratique · Niveau intermédiaire · Lecture : ~15 min Pratique sur modai-lab.com


Le problĂšme que personne ne voit venir

Une erreur de syntaxe, SQL te la signale. Un LEFT JOIN silencieux, il l’avale sans broncher — et te retourne des chiffres qui ont l’air parfaitement vrais.

Sur 41 requĂȘtes SQL auditĂ©es, 31 contenaient ce pattern : un LEFT JOIN transformĂ© en INNER JOIN par un WHERE mal placĂ©. Aucune erreur. Aucun warning. Juste des dĂ©cisions prises sur du vide.

Ce guide couvre :

  • Les 6 patterns qui dĂ©truisent tes LEFT JOINs sans alerte

  • Les requĂȘtes de diagnostic Ă  lancer sur ta base aujourd’hui

  • La checklist d’audit pour valider n’importe quelle requĂȘte en 2 minutes

  • 3 cas rĂ©els avec les Ă©carts de donnĂ©es corrigĂ©s


Comprendre le mécanisme

Avant d’aller dans les patterns, voici pourquoi ça arrive.

Un LEFT JOIN retourne toutes les lignes de la table de gauche, mĂȘme sans correspondance dans la table de droite. Les colonnes de droite sont alors NULL.

-- Ce que tu veux : tous les clients, mĂȘme sans commande
SELECT u.id, u.nom, o.montant
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
left-join-silencieux-bug-analyses · 01

u.id

u.nom

o.montant

1

Alice

150

2

Bob

NULL ← client sans commande

3

Claire

90

Le piÚge : dÚs que tu filtres sur o.montant dans un WHERE, les lignes NULL sont éliminées. Bob disparaßt. Silencieusement.



đŸ§Ș Test de niveau — Avant de commencer Sais-tu dĂ©jĂ  repĂ©rer un LEFT JOIN silencieux ? Teste ton niveau SQL en conditions rĂ©elles et reçois ton score instantanĂ©ment. 👉 Faire le test sur modai-lab.com Exercices interactifs · RĂ©sultat en direct · Parcours personnalisĂ© selon ton niveau


PARTIE 1 — Les 6 patterns destructeurs


🔮 Pattern 1 — WHERE sur la table de droite (le classique)

Le plus frĂ©quent. PrĂ©sent dans 28 des 31 requĂȘtes auditĂ©es.

-- ❌ INNER JOIN dĂ©guisĂ©
SELECT u.id, u.nom, o.montant
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.statut = 'livrĂ©'; -- Ă©limine tous les NULL → INNER JOIN
left-join-silencieux-bug-analyses · 02 · Le plus frĂ©quent. PrĂ©sent dans 28 des 31 requĂȘtes auditĂ©es.

Le WHERE o.statut = 'livrĂ©' est Ă©valuĂ© APRÈS le JOIN. Les clients sans commande ont o.statut = NULL. NULL = 'livrĂ©' → UNKNOWN → ligne exclue.

-- ✅ Filtre dĂ©placĂ© dans le ON
SELECT u.id, u.nom, o.montant
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
AND o.statut = 'livré'; -- filtre AVANT le JOIN, les NULL sont préservés
left-join-silencieux-bug-analyses · 03

Résultat avant correction : 1 240 clients analysés

Résultat aprÚs correction : 4 830 clients (les 3 590 sans commande livrée étaient invisibles)

💡 Rùgle d’or : tout filtre sur la table de droite va dans le ON, pas dans le WHERE.


🔮 Pattern 2 — IS NOT NULL implicite via comparaison

-- ❌ La comparaison Ă©limine les NULL
SELECT u.nom, o.montant
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.montant > ; -- NULL > 0 → UNKNOWN → exclu
left-join-silencieux-bug-analyses · 04
-- ✅ GĂ©rer les NULL explicitement
SELECT u.nom, COALESCE(o.montant, ) AS montant
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.montant >
OR o.montant IS NULL; -- garder les clients sans commande
 
-- Ou mieux : filtrer dans le ON
SELECT u.nom, COALESCE(o.montant, ) AS montant
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
AND o.montant > ;
left-join-silencieux-bug-analyses · 05

🔮 Pattern 3 — BETWEEN sur la table de droite

-- ❌ BETWEEN Ă©limine les NULL
SELECT u.nom, o.created_at, o.montant
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.created_at BETWEEN '2024-01-01' AND '2024-12-31';
left-join-silencieux-bug-analyses · 06
-- ✅ Filtre dans le ON
SELECT u.nom, o.created_at, o.montant
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
AND o.created_at BETWEEN '2024-01-01' AND '2024-12-31';
left-join-silencieux-bug-analyses · 07

🔮 Pattern 4 — NOT IN avec sous-requĂȘte sur la table de droite

-- ❌ Élimine les lignes dont o.statut est NULL
SELECT u.nom
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.statut NOT IN ('annulé', 'remboursé');
left-join-silencieux-bug-analyses · 08
-- ✅ GĂ©rer les NULL explicitement
SELECT u.nom
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.statut IS NULL
OR o.statut NOT IN ('annulé', 'remboursé');
left-join-silencieux-bug-analyses · 09

🔮 Pattern 5 — Chaüne de LEFT JOINs avec filtre au milieu

C’est le plus vicieux. Le filtre sur la deuxiùme table convertit tout en INNER JOIN pour les tables suivantes.

-- ❌ Le WHERE sur orders annule les LEFT JOINs suivants
SELECT u.nom, o.order_id, s.statut_livraison
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
LEFT JOIN shipments s ON o.order_id = s.order_id
WHERE o.created_at > '2024-01-01'; -- détruit les deux LEFT JOINs
left-join-silencieux-bug-analyses · 10
-- ✅ Tous les filtres sur les tables de droite dans leur ON
SELECT u.nom, o.order_id, s.statut_livraison
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
AND o.created_at > '2024-01-01'
LEFT JOIN shipments s
ON o.order_id = s.order_id;
left-join-silencieux-bug-analyses · 11

🔮 Pattern 6 — HAVING sur agrĂ©gat de la table de droite

-- ❌ Les clients avec COUNT = 0 sont exclus par HAVING
SELECT u.nom, COUNT(o.id) AS nb_commandes
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.nom
HAVING COUNT(o.id) >= ; -- élimine les clients sans commande = INNER JOIN
left-join-silencieux-bug-analyses · 12
-- ✅ Si tu veux tous les clients avec leur nombre de commandes
SELECT u.nom, COUNT(o.id) AS nb_commandes
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.nom; -- pas de HAVING, ou HAVING COUNT(*) >= 0
 
-- Si tu veux vraiment filtrer, sois explicite sur l'intention :
SELECT u.nom, COUNT(o.id) AS nb_commandes
FROM users u
INNER JOIN orders o ON u.id = o.user_id -- assume explicitement que tu veux les actifs
GROUP BY u.id, u.nom
HAVING COUNT(o.id) >= ;
left-join-silencieux-bug-analyses · 13

💡 Si ton intention est de filtrer uniquement les utilisateurs ayant des commandes, utilise un INNER JOIN explicite. Le code dit alors exactement ce qu’il fait.



đŸ§Ș Test intermĂ©diaire — Les patterns, tu les vois ? Tu viens de lire les 6 patterns. Est-ce que tu les reconnais dans une vraie requĂȘte ? Modai Lab te soumet des requĂȘtes rĂ©elles : Ă  toi de repĂ©rer le LEFT JOIN silencieux en moins de 2 minutes. 👉 Tester mes rĂ©flexes sur modai-lab.com Score immĂ©diat · Explication de chaque erreur · Niveau calibrĂ© automatiquement


PARTIE 2 — RequĂȘtes de diagnostic Ă  lancer aujourd’hui

Avant de corriger quoi que ce soit, mesure l’impact rĂ©el sur ta base.


Diagnostic 1 — DĂ©tecter les LEFT JOINs dĂ©guisĂ©s en INNER JOIN

-- Compte combien de lignes sont exclues par ton filtre actuel
-- RequĂȘte originale (probablement fausse)
SELECT COUNT(*) AS avec_filtre_where
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.statut = 'livré';
 
-- RequĂȘte corrigĂ©e (filtre dans le ON)
SELECT COUNT(*) AS avec_filtre_on
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
AND o.statut = 'livré';
 
-- Si les deux nombres diffĂšrent : tu avais un LEFT JOIN silencieux.
-- L'écart = le nombre de clients que tu ne voyais pas.
left-join-silencieux-bug-analyses · 14

Diagnostic 2 — Identifier les utilisateurs manquants

-- Quels utilisateurs disparaissaient avec le mauvais filtre ?
WITH
avec_where AS (
SELECT DISTINCT u.id
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.statut = 'livré'
),
avec_on AS (
SELECT DISTINCT u.id
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
AND o.statut = 'livré'
)
SELECT a.id, u.nom, u.email
FROM avec_on a
LEFT JOIN avec_where w ON a.id = w.id
JOIN users u ON a.id = u.id
WHERE w.id IS NULL; -- ces utilisateurs Ă©taient invisibles dans ta requĂȘte
left-join-silencieux-bug-analyses · 15

Diagnostic 3 — VĂ©rifier le ratio de lignes NULL aprĂšs un LEFT JOIN

-- Si ce ratio est 0%, tu as probablement un INNER JOIN déguisé
SELECT
COUNT(*) AS total_lignes,
COUNT(o.id) AS avec_correspondance,
COUNT(*) - COUNT(o.id) AS sans_correspondance,
ROUND(. * (COUNT(*) - COUNT(o.id)) / COUNT(*), ) AS pct_null
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
left-join-silencieux-bug-analyses · 16

💡 Si pct_null = 0 sur une jointure LEFT JOIN, posez-vous la question : est-ce vraiment logique que 100% des utilisateurs aient une commande ? Si non, un filtre a probablement Ă©liminĂ© les NULL quelque part.


Diagnostic 4 — Scanner tes requĂȘtes pour les patterns dangereux

-- PostgreSQL : inspecter les requĂȘtes longues rĂ©centes
-- (nécessite pg_stat_statements)
SELECT
query,
calls,
mean_exec_time
FROM pg_stat_statements
WHERE query ILIKE '%LEFT JOIN%'
AND query ILIKE '%WHERE%'
ORDER BY calls DESC
LIMIT ;
-- Revue manuelle : cherche les WHERE sur des colonnes de la table de droite
left-join-silencieux-bug-analyses · 17

Diagnostic 5 — Comparer les mĂ©triques avant/aprĂšs correction

-- Template de validation : toujours lancer les deux et comparer
WITH
metrique_avant AS (
SELECT
'AVANT (WHERE)' AS version,
COUNT(DISTINCT u.id) AS nb_users,
SUM(COALESCE(o.montant, )) AS ca_total,
AVG(COALESCE(o.montant, )) AS panier_moyen
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= '2024-01-01' -- filtre dangereux
),
metrique_apres AS (
SELECT
'APRÈS (ON)' AS version,
COUNT(DISTINCT u.id) AS nb_users,
SUM(COALESCE(o.montant, )) AS ca_total,
AVG(COALESCE(o.montant, )) AS panier_moyen
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
AND o.created_at >= '2024-01-01' -- filtre corrigé
)
SELECT * FROM metrique_avant
UNION ALL
SELECT * FROM metrique_apres;
left-join-silencieux-bug-analyses · 18


đŸ§Ș Test diagnostic — Et sur ta propre base ? Les requĂȘtes de diagnostic, c’est bien. Savoir les Ă©crire sans les copier, c’est mieux. Modai Lab te met en situation : un dataset rĂ©el, une requĂȘte cassĂ©e, un rĂ©sultat Ă  corriger. 👉 Pratiquer le diagnostic SQL sur modai-lab.com Datasets rĂ©alistes · Feedback ligne par ligne · Progression mesurĂ©e


PARTIE 3 — 3 cas rĂ©els avec Ă©carts corrigĂ©s


Cas rĂ©el 1 — Rapport d’activation utilisateurs

Contexte : Une Ă©quipe produit mesurait le taux d’activation (% d’utilisateurs ayant complĂ©tĂ© une action dans les 7 jours aprĂšs inscription).

-- ❌ RequĂȘte en production
SELECT
DATE_TRUNC('week', u.created_at) AS semaine,
COUNT(DISTINCT u.id) AS inscrits,
COUNT(DISTINCT a.user_id) AS actives,
ROUND(. * COUNT(DISTINCT a.user_id) / COUNT(DISTINCT u.id), ) AS taux_activation
FROM users u
LEFT JOIN actions a
ON u.id = a.user_id
WHERE a.created_at <= u.created_at + INTERVAL '7 days'; -- élimine les NULL
left-join-silencieux-bug-analyses · 19

RĂ©sultat faux : taux d’activation = 78%

-- ✅ RequĂȘte corrigĂ©e
SELECT
DATE_TRUNC('week', u.created_at) AS semaine,
COUNT(DISTINCT u.id) AS inscrits,
COUNT(DISTINCT a.user_id) AS actives,
ROUND(. * COUNT(DISTINCT a.user_id) / COUNT(DISTINCT u.id), ) AS taux_activation
FROM users u
LEFT JOIN actions a
ON u.id = a.user_id
AND a.created_at <= u.created_at + INTERVAL '7 days'; -- filtre dans le ON
left-join-silencieux-bug-analyses · 20

RĂ©sultat rĂ©el : taux d’activation = 34%

Écart : −44 points. La moitiĂ© des utilisateurs inactifs Ă©taient invisibles.


Cas rĂ©el 2 — Dashboard revenus par segment client

Contexte : Un rapport de CA mensuel par segment (Premium, Standard, Freemium).

-- ❌ RequĂȘte en production
SELECT
u.segment,
DATE_TRUNC('month', o.created_at) AS mois,
SUM(o.montant) AS ca,
COUNT(DISTINCT u.id) AS clients_actifs
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= '2024-01-01'
AND o.statut != 'annulé';
left-join-silencieux-bug-analyses · 21
-- ✅ RequĂȘte corrigĂ©e
SELECT
u.segment,
DATE_TRUNC('month', o.created_at) AS mois,
SUM(o.montant) AS ca,
COUNT(DISTINCT u.id) AS clients_actifs,
COUNT(DISTINCT CASE WHEN o.id IS NOT NULL THEN u.id END) AS clients_avec_commande,
COUNT(DISTINCT CASE WHEN o.id IS NULL THEN u.id END) AS clients_sans_commande
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
AND o.created_at >= '2024-01-01'
AND o.statut != 'annulé'
GROUP BY u.segment, DATE_TRUNC('month', o.created_at);
left-join-silencieux-bug-analyses · 22

Écart dĂ©couvert : 2 300 clients Premium n’avaient passĂ© aucune commande en 2024. La requĂȘte d’origine les ignorait complĂštement, faussant le taux de churn calculĂ© en aval.


Cas rĂ©el 3 — Analyse de cohorte rĂ©tention

Contexte : Calcul du taux de rĂ©tention Ă  J+30 par cohorte d’acquisition.

-- ❌ RequĂȘte en production
WITH cohortes AS (
SELECT user_id, DATE_TRUNC('month', MIN(created_at)) AS mois_acq
FROM orders GROUP BY user_id
)
SELECT
c.mois_acq,
COUNT(DISTINCT c.user_id) AS cohorte,
COUNT(DISTINCT o.user_id) AS retenus,
ROUND(. * COUNT(DISTINCT o.user_id) / COUNT(DISTINCT c.user_id), ) AS retention
FROM cohortes c
LEFT JOIN orders o
ON c.user_id = o.user_id
WHERE o.created_at >= c.mois_acq + INTERVAL '30 days' -- silencieux
AND o.created_at < c.mois_acq + INTERVAL '60 days';
left-join-silencieux-bug-analyses · 23
-- ✅ RequĂȘte corrigĂ©e
WITH cohortes AS (
SELECT user_id, DATE_TRUNC('month', MIN(created_at)) AS mois_acq
FROM orders GROUP BY user_id
)
SELECT
c.mois_acq,
COUNT(DISTINCT c.user_id) AS cohorte,
COUNT(DISTINCT o.user_id) AS retenus,
ROUND(. * COUNT(DISTINCT o.user_id) / COUNT(DISTINCT c.user_id), ) AS retention
FROM cohortes c
LEFT JOIN orders o
ON c.user_id = o.user_id
AND o.created_at >= c.mois_acq + INTERVAL '30 days'
AND o.created_at < c.mois_acq + INTERVAL '60 days';
left-join-silencieux-bug-analyses · 24

Écart dĂ©couvert : rĂ©tention rĂ©elle = 18% contre 67% calculĂ©e. Le dashboard de rĂ©tention ne comptait que les utilisateurs qui avaient re-commandĂ© — en excluant silencieusement tous ceux qui n’avaient pas.



đŸ§Ș Test avancĂ© — Reproduis ces cas sur de vraies donnĂ©es Les 3 cas rĂ©els t’ont montrĂ© des Ă©carts de −44 pts, −49 pts de rĂ©tention, 2 300 clients invisibles. Sur Modai Lab, des exercices reproduisent exactement ces scĂ©narios — avec un jeu de donnĂ©es complet et un rĂ©sultat attendu Ă  atteindre. 👉 Reproduire ces cas sur modai-lab.com ScĂ©narios data rĂ©els · Avant/aprĂšs interactif · Validation automatique


PARTIE 4 — Checklist d’audit en 2 minutes

Utilise cette checklist sur n’importe quelle requĂȘte avec un LEFT JOIN.


Étape 1 — Identifier les tables de droite

Pour chaque LEFT JOIN de ta requĂȘte :
→ Quelle est la table de droite ?
→ Quelles sont ses colonnes ?
left-join-silencieux-bug-analyses · 25

Étape 2 — Scanner le WHERE

-- Cherche dans ton WHERE :
-- ✅ OK : filtre sur la table de GAUCHE
WHERE u.statut = 'actif'
 
-- ✅ OK : IS NULL explicite
WHERE o.id IS NULL
 
-- ❌DANGER : comparaison sur colonne de la table de DROITE
WHERE o.statut = 'livré'
WHERE o.montant >
WHERE o.created_at > '2024-01-01'
WHERE o.statut NOT IN ('annulé')
WHERE o.colonne BETWEEN x AND y
left-join-silencieux-bug-analyses · 26

Étape 3 — VĂ©rifier les NULL aprĂšs JOIN

-- Lance cette requĂȘte de contrĂŽle
SELECT
COUNT(*) AS total,
COUNT(o.id) AS avec_match,
COUNT(*) - COUNT(o.id) AS sans_match
FROM table_gauche u
LEFT JOIN table_droite o ON u.id = o.user_id;
 
-- Si sans_match = 0 → suspect. Est-ce logique dans ton contexte ?
left-join-silencieux-bug-analyses · 27

Étape 4 — Comparer les comptages

-- Avant toute correction en prod, mesure l'écart
SELECT 'version_actuelle' AS v, COUNT(DISTINCT u.id) AS n
FROM ... [ta requĂȘte actuelle]
UNION ALL
SELECT 'version_corrigee' AS v, COUNT(DISTINCT u.id) AS n
FROM ... [ta requĂȘte corrigĂ©e]
left-join-silencieux-bug-analyses · 28

Étape 5 — Valider l’intention

Pose-toi la question avant de “corriger” :

Question

Réponse attendue

Type de JOIN

Je veux TOUS les users, mĂȘme sans commande ?

Oui

LEFT JOIN + filtre dans ON

Je veux uniquement les users avec commande ?

Oui

INNER JOIN explicite

Je veux les users SANS commande uniquement ?

Oui

LEFT JOIN + WHERE IS NULL

💡 Un INNER JOIN explicite vaut mieux qu’un LEFT JOIN avec WHERE qui fait un INNER JOIN en secret. Le code doit dire ce qu’il fait.



đŸ§Ș Test final — Valide tes acquis en conditions rĂ©elles Tu as les patterns, les diagnostics, les cas rĂ©els, la checklist. Il ne reste plus qu’une chose : Ă©crire les requĂȘtes toi-mĂȘme sous contrainte de temps. C’est exactement ce que Modai Lab simule — les mĂȘmes conditions qu’en entretien data. 👉 Valider mes acquis sur modai-lab.com Mode entretien · Timer · Score et feedback dĂ©taillĂ© Ă  la fin


RĂ©capitulatif — Les 6 patterns

#

Pattern

Fréquence

Impact

1

WHERE sur colonne de la table de droite

⭐⭐⭐⭐⭐

Critique

2

Comparaison numérique qui élimine NULL

⭐⭐⭐⭐

Critique

3

BETWEEN sur la table de droite

⭐⭐⭐

ÉlevĂ©

4

NOT IN sur la table de droite

⭐⭐⭐

ÉlevĂ©

5

ChaĂźne de LEFT JOINs avec WHERE au milieu

⭐⭐⭐⭐

Critique

6

HAVING sur agrégat de la table de droite

⭐⭐⭐

ÉlevĂ©


✅ Checklist rapide (à imprimer)

  • ☐ Tous les filtres sur les tables de droite sont dans le ON, pas dans le WHERE

  • ☐ J’ai vĂ©rifiĂ© que le ratio de NULL aprĂšs JOIN est cohĂ©rent avec la rĂ©alitĂ©

  • ☐ J’ai comparĂ© COUNT avant et aprĂšs la correction

  • ☐ Les INNER JOINs implicites ont Ă©tĂ© remplacĂ©s par des INNER JOINs explicites

  • ☐ Les BETWEEN et NOT IN sur tables de droite sont dans le ON

  • ☐ Les chaĂźnes de LEFT JOINs n’ont aucun WHERE sur les tables intermĂ©diaires


🚀 S’entraüner sur Modai Lab

Ces patterns, tu ne les retiendras vraiment qu’en les pratiquant sur des cas rĂ©els.

modai-lab.com — Exercices SQL data rĂ©els · Feedback immĂ©diat · Parcours entretien

Modai Lab propose des exercices spĂ©cifiquement construits autour des piĂšges SQL comme celui-ci : des datasets rĂ©alistes, des requĂȘtes Ă  dĂ©boguer, et une correction expliquĂ©e pour chaque cas.


Guide offert suite au post LinkedIn · EntraĂźnez-vous sur modai-lab.com · Partagez librement ♻