Les data analysts qui gagnent le mieux ne savent pas juste "écrire du SQL". Ils savent éviter les erreurs qui faussent silencieusement chaque rapport.
Après analyse de centaines de requêtes en production, 7 patterns reviennent presque partout. Ils provoquent de faux pics de conversion, des cohortes incohérentes, des KPIs qui mentent en COMEX.
Une mauvaise jointure = une décision métier faussée.Une bonne requête = une décision à 6 chiffres.
Ce guide couvre :
Les 7 erreurs qui biaisent tes métriques sans alerte
Les requêtes corrigées, prêtes à copier-coller
Des cas concrets : e-commerce, SaaS, finance
Un prompt Claude pour auditer tes propres requêtes en 2 minutes
🧪 Calibre ton niveau avant de commencer
Ces 7 erreurs, en reconnais-tu déjà certaines dans tes requêtes ?
Fais le test en direct sur Modai Lab et reçois ton score instantanément.
Résultat en direct · Parcours personnalisé selon ton niveau_
PARTIE 1 — Erreurs de jointure et de granularité
Les erreurs les plus silencieuses. Aucune alerte. Des chiffres qui ont l'air vrais.
❌ Erreur 1 — La multiplication de lignes par JOIN (Fan-out)
Le problème
C'est l'erreur numéro 1 des faux pics de conversion. Quand tu joins une table A à une table B où plusieurs lignes de B correspondent à une ligne de A, chaque ligne de A est dupliquée autant de fois.
Cas concret e-commerce — Faux CA doublé
-- Contexte : un rapport CA par campagne marketing
-- Problème : un utilisateur peut avoir plusieurs lignes dans user_events
-- ❌ Requête en production
SELECT
c.nom_campagne,
SUM(o.montant) AS ca_total
FROM campaigns c
JOIN user_events ue ON c.id = ue.campaign_id -- plusieurs events par user
JOIN orders o ON ue.user_id = o.user_id -- plusieurs orders par user
GROUPBY c.nom_campagne;
-- Résultat : CA multiplié par le nombre d'événements par user
7-erreurs-sql-metriques · 01 · Cas concret e-commerce — Faux CA doublé
-- ✅ Requête corrigée : agréger avant de joindre
WITH ca_par_user AS (
SELECT user_id, SUM(montant) AS ca
FROM orders
GROUPBY user_id
),
campagne_par_user AS (
SELECTDISTINCT user_id, campaign_id
FROM user_events
)
SELECT
c.nom_campagne,
SUM(cpu.ca) AS ca_total
FROM campaigns c
JOIN campagne_par_user ue ON c.id = ue.campaign_id
JOIN ca_par_user cpu ON ue.user_id = cpu.user_id
GROUPBY c.nom_campagne;
7-erreurs-sql-metriques · 02
Diagnostic — Détecter une multiplication
-- Si ce ratio > 1 : tu as un fan-out
SELECT
COUNT(*) AS lignes_apres_join,
COUNT(DISTINCT o.id) AS orders_distincts,
ROUND(COUNT(*) * . / NULLIF(COUNT(DISTINCT o.id), ), ) AS ratio_multiplication
FROM orders o
JOIN user_events ue ON o.user_id = ue.user_id;
-- ratio > 1 = chaque order est compté plusieurs fois
7-erreurs-sql-metriques · 03 · Diagnostic — Détecter une multiplication
💡 Règle : avant tout SUM() sur une requête avec plusieurs JOINs, vérifier que le ratio COUNT(*) / COUNT(DISTINCT id) vaut 1.
❌ Erreur 2 — Le LEFT JOIN transformé en INNER JOIN (filtre WHERE)
Le problème
Tu fais un LEFT JOIN pour garder tous tes utilisateurs. Tu ajoutes un WHERE sur la table de droite. SQL élimine silencieusement tous les NULL. Tu te retrouves avec un INNER JOIN déguisé.
Cas concret SaaS — Taux d'activation faussé
-- Contexte : mesurer le taux d'activation (% users ayant fait une action clé)
-- Faux résultat : 78% d'activation
-- ❌ 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
FROM users u
LEFTJOIN actions a ON u.id = a.user_id
WHERE a.type = 'activation'-- ← transforme le LEFT JOIN en INNER JOIN
AND a.created_at < u.created_at + INTERVAL '7 days';
ROUND(. * COUNT(DISTINCT a.user_id) / COUNT(DISTINCT u.id), ) AS taux
FROM users u
LEFTJOIN actions a
ON u.id = a.user_id
AND a.type = 'activation'
AND a.created_at < u.created_at + INTERVAL '7 days';
-- Résultat réel : 34% — les 44 points d'écart venaient des users inactifs exclus
7-erreurs-sql-metriques · 05
❌ Erreur 3 — La mauvaise granularité de la date de référence
Le problème
Tu jointures sur une date, mais la granularité ne correspond pas. Résultat : des lignes manquantes ou des doublons selon l'heure exacte de l'événement.
Cas concret Finance — Valorisation portefeuille
-- Contexte : valoriser chaque position avec le prix de clôture du jour
-- Problème : les prix sont à 17h30, les positions peuvent être en intraday
-- ❌ Jointure exacte sur timestamp → beaucoup de lignes non jointes
SELECT p.ticker, p.quantite, pr.prix_cloture
FROM positions p
JOIN prix_historique pr
ON p.ticker = pr.ticker
AND p.date_position = pr.date_prix; -- timestamp ≠ date de clôture
-- ✅ Tronquer à la journée pour la jointure
SELECT p.ticker, p.quantite, pr.prix_cloture,
p.quantite * pr.prix_cloture AS valorisation
FROM positions p
JOIN prix_historique pr
ON p.ticker = pr.ticker
AND DATE_TRUNC('day', p.date_position) = pr.date_prix;
7-erreurs-sql-metriques · 06 · Cas concret Finance — Valorisation portefeuille
🧪 Teste tes réflexes — Jointures et granularité Fan-out, LEFT JOIN silencieux, granularité date : ces 3 patterns sont testés dans presque tous les entretiens data.
Modai Lab te met en situation avec un dataset réel — score immédiat.
Score immédiat · Explication de chaque erreur · Niveau calibré automatiquement_
PARTIE 2 — Erreurs de calcul et d'agrégation
Ces erreurs ne plantent pas ta requête. Elles te donnent un résultat plausible — mais faux.
❌ Erreur 4 — Le taux calculé sur un mauvais dénominateur
Le problème
Le taux de conversion, le taux de churn, le taux de rétention — tous ces indicateurs dépendent d'un dénominateur correct. Un filtre mal placé ou un NULL non géré change silencieusement la base de calcul.
Cas concret E-commerce — Taux de conversion faussé
-- Contexte : taux de conversion par source d'acquisition
-- Définition correcte : sessions → achat
-- ❌ Version fausse : dénominateur = sessions avec email capturé seulement
SELECT
source,
COUNT(DISTINCTCASEWHEN achat = trueTHEN user_id END) AS acheteurs,
/ NULLIF(COUNT(DISTINCT user_id), ), ) AS taux_conv_reel
FROM sessions
GROUPBY source;
7-erreurs-sql-metriques · 08
Template universel pour les taux
-- Structure à toujours utiliser pour un taux
SELECT
dimension,
COUNT(DISTINCT id) AS denominateur,
COUNT(DISTINCTCASEWHEN condition THEN id END) AS numerateur,
ROUND(.
* COUNT(DISTINCTCASEWHEN condition THEN id END)
/ NULLIF(COUNT(DISTINCT id), ), ) AS taux_pct
FROMtable
WHERE [filtre sur le dénominateur uniquement]
GROUPBY dimension;
7-erreurs-sql-metriques · 09 · Template universel pour les taux
💡 Toujours se poser la question : mon WHERE filtre-t-il le numérateur, le dénominateur, ou les deux ? Documenter l'intention dans un commentaire.
❌ Erreur 5 — L'analyse de cohorte avec une mauvaise date d'origine
Le problème
Une cohorte est définie par la première occurrence d'un événement. Si tu prends la mauvaise date (date de création du compte vs date du premier achat), tes cohortes sont fausses — et les taux de rétention avec elles.
Cas concret SaaS — Rétention cohorte faussée
-- ❌ Mauvaise date d'origine : date de création du compte
-- Problème : un user peut s'inscrire et ne pas utiliser le produit pendant 2 mois
WITH cohortes AS (
SELECT id AS user_id,
DATE_TRUNC('month', created_at) AS mois_cohorte -- ← date d'inscription
FROM users
)
SELECT
c.mois_cohorte,
COUNT(DISTINCT c.user_id) AS taille_cohorte,
COUNT(DISTINCT o.user_id) AS retenus_m1
FROM cohortes c
LEFTJOIN orders o
ON c.user_id = o.user_id
AND o.created_at BETWEEN c.mois_cohorte + INTERVAL '30 days'
-- ✅ Date d'origine = premier achat (comportement réel)
WITH cohortes AS (
SELECT
user_id,
DATE_TRUNC('month', MIN(created_at)) AS mois_cohorte -- ← premier achat
FROM orders
GROUPBY user_id
)
SELECT
c.mois_cohorte,
COUNT(DISTINCT c.user_id) AS taille_cohorte,
COUNT(DISTINCT o.user_id) AS retenus_m1,
ROUND(. * COUNT(DISTINCT o.user_id)
/ NULLIF(COUNT(DISTINCT c.user_id), ), ) AS retention_m1_pct
FROM cohortes c
LEFTJOIN orders o
ON c.user_id = o.user_id
AND o.created_at >= c.mois_cohorte + INTERVAL '30 days'
AND o.created_at < c.mois_cohorte + INTERVAL '60 days'
GROUPBY c.mois_cohorte
ORDERBY c.mois_cohorte;
7-erreurs-sql-metriques · 11
Vérification : détecter les cohortes incohérentes
-- Si ce nombre est > 0 : ta date d'origine est mauvaise
SELECTCOUNT(*) AS cohortes_incoherentes
FROM (
SELECT user_id, MIN(order_date) AS premier_achat, MIN(signup_date) AS inscription
FROM (
SELECT u.id AS user_id, u.created_at AS signup_date, o.created_at AS order_date
FROM users u JOIN orders o ON u.id = o.user_id
) t
GROUPBY user_id
) t
WHERE premier_achat < inscription; -- achat avant inscription = donnée corrompue
7-erreurs-sql-metriques · 12 · Vérification : détecter les cohortes incohérentes
❌ Erreur 6 — La déduplication oubliée avant agrégation
Le problème
Tu comptes des utilisateurs distincts, mais en joignant plusieurs tables, un même utilisateur apparaît plusieurs fois. COUNT(DISTINCT user_id) ne suffit pas si la même combinaison (user, event) peut être dupliquée dans la source.
Cas concret Finance — Double-comptage des transactions
-- Contexte : rapport de transactions par client (données issues de 2 systèmes)
-- Problème : certaines transactions existent dans les deux systèmes
-- ❌ Double-comptage
SELECT client_id, SUM(montant) AS total
FROM (
SELECT client_id, montant, transaction_id FROM transactions_sys1
UNIONALL
SELECT client_id, montant, transaction_id FROM transactions_sys2
) t
GROUPBY client_id;
-- Les transactions en double sont comptées deux fois
-- ✅ Dédupliquer par transaction_id avant d'agréger
SELECT client_id, montant, transaction_id FROM transactions_sys1
UNIONALL
SELECT client_id, montant, transaction_id FROM transactions_sys2
) t
ORDERBY transaction_id
) dedup
GROUPBY client_id;
-- Ou avec ROW_NUMBER pour plus de contrôle
SELECT client_id, SUM(montant) AS total
FROM (
SELECT client_id, montant, transaction_id,
ROW_NUMBER() OVER (PARTITION BY transaction_id ORDERBY source) AS rn
FROM (
SELECT client_id, montant, transaction_id, 'sys1'AS source FROM transactions_sys1
UNIONALL
SELECT client_id, montant, transaction_id, 'sys2'AS source FROM transactions_sys2
) t
) dedup
WHERE rn =
GROUPBY client_id;
7-erreurs-sql-metriques · 13 · Cas concret Finance — Double-comptage des transactions
🧪 Test intermédiaire
Calculs et agrégations Taux sur mauvais dénominateur, cohortes mal définies, double-comptage : 3 patterns qui faussent les COMEX.
Modai Lab simule exactement ces cas avec des datasets réalistes. 👉 Pratiquer sur modai-lab.com/test-sql/ Datasets réalistes · Feedback ligne par ligne · Progression mesurée
PARTIE 3 — Erreurs de périmètre et de contexte
Ces erreurs viennent d'une mauvaise compréhension du périmètre de données — pas de la syntaxe.
❌ Erreur 7 — Le biais de survivant dans les analyses temporelles
Le problème
Tu analyses les utilisateurs "actifs aujourd'hui" sur une période passée. Mais tu oublies d'inclure ceux qui ont churné entre-temps. Tes métriques historiques ne reflètent que les survivants — les meilleurs clients — pas la réalité de la période.
Cas concret SaaS — MRR historique surestimé
-- Contexte : MRR mensuel des 12 derniers mois
-- Problème : seuls les clients encore actifs aujourd'hui sont dans la table
-- ❌ Biais de survivant
SELECT
DATE_TRUNC('month', invoice_date) AS mois,
SUM(montant) AS mrr
FROM invoices
JOIN subscriptions s ON invoices.sub_id = s.id
WHERE s.statut = 'actif'-- ← inclut uniquement les abonnements encore actifs
GROUPBY
ORDERBY ;
-- MRR des mois passés = MRR des clients qui ont survécu jusqu'aujourd'hui
-- Les churns de l'année sont invisibles → MRR historique surestimé
- Y a-t-il un risque de fan-out (multiplication de lignes) ?
- Les LEFT JOINs sont-ils transformés en INNER JOIN par un WHERE ?
- Les conditions de jointure correspondent-elles à la bonne granularité ?
2. AGRÉGATIONS
- Le dénominateur du taux est-il correctement défini ?
- Y a-t-il des NULL ignorés silencieusement dans SUM() ou AVG() ?
- Y a-t-il un risque de double-comptage ?
3. PÉRIMÈTRE
- La date de référence est-elle correcte pour le calcul de cohorte ?
- Y a-t-il un biais de survivant (filtre sur statut actuel appliqué à l'historique) ?
- Le filtre WHERE exclut-il involontairement des données du périmètre ?
4. RÉSULTAT ATTENDU
- Donne un exemple de résultat incorrect que cette requête pourrait produire
- Propose la requête corrigée avec commentaires
Format de réponse : une section par point, avec ❌ pour les problèmes trouvés
et ✅ pour les points corrects. Si tout est correct, indique-le explicitement.
7-erreurs-sql-metriques · 17
Prompt rapide (version 30 secondes)
Audite cette requête SQL pour identifier les erreurs silencieuses :
fan-out sur JOIN, LEFT JOIN converti en INNER JOIN, mauvais dénominateur
de taux, NULL ignorés, double-comptage, biais de survivant.
[coller ta requête]
Contexte : [objectif en une phrase]
Réponds avec les problèmes trouvés et la requête corrigée.
7-erreurs-sql-metriques · 18
Comment interpréter la réponse de Claude
Quand Claude identifie un problème, demande-lui toujours :
1. "Montre-moi un exemple de données où cette erreur donnerait un résultat faux"
2. "Quel serait l'écart entre la version fausse et la version correcte sur ces données ?"
3. "Comment vérifier en production si ce problème existe dans mes données réelles ?"
7-erreurs-sql-metriques · 19
🧪 Test final
Valide tes acquis sur les 7 erreurs Tu as les patterns, les requêtes corrigées, le prompt d'audit. Il reste une étape : les reconnaître sous contrainte de temps, sur des données inconnues. C'est exactement ce que Modai Lab simule.
☐ Fan-out vérifié : COUNT(*) / COUNT(DISTINCT id) = 1 après chaque JOIN
☐ LEFT JOINs propres : tous les filtres sur tables de droite sont dans le ON
☐ Granularité de date : les jointures sur date tronquent au même niveau
☐ Dénominateur documenté : le commentaire précise ce qu'il inclut/exclut
☐ Date d'origine cohorte : basée sur le premier événement, pas la création de compte
☐ Déduplication : DISTINCT ON ou ROW_NUMBER appliqué avant SUM()
☐ Périmètre temporel : les filtres sur statut actuel n'écrasent pas l'historique
☐ NULL contrôlés : COALESCE ou IS NULL explicite sur toutes les colonnes nullable critiques
🚀 S'entraîner sur Modai Lab
Lire ce guide, c'est comprendre les patterns. Les éviter sous pression d'un entretien ou d'un rapport urgent, c'est une autre compétence — qui s'entraîne.
Guide offert suite au post LinkedIn · Entraînez-vous sur modai-lab.com/test-sql/ · Partagez librement ♻️
Modai Lab
La théorie c’est bien. La pratique sur des cas concrets, c’est mieux.
Exerce-toi sur les cas SQL qu’on te posera en entretien, avec un coach IA qui review chaque requête comme un senior. Essai gratuit, sans carte bancaire.