Guide pratique · E-commerce · SaaS · Finance

Lecture : ~20 min

Teste ton niveau sur modai-lab.com/test-sql/


Pourquoi ce guide existe

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.

👉 Faire le test sur modai-lab.com/test-sql/ _Exercices interactifs

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
GROUP BY 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
GROUP BY user_id
),
campagne_par_user AS (
SELECT DISTINCT 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
GROUP BY 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
LEFT JOIN 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';
7-erreurs-sql-metriques · 04 · Cas concret SaaS — Taux d'activation faussé
-- ✅ Filtre déplacé dans le ON
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
LEFT JOIN 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.

👉 Tester mes réflexes sur modai-lab.com/test-sql/

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(DISTINCT CASE WHEN achat = true THEN user_id END) AS acheteurs,
COUNT(DISTINCT user_id) AS base,
ROUND(. * COUNT(DISTINCT CASE WHEN achat = true THEN user_id END)
/ COUNT(DISTINCT user_id), ) AS taux_conv
FROM sessions
WHERE email IS NOT NULL -- ← filtre involontaire sur la base
GROUP BY source;
7-erreurs-sql-metriques · 07 · Cas concret E-commerce — Taux de conversion faussé
-- ✅ Dénominateur = toutes les sessions, explicitement
SELECT
source,
COUNT(DISTINCT CASE WHEN achat = true THEN user_id END) AS acheteurs,
COUNT(DISTINCT user_id) AS toutes_sessions,
ROUND(. * COUNT(DISTINCT CASE WHEN achat = true THEN user_id END)
/ NULLIF(COUNT(DISTINCT user_id), ), ) AS taux_conv_reel
FROM sessions
GROUP BY 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(DISTINCT CASE WHEN condition THEN id END) AS numerateur,
ROUND(.
* COUNT(DISTINCT CASE WHEN condition THEN id END)
/ NULLIF(COUNT(DISTINCT id), ), ) AS taux_pct
FROM table
WHERE [filtre sur le dénominateur uniquement]
GROUP BY 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
LEFT JOIN orders o
ON c.user_id = o.user_id
AND o.created_at BETWEEN c.mois_cohorte + INTERVAL '30 days'
AND c.mois_cohorte + INTERVAL '60 days'
GROUP BY c.mois_cohorte;
7-erreurs-sql-metriques · 10 · Cas concret SaaS — Rétention cohorte faussée
-- ✅ 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
GROUP BY 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
LEFT JOIN 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'
GROUP BY c.mois_cohorte
ORDER BY 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
SELECT COUNT(*) 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
GROUP BY 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
UNION ALL
SELECT client_id, montant, transaction_id FROM transactions_sys2
) t
GROUP BY client_id;
-- Les transactions en double sont comptées deux fois
 
-- ✅ Dédupliquer par transaction_id avant d'agréger
SELECT client_id, SUM(montant) AS total
FROM (
SELECT DISTINCT ON (transaction_id) client_id, montant, transaction_id
FROM (
SELECT client_id, montant, transaction_id FROM transactions_sys1
UNION ALL
SELECT client_id, montant, transaction_id FROM transactions_sys2
) t
ORDER BY transaction_id
) dedup
GROUP BY 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 ORDER BY source) AS rn
FROM (
SELECT client_id, montant, transaction_id, 'sys1' AS source FROM transactions_sys1
UNION ALL
SELECT client_id, montant, transaction_id, 'sys2' AS source FROM transactions_sys2
) t
) dedup
WHERE rn =
GROUP BY 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
GROUP BY
ORDER BY ;
-- 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é
7-erreurs-sql-metriques · 14 · Cas concret SaaS — MRR historique surestimé
-- ✅ Inclure tous les abonnements actifs au moment de chaque facture
SELECT
DATE_TRUNC('month', i.invoice_date) AS mois,
SUM(i.montant) AS mrr
FROM invoices i
JOIN subscriptions s ON i.sub_id = s.id
WHERE s.start_date <= i.invoice_date -- abonnement existait à la date
AND (s.end_date IS NULL OR s.end_date >= i.invoice_date) -- pas encore churné
GROUP BY
ORDER BY ;
7-erreurs-sql-metriques · 15

Vérification — Mesurer l'impact du biais

WITH
avec_biais AS (
SELECT DATE_TRUNC('month', invoice_date) AS mois, SUM(montant) AS mrr
FROM invoices i JOIN subscriptions s ON i.sub_id = s.id
WHERE s.statut = 'actif'
GROUP BY
),
sans_biais AS (
SELECT DATE_TRUNC('month', i.invoice_date) AS mois, SUM(i.montant) AS mrr
FROM invoices i JOIN subscriptions s ON i.sub_id = s.id
WHERE s.start_date <= i.invoice_date
AND (s.end_date IS NULL OR s.end_date >= i.invoice_date)
GROUP BY
)
SELECT
a.mois,
a.mrr AS mrr_avec_biais,
b.mrr AS mrr_reel,
a.mrr - b.mrr AS surestimation,
ROUND(. * (a.mrr - b.mrr) / NULLIF(b.mrr, ), ) AS surestimation_pct
FROM avec_biais a
JOIN sans_biais b ON a.mois = b.mois
ORDER BY a.mois;
7-erreurs-sql-metriques · 16 · Vérification — Mesurer l'impact du biais

🧪 Test avancé
Biais de périmètre et survivant Le biais de survivant est l'erreur la plus difficile à détecter et la plus coûteuse en décision COMEX.

Sur Modai Lab, des exercices spécifiques t'entraînent à le repérer sur des données réelles.

👉 Reproduire ces cas sur modai-lab.com/test-sql/
Mode entretien · Timer · Score et feedback détaillé


PARTIE 4 — Le prompt Claude pour auditer tes requêtes

Copie ce prompt dans Claude avec ta requête pour obtenir un audit structuré en 2 minutes.


Prompt d'audit complet

Tu es un expert SQL data analytics. Audite la requête suivante et identifie
les erreurs silencieuses qui pourraient biaiser les métriques.
 
REQUÊTE À AUDITER :
[coller ta requête ici]
 
CONTEXTE MÉTIER :
- Objectif de la requête : [ex: calcul du taux de conversion hebdomadaire]
- Tables impliquées : [ex: users, orders, sessions]
- Granularité attendue : [ex: une ligne par semaine par source]
- KPI calculé : [ex: taux de conversion = acheteurs / visiteurs]
 
Analyse les points suivants dans l'ordre :
 
1. JOINTURES
- 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.

👉 Valider mes acquis sur modai-lab.com/test-sql/
Conditions d'entretien · Score détaillé · Feedback personnalisé


📋 Récapitulatif — Les 7 erreurs

#

Erreur

Symptôme métier

Secteur touché

1

Fan-out sur JOIN

Faux pics de CA, conversions multipliées

E-commerce, Marketing

2

LEFT JOIN → INNER JOIN silencieux

Taux d'activation surestimé

SaaS, Produit

3

Mauvaise granularité de date

Valorisations manquantes

Finance, Ops

4

Mauvais dénominateur de taux

Taux de conversion faussé

E-commerce, Growth

5

Mauvaise date d'origine cohorte

Rétention incohérente entre rapports

SaaS, Subscription

6

Double-comptage avant agrégation

CA gonflé, transactions en double

Finance, Comptabilité

7

Biais de survivant

MRR historique surestimé

SaaS, Finance


✅ Checklist avant de publier un rapport

  • 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.

👉 modai-lab.com/test-sql/ — Exercices SQL data réels · Feedback immédiat · Parcours entretien


Guide offert suite au post LinkedIn · Entraînez-vous sur modai-lab.com/test-sql/ · Partagez librement ♻️