Guide pratique · E-commerce · SaaS · Finance · Lecture : ~20 min Teste ton niveau sur modai-lab.com/test-sql/
Ce que les recruteurs testent vraiment
Pas la syntaxe. Tout le monde connaît la syntaxe.
Ils testent ce que tu fais quand les données sont sales, incomplètes, incohérentes. Ils testent si tu sais expliquer pourquoi COUNT(*) et COUNT(colonne) donnent des résultats différents. Pourquoi AVG() peut être faussée sans alerte. Pourquoi l'ordre des colonnes dans GROUP BY change tout.
Ce guide couvre :
Les pièges classiques de chaque fonction en conditions réelles
Les erreurs qui faussent silencieusement tes résultats
Des cas business concrets : e-commerce, SaaS, finance
Un prompt Claude pour auditer tes agrégations en 2 minutes
🧪 Calibre ton niveau avant de commencer
Sais-tu déjà expliquer la différence entre COUNT(*), COUNT(col) et COUNT(DISTINCT col) ? Fais le test en 5 minutes et reçois ton score instantanément.
5-fonctions-aggregations-sql-indispensables · 08 · Piège 2 — SUM() conditionnel avec CASE WHEN
Piège 3 — SUM() cumulatif avec window function
-- Cumul des ventes depuis le début de l'année
SELECT
DATE_TRUNC('day', created_at) AS jour,
SUM(montant) AS ca_jour,
SUM(SUM(montant)) OVER (
ORDERBY DATE_TRUNC('day', created_at)
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS ca_cumulatif
FROM orders
WHERE created_at >= DATE_TRUNC('year', NOW())
GROUPBY
ORDERBY ;
-- SUM(SUM(...)) : le SUM externe est la window function,
-- le SUM interne est l'agrégation du GROUP BY
5-fonctions-aggregations-sql-indispensables · 09 · Piège 3 — SUM() cumulatif avec window function
Cas business — Finance : P&L mensuel
-- Construire un compte de résultat simplifié
WITH transactions_classees AS (
SELECT
DATE_TRUNC('month', date_transaction) AS mois,
type_compte,
SUM(CASEWHEN sens = 'crédit'THEN montant ELSEEND) AS credits,
SUM(CASEWHEN sens = 'débit'THEN montant ELSEEND) AS debits,
SUM(CASEWHEN sens = 'crédit'THEN montant ELSE -montant END) AS solde
FROM transactions
WHERE annee_fiscale =
GROUPBY ,
)
SELECT
mois,
SUM(CASEWHEN type_compte = 'revenus'THEN solde ELSEEND) AS revenus,
SUM(CASEWHEN type_compte = 'charges'THEN -solde ELSEEND) AS charges,
SUM(CASEWHEN type_compte = 'revenus'THEN solde ELSE -solde END) AS resultat_net
FROM transactions_classees
GROUPBY mois
ORDERBY mois;
5-fonctions-aggregations-sql-indispensables · 10
🧪 Teste COUNT et SUM sur des données réelles
Ces deux fonctions sont au cœur de tous les rapports analytiques. Modai Lab te soumet des datasets avec des NULL, des doublons et des cas limites — score immédiat.
-- ✅ Toujours comparer AVG et médiane sur des distributions asymétriques
SELECT
AVG(salaire) AS salaire_moyen,
PERCENTILE_CONT(.) WITHIN GROUP (ORDERBY salaire) AS salaire_median,
PERCENTILE_CONT(.) WITHIN GROUP (ORDERBY salaire) AS q1,
PERCENTILE_CONT(.) WITHIN GROUP (ORDERBY salaire) AS q3,
MAX(salaire) ASmax,
MIN(salaire) ASmin
FROM employes;
-- Si moyen >> médiane : distribution asymétrique, outliers présents
5-fonctions-aggregations-sql-indispensables · 13 · Piège 2 — AVG() vs médiane
Piège 3 — AVG() mobile pour lisser les tendances
-- Moyenne mobile sur 7 jours pour lisser les fluctuations journalières
SELECT
DATE_TRUNC('day', created_at) AS jour,
AVG(montant) AS panier_moyen_jour,
AVG(AVG(montant)) OVER (
ORDERBY DATE_TRUNC('day', created_at)
ROWS BETWEEN PRECEDING AND CURRENT ROW
) AS panier_moyen_7j
FROM orders
GROUPBY
ORDERBY ;
5-fonctions-aggregations-sql-indispensables · 14 · Piège 3 — AVG() mobile pour lisser les tendances
Cas business — E-commerce : analyse du cycle de vie client
-- AVG() sur des durées pour comprendre les comportements
WITH cycle_client AS (
SELECT
user_id,
MIN(created_at) AS premier_achat,
MAX(created_at) AS dernier_achat,
COUNT(*) AS nb_commandes,
SUM(montant) AS ca_total,
AVG(montant) AS panier_moyen
FROM orders
WHERE statut = 'livré'
GROUPBY user_id
)
SELECT
nb_commandes AS frequence_achat,
COUNT(*) AS nb_clients,
ROUND(AVG(ca_total), ) AS ca_moyen_par_client,
ROUND(AVG(panier_moyen), ) AS panier_moyen,
ROUND(AVG(
EXTRACT(EPOCH FROM (dernier_achat - premier_achat)) /
), ) AS duree_moy_relation_jours
FROM cycle_client
GROUPBY
ORDERBY ;
5-fonctions-aggregations-sql-indispensables · 15
FONCTION 4 — MAX() / MIN()
La question de l'entretien
"Comment utilises-tu MAX() et MIN() sur des dates ? Et sur des chaînes de caractères ?"
MAX() / MIN() sur différents types de données
-- Sur des nombres (comportement attendu)
SELECTMAX(montant) AS plus_grosse_commande, MIN(montant) AS plus_petite_commande
FROM orders;
-- Sur des dates (très utile en pratique)
SELECT
MIN(created_at) AS premiere_commande,
MAX(created_at) AS derniere_commande,
MAX(created_at) - MIN(created_at) AS duree_activite
FROM orders
WHERE user_id = ;
-- Sur des chaînes de caractères (ordre alphabétique)
SELECTMAX(nom) AS dernier_alpha, MIN(nom) AS premier_alpha
FROM users;
-- MAX('Z...') > MIN('A...') — ordre lexicographique, pas logique métier
-- Attention : la casse peut changer le résultat (majuscules < minuscules en ASCII)
5-fonctions-aggregations-sql-indispensables · 16
Les pièges en conditions réelles
Piège 1 — MAX() pour "la dernière valeur" sans ORDER BY
-- ❌ MAX(created_at) donne la date la plus récente, mais pas la ligne associée
SELECT user_id, MAX(created_at) AS derniere_commande, montant
FROM orders
GROUPBY user_id, montant; -- ← faux : montant n'est pas agrégé correctement
-- ✅ Pour récupérer la ligne la plus récente : ROW_NUMBER ou DISTINCT ON
SELECTDISTINCTON (user_id)
user_id, created_at, montant, statut
FROM orders
ORDERBY user_id, created_at DESC;
-- Ou avec ROW_NUMBER (portable)
SELECT user_id, created_at, montant, statut
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDERBY created_at DESC) AS rn
FROM orders
) t
WHERE rn = ;
5-fonctions-aggregations-sql-indispensables · 17 · Piège 1 — MAX() pour "la dernière valeur" sans ORDER BY
Piège 2 — MIN(date) pour la date d'acquisition
-- Pattern classique d'analyse de cohorte
WITH premiere_commande AS (
SELECT
user_id,
MIN(created_at) AS date_premier_achat,
DATE_TRUNC('month', MIN(created_at)) AS cohorte
FROM orders
GROUPBY user_id
)
SELECT
cohorte,
COUNT(*) AS taille_cohorte,
ROUND(AVG(
EXTRACT(EPOCH FROM (MIN(o.created_at) - pc.date_premier_achat)) /
), ) AS delai_moyen_premier_achat_jours
FROM premiere_commande pc
JOIN orders o ON pc.user_id = o.user_id
AND o.created_at > pc.date_premier_achat -- commandes APRÈS la première
GROUPBY
ORDERBY ;
5-fonctions-aggregations-sql-indispensables · 18 · Piège 2 — MIN(date) pour la date d'acquisition
Piège 3 — MAX() / MIN() ignorent les NULL comme SUM()
-- Si toutes les valeurs d'un groupe sont NULL, MAX() et MIN() retournent NULL
SELECT categorie, MAX(prix_promo) AS promo_max
FROM produits
GROUPBY categorie;
-- Les catégories sans promotion ont MAX(prix_promo) = NULL, pas 0
-- ✅ Gérer explicitement avec COALESCE si nécessaire
SELECT
categorie,
MAX(prix_promo) AS promo_max,
COALESCE(MAX(prix_promo), ) AS promo_max_ou_zero,
CASEWHENMAX(prix_promo) ISNULLTHEN'Pas de promo'ELSE'Promo active'ENDAS statut_promo
FROM produits
GROUPBY categorie;
5-fonctions-aggregations-sql-indispensables · 19 · Piège 3 — MAX() / MIN() ignorent les NULL comme SUM()
Cas business — SaaS : identifier les pics d'utilisation
-- MAX() pour détecter les pics et les anomalies
WITH activite_horaire AS (
SELECT
DATE_TRUNC('hour', created_at) AS heure,
COUNT(*) AS nb_events,
MAX(duree_session) AS session_max,
MIN(duree_session) AS session_min,
AVG(duree_session) AS session_moyenne
FROM user_sessions
WHERE created_at >= NOW() - INTERVAL '7 days'
GROUPBY
)
SELECT
heure,
nb_events,
session_max,
session_min,
ROUND(session_moyenne, ) AS session_moyenne,
CASE
WHEN nb_events > AVG(nb_events) OVER () * THEN'⚠️ Pic anormal'
WHEN session_max > THEN'⚠️ Session longue'
ELSE'✅ Normal'
ENDAS alerte
FROM activite_horaire
ORDERBY heure;
5-fonctions-aggregations-sql-indispensables · 20
🧪 Teste AVG, MAX et MIN sur des cas limites
Les recruteurs adorent poser des questions sur le comportement de ces fonctions avec des données réelles. Modai Lab simule exactement ces scénarios — feedback ligne par ligne.
COALESCE(categorie, 'Toutes catégories') AS categorie,
COALESCE(pays, 'Tous pays') AS pays,
COALESCE(CAST(trimestre ASVARCHAR), 'Total') AS trimestre,
SUM(ca) AS ca_total,
SUM(nb_commandes) AS nb_commandes,
SUM(nb_clients) AS nb_clients,
ROUND(SUM(ca) / NULLIF(SUM(nb_commandes), ), ) AS panier_moyen
FROM ventes
GROUPBY ROLLUP(categorie, pays, trimestre)
ORDERBY categorie, pays, trimestre;
5-fonctions-aggregations-sql-indispensables · 27
🧪 Teste GROUP BY avancé en conditions réelles
ROLLUP, CUBE, GROUP BY sur des expressions, gestion des NULL dans les groupes : Modai Lab propose ces exercices sur de vraies données avec feedback immédiat.
_Scénarios data réels · Feedback senior · Progression mesurée_
PARTIE 6 — Prompt Claude pour auditer tes agrégations
Prompt d'audit complet
Tu es un expert SQL data analytics. Audite les agrégations de la requête suivante.
REQUÊTE À AUDITER :
[coller ta requête]
CONTEXTE :
- Objectif : [décrire ce que la requête doit calculer]
- Volume de données : [ordre de grandeur]
- Présence de NULL connue : [oui / non / inconnu]
Analyse ces points dans l'ordre :
1. COUNT()
- Y a-t-il une confusion entre COUNT(*), COUNT(col) et COUNT(DISTINCT col) ?
- Un COUNT(DISTINCT) masque-t-il un fan-out de JOIN ?
- Le dénominateur utilisé pour les taux est-il correct ?
2. SUM() et AVG()
- Les NULL sont-ils gérés intentionnellement (ignorés ou remplacés par 0) ?
- Y a-t-il un fan-out de JOIN qui multiplie les valeurs sommées ?
- AVG() est-il calculé sur le bon périmètre ? (pas de filtre qui biaise le dénominateur)
3. MAX() / MIN()
- MAX() est-il utilisé pour récupérer une ligne entière ? (→ préférer ROW_NUMBER)
- Les NULL dans MAX/MIN sont-ils gérés ?
4. GROUP BY
- Toutes les colonnes non agrégées sont-elles dans le GROUP BY ?
- Y a-t-il des colonnes avec NULL qui forment un groupe inattendu ?
- L'ordre des colonnes dans GROUP BY est-il cohérent avec l'intention ?
Format : ❌ pour chaque problème, ✅ pour les points corrects. Requête corrigée si nécessaire.
5-fonctions-aggregations-sql-indispensables · 28
Prompt rapide
Audite les agrégations COUNT, SUM, AVG, MAX, MIN et GROUP BY de cette requête SQL.
Identifie les erreurs silencieuses liées aux NULL, aux fan-outs et aux dénominateurs incorrects.
Propose la version corrigée avec commentaires.
[coller ta requête]
5-fonctions-aggregations-sql-indispensables · 29
Récapitulatif — Les 5 fonctions et leurs pièges
Fonction
Piège principal
Ce qui le révèle
La correction
COUNT(*)
Compte les NULL
Résultat > attendu
COUNT(col) ou COUNT(DISTINCT col)
COUNT(col)
Ignore les NULL
Résultat < attendu
Diagnostic NULL avant d'interpréter
SUM()
Ignore les NULL, multipliable par fan-out
CA sous-estimé ou gonflé
COALESCE, agréger avant JOIN
AVG()
Dénominateur réduit silencieusement, sensible aux outliers
Moyenne faussée
Afficher taux de réponse, comparer à la médiane
MAX/MIN()
Ne retourne pas la ligne, ignore les NULL
Donnée incomplète
ROW_NUMBER, COALESCE
GROUP BY
NULL forme un groupe, colonnes manquantes
Erreur SQL ou groupe fantôme
COALESCE, vérifier le SELECT
✅ Checklist entretien — Les 5 fonctions
☐ Je sais expliquer la différence entre COUNT(*), COUNT(col) et COUNT(DISTINCT col)
☐ Je sais pourquoi SUM() peut être multiplié après un JOIN
☐ Je sais pourquoi AVG() peut être faussé sans erreur
☐ Je sais utiliser MAX(date) vs ROW_NUMBER pour récupérer la dernière ligne
☐ Je sais que GROUP BY crée un groupe pour les NULL
☐ Je sais utiliser ROLLUP et GROUPING SETS pour les rapports multi-dimensionnels
☐ Je peux construire un taux avec le bon dénominateur et NULLIF pour éviter la division par zéro
🚀 Pratiquer sur Modai Lab
Les 5 fonctions de ce guide sont testées dans 90% des entretiens data. La différence entre un candidat qui "connaît la syntaxe" et un candidat qui "sait les utiliser" se voit en 30 secondes sur des données réelles.
_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.