Guide pratique · E-commerce · SaaS · Finance ·

Lecture : ~15 min

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


Pourquoi ce guide existe

La majorité des analysts utilisent WHERE et HAVING au hasard. Leurs résultats sont faux — sans message d'erreur, sans alerte.

La règle est simple :

  • Tu filtres sur une colonne bruteWHERE

  • Tu filtres sur un résultat agrégé (SUM, COUNT, AVG...) → HAVING

Mais la comprendre vraiment, c'est savoir pourquoi — et reconnaître les 4 cas où ça fausse silencieusement tes chiffres.

Ce guide couvre :

  • La différence exacte avec des exemples concrets

  • Les 4 erreurs classiques WHERE/HAVING en production

  • Des cas business réels : e-commerce, SaaS, finance

  • Un prompt Claude pour détecter les filtres mal placés


🧪 Calibre ton niveau avant de commencer

Sais-tu déjà expliquer pourquoi WHERE COUNT(*) > 10 génère une erreur ? Fais le test en 5 minutes et reçois ton score instantanément.

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

_Résultat en direct · Niveau calibré · Parcours personnalisé_


PARTIE 1 — Le mécanisme exact


L'ordre d'exécution SQL

C'est la clé de tout. SQL n'exécute pas les clauses dans l'ordre où tu les écris.

Ordre d'écriture : SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
Ordre d'exécution : FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
where-vs-having-erreur-agregations · 01

Ce que ça signifie concrètement :

  • WHERE est évalué avant GROUP BY → il voit les lignes brutes, pas les agrégats

  • HAVING est évalué après GROUP BY → il voit les groupes et leurs agrégats

  • SELECT est évalué en dernier → les alias du SELECT ne sont pas disponibles dans WHERE ni HAVING (sauf dans certains SGBD)


La démonstration sur un exemple simple

-- Données : commandes par client
-- user_id | montant | statut
-- 1 | 100 | livré
-- 1 | 50 | annulé
-- 2 | 200 | livré
-- 2 | 80 | livré
-- 3 | 30 | livré
 
-- Question 1 : "Clients dont le total des commandes livrées > 150€"
-- ✅ Correct : WHERE filtre les lignes, HAVING filtre le total
SELECT user_id, SUM(montant) AS total
FROM orders
WHERE statut = 'livré' -- filtre AVANT agrégation : exclut les annulées
GROUP BY user_id
HAVING SUM(montant) > ; -- filtre APRÈS agrégation : garde les gros clients
 
-- Résultat : user_id 2 → total 280€ ✅
 
-- Question 2 : "Clients dont au moins une commande dépasse 100€"
-- ✅ Correct : WHERE sur la colonne brute
SELECT DISTINCT user_id
FROM orders
WHERE montant > ;
 
-- Question 3 : "Clients avec plus de 2 commandes au total"
-- ✅ Correct : HAVING sur l'agrégat COUNT
SELECT user_id, COUNT(*) AS nb_commandes
FROM orders
GROUP BY user_id
HAVING COUNT(*) > ;
where-vs-having-erreur-agregations · 02

La règle des 30 secondes

Avant d'écrire un filtre, pose-toi une question :

Est-ce que cette valeur existe dans les données BRUTES ?
→ Oui (un statut, un pays, un montant de ligne) → WHERE
 
Est-ce que cette valeur résulte d'un CALCUL sur plusieurs lignes ?
→ Oui (un total, une moyenne, un comptage) → HAVING
where-vs-having-erreur-agregations · 03

PARTIE 2 — Les 4 erreurs classiques en production


❌ Erreur 1 — WHERE sur une fonction d'agrégation

C'est l'erreur la plus fréquente. Elle génère une erreur SQL explicite — mais beaucoup de débutants ne comprennent pas pourquoi.

-- ❌ Erreur : WHERE ne peut pas voir le résultat de COUNT()
SELECT user_id, COUNT(*) AS nb_commandes
FROM orders
WHERE COUNT(*) > -- erreur : agrégat dans WHERE
GROUP BY user_id;
-- SQL Error: aggregate functions are not allowed in WHERE
 
-- ✅ Correction : HAVING pour les filtres sur agrégats
SELECT user_id, COUNT(*) AS nb_commandes
FROM orders
GROUP BY user_id
HAVING COUNT(*) > ;
where-vs-having-erreur-agregations · 04

Cas concret SaaS — Identifier les utilisateurs très actifs

-- ❌ Erreur de débutant
SELECT user_id, COUNT(*) AS nb_events
FROM events
WHERE COUNT(*) > -- erreur SQL
GROUP BY user_id;
 
-- ✅ Version correcte
SELECT user_id, COUNT(*) AS nb_events
FROM events
GROUP BY user_id
HAVING COUNT(*) >
ORDER BY nb_events DESC;
where-vs-having-erreur-agregations · 05 · Cas concret SaaS — Identifier les utilisateurs très actifs

❌ Erreur 2 — WHERE qui filtre les lignes au mauvais moment

Ici, la requête ne génère pas d'erreur. Elle retourne un résultat — mais faux.

-- Contexte e-commerce : CA mensuel des commandes livrées
-- Données :
-- 2024-01 : 5 commandes livrées (total 800€), 2 annulées (total 150€)
-- 2024-02 : 4 commandes livrées (total 600€), 3 annulées (total 200€)
 
-- ❌ Résultat faux : pas d'erreur, mais mauvais chiffres
SELECT
DATE_TRUNC('month', created_at) AS mois,
SUM(montant) AS ca
FROM orders
GROUP BY
HAVING statut = 'livré'; -- ❌ HAVING sur une colonne non agrégée
-- Comportement indéfini selon le SGBD : erreur ou résultat aléatoire
 
-- ❌ Autre variante fausse : WHERE après GROUP BY (impossible syntaxiquement)
-- mais la vraie erreur est de confondre les niveaux
 
-- ✅ Version correcte : WHERE pour filtrer les lignes avant agrégation
SELECT
DATE_TRUNC('month', created_at) AS mois,
SUM(montant) AS ca
FROM orders
WHERE statut = 'livré' -- filtre les lignes annulées AVANT de sommer
GROUP BY
ORDER BY ;
where-vs-having-erreur-agregations · 06

Impact concret :

-- Mesurer l'écart entre les deux approches
WITH
sans_filtre AS (
SELECT DATE_TRUNC('month', created_at) AS mois, SUM(montant) AS ca
FROM orders GROUP BY
),
avec_filtre AS (
SELECT DATE_TRUNC('month', created_at) AS mois, SUM(montant) AS ca
FROM orders WHERE statut = 'livré' GROUP BY
)
SELECT
s.mois,
s.ca AS ca_toutes_commandes,
a.ca AS ca_livrees_seulement,
s.ca - a.ca AS commandes_annulees_incluses_par_erreur
FROM sans_filtre s
JOIN avec_filtre a ON s.mois = a.mois
ORDER BY s.mois;
where-vs-having-erreur-agregations · 07 · Impact concret :

❌ Erreur 3 — Filtrer un alias du SELECT dans WHERE ou HAVING

-- ❌ L'alias 'ca_total' n'existe pas encore au moment où WHERE est évalué
SELECT user_id, SUM(montant) AS ca_total
FROM orders
WHERE ca_total > -- erreur : alias non disponible dans WHERE
GROUP BY user_id;
 
-- ❌ Même problème avec HAVING dans certains SGBD
SELECT user_id, SUM(montant) AS ca_total
FROM orders
GROUP BY user_id
HAVING ca_total > ; -- fonctionne dans MySQL, erreur dans PostgreSQL standard
where-vs-having-erreur-agregations · 08
-- ✅ Répéter l'expression dans HAVING (portable, toujours correct)
SELECT user_id, SUM(montant) AS ca_total
FROM orders
GROUP BY user_id
HAVING SUM(montant) > ;
 
-- ✅ Utiliser une sous-requête ou une CTE si tu veux filtrer sur un alias
WITH ca_par_user AS (
SELECT user_id, SUM(montant) AS ca_total
FROM orders
GROUP BY user_id
)
SELECT * FROM ca_par_user WHERE ca_total > ;
where-vs-having-erreur-agregations · 09

Compatibilité par SGBD :

SGBD

Alias dans HAVING ?

PostgreSQL

❌ Non (erreur)

MySQL

✅ Oui (extension non-standard)

BigQuery

✅ Oui

Snowflake

✅ Oui

SQL Server

❌ Non (erreur)

_Recommandation : ne pas dépendre des extensions non-standard. Répéter l'expression dans HAVING est toujours portable._


❌ Erreur 4 — Combiner WHERE et HAVING de façon contradictoire

-- Contexte : rapport des ventes par commercial
-- Objectif : commerciaux ayant fait > 10 ventes de produits "Premium"
 
-- ❌ Logique contradictoire : WHERE filtre les produits, HAVING filtre le count
-- mais le count ne compte que les lignes restantes après WHERE
 
SELECT
commercial_id,
COUNT(*) AS nb_ventes,
SUM(montant) AS ca
FROM ventes
WHERE categorie = 'Premium' -- garde seulement les lignes Premium
AND statut = 'conclue'
GROUP BY commercial_id
HAVING COUNT(*) > ; -- compte les ventes Premium conclues > 10
-- ← Ici WHERE et HAVING sont cohérents, c'est correct ✅
 
-- ❌ Cas vraiment contradictoire : filtrer sur le même champ dans WHERE et HAVING
SELECT commercial_id, COUNT(*) AS nb_ventes
FROM ventes
WHERE montant > -- lignes avec montant > 100
GROUP BY commercial_id
HAVING AVG(montant) < ; -- moyenne des lignes > 100 < 200
-- ← Logiquement valide mais le résultat peut surprendre :
-- la moyenne est calculée SEULEMENT sur les lignes avec montant > 100
-- Les ventes à 50€ ne sont pas dans le calcul de la moyenne
where-vs-having-erreur-agregations · 10
-- ✅ Toujours documenter l'intention pour éviter la confusion
SELECT
commercial_id,
COUNT(*) AS nb_ventes_premium, -- seulement les ventes Premium (filtré par WHERE)
AVG(montant) AS panier_moyen_premium -- moyenne des ventes Premium uniquement
FROM ventes
WHERE categorie = 'Premium' -- ← périmètre : Premium seulement
AND statut = 'conclue'
GROUP BY commercial_id
HAVING COUNT(*) > -- ← seuil : > 10 ventes Premium conclues
ORDER BY nb_ventes_premium DESC;
where-vs-having-erreur-agregations · 11

🧪 Teste les 4 erreurs sur des données réelles

Ces 4 patterns reviennent dans presque tous les entretiens data. Modai Lab te soumet des requêtes cassées : à toi d'identifier WHERE ou HAVING mal placé.

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

_Score immédiat · Explication de chaque erreur · Niveau calibré automatiquement_


PARTIE 3 — Cas business réels


Cas réel 1 — E-commerce : taux de conversion par source d'acquisition

Objectif : identifier les sources qui génèrent plus de 100 acheteurs avec un taux de conversion > 5%.

-- ❌ Version avec les deux filtres au mauvais endroit
SELECT
source,
COUNT(DISTINCT user_id) AS visiteurs,
COUNT(DISTINCT CASE WHEN a_achete THEN user_id END) AS acheteurs
FROM sessions
HAVING COUNT(DISTINCT user_id) > -- ❌ devrait être dans WHERE ou HAVING mais cohérent ?
AND COUNT(DISTINCT CASE WHEN a_achete THEN user_id END) * .
/ COUNT(DISTINCT user_id) > .
GROUP BY source;
-- Erreur : HAVING avant GROUP BY syntaxiquement
where-vs-having-erreur-agregations · 12
-- ✅ Version correcte
SELECT
source,
COUNT(DISTINCT user_id) AS visiteurs,
COUNT(DISTINCT CASE WHEN a_achete THEN user_id END) AS acheteurs,
ROUND(
. * COUNT(DISTINCT CASE WHEN a_achete THEN user_id END)
/ NULLIF(COUNT(DISTINCT user_id), ),
) AS taux_conv_pct
FROM sessions
WHERE created_at >= '2024-01-01' -- WHERE : filtre sur colonne brute
GROUP BY source
HAVING COUNT(DISTINCT user_id) > -- HAVING : filtre sur agrégat
AND COUNT(DISTINCT CASE WHEN a_achete THEN user_id END) * .
/ NULLIF(COUNT(DISTINCT user_id), ) > .
ORDER BY taux_conv_pct DESC;
where-vs-having-erreur-agregations · 13

Cas réel 2 — SaaS : détecter les comptes à risque de churn

Objectif : comptes actifs depuis plus de 6 mois avec moins de 5 connexions dans les 30 derniers jours.

-- ❌ Version fausse : WHERE sur un agrégat
SELECT
account_id,
COUNT(*) AS nb_connexions_recentes
FROM logins
WHERE created_at > NOW() - INTERVAL '30 days'
AND COUNT(*) < -- ❌ erreur SQL : agrégat dans WHERE
GROUP BY account_id;
where-vs-having-erreur-agregations · 14
-- ✅ Version correcte
SELECT
a.account_id,
a.created_at AS date_inscription,
COUNT(l.id) AS nb_connexions_30j
FROM accounts a
LEFT JOIN logins l
ON a.account_id = l.account_id
AND l.created_at > NOW() - INTERVAL '30 days' -- filtre dans le ON, pas WHERE
WHERE a.created_at < NOW() - INTERVAL '6 months' -- WHERE : filtre sur colonne brute (ancienneté)
AND a.statut = 'actif'
GROUP BY a.account_id, a.created_at
HAVING COUNT(l.id) < -- HAVING : filtre sur l'agrégat
ORDER BY nb_connexions_30j ASC;
where-vs-having-erreur-agregations · 15

Cas réel 3 — Finance : identifier les fournisseurs à auditer

Objectif : fournisseurs avec plus de 20 factures dont le montant moyen est supérieur à 5 000€ et au moins une facture en retard.

-- ❌ Mélange WHERE/HAVING incohérent
SELECT
fournisseur_id,
COUNT(*) AS nb_factures,
AVG(montant) AS montant_moyen
FROM factures
WHERE AVG(montant) > -- ❌ agrégat dans WHERE
AND statut = 'en_retard' -- fausse le COUNT : ne compte que les retards
GROUP BY fournisseur_id
HAVING COUNT(*) > ;
where-vs-having-erreur-agregations · 16
-- ✅ Version correcte avec logique séparée proprement
SELECT
fournisseur_id,
COUNT(*) AS nb_factures_total,
AVG(montant) AS montant_moyen,
COUNT(CASE WHEN statut = 'en_retard' THEN END) AS nb_retards
FROM factures
WHERE annee_fiscale = -- WHERE : périmètre temporel (colonne brute)
GROUP BY fournisseur_id
HAVING COUNT(*) > -- HAVING : filtre sur nb total de factures
AND AVG(montant) > -- HAVING : filtre sur la moyenne
AND COUNT(CASE WHEN statut = 'en_retard' THEN END) >= -- HAVING : au moins un retard
ORDER BY nb_retards DESC, montant_moyen DESC;
where-vs-having-erreur-agregations · 17

🧪 Pratique sur les cas métiers réels

Sales, RH, Finance, E-commerce : Modai Lab propose ces scénarios avec de vraies données imparfaites. Le feedback t'indique exactement quel filtre est mal placé et pourquoi.

👉 Pratiquer sur modai-lab.com/test-sql/

_Scénarios data réels · Feedback senior · Progression mesurée_


PARTIE 4 — Prompt Claude pour détecter les filtres mal placés

Copie ce prompt avec ta requête pour obtenir un diagnostic en 30 secondes.


Prompt d'audit complet

Tu es un expert SQL. Audite la requête suivante et identifie les erreurs
liées à WHERE et HAVING.
 
REQUÊTE À AUDITER :
[coller ta requête]
 
CONTEXTE MÉTIER :
- Objectif : [ex: compter les clients avec plus de 3 commandes livrées]
- Résultat attendu : [ex: une ligne par client avec son nombre de commandes]
 
Analyse ces points dans l'ordre :
 
1. ORDRE D'EXÉCUTION
- Quelles clauses s'exécutent avant GROUP BY ?
- Quelles clauses s'exécutent après GROUP BY ?
- Y a-t-il des alias du SELECT utilisés dans WHERE ou HAVING ?
 
2. WHERE
- Tous les filtres WHERE portent-ils sur des colonnes brutes (pas d'agrégats) ?
- Y a-t-il des filtres WHERE qui devraient être dans HAVING ?
- Un filtre WHERE exclut-il des lignes qui devraient entrer dans l'agrégation ?
 
3. HAVING
- Tous les filtres HAVING portent-ils sur des agrégats ou des expressions agrégées ?
- Y a-t-il des filtres HAVING qui devraient être dans WHERE (pour des raisons de performance) ?
- Les filtres HAVING sont-ils portables entre SGBD (pas d'alias) ?
 
4. CORRECTION
- Montre la requête corrigée avec commentaires
- Explique l'impact sur le résultat si les filtres étaient mal placés
 
Format : ❌ pour chaque problème trouvé, ✅ pour les points corrects.
where-vs-having-erreur-agregations · 18

Prompt rapide

Audite cette requête SQL pour détecter les filtres WHERE et HAVING mal placés.
Indique pour chaque filtre s'il est au bon endroit et pourquoi.
Si un filtre est mal placé, montre la version corrigée.
 
[coller ta requête]
where-vs-having-erreur-agregations · 19

Checklist à appliquer avant de soumettre une requête

Pour chaque filtre de ta requête :
 
□ Ce filtre porte sur une colonne brute (pas calculée) → WHERE
□ Ce filtre porte sur COUNT(), SUM(), AVG(), MIN(), MAX() → HAVING
□ Ce filtre porte sur un alias du SELECT → HAVING ou CTE
□ J'ai vérifié qu'aucun agrégat n'est dans le WHERE
□ J'ai vérifié qu'aucune colonne brute n'est dans le HAVING par erreur
□ Les alias du SELECT ne sont pas référencés dans WHERE (non portable)
where-vs-having-erreur-agregations · 20

Récapitulatif — WHERE vs HAVING en une page

WHERE

HAVING

S'exécute

Avant GROUP BY

Après GROUP BY

Voit

Les lignes brutes

Les groupes agrégés

Accepte

Colonnes, expressions, sous-requêtes

Fonctions d'agrégation

N'accepte pas

COUNT(), SUM(), AVG()...

Usage typique

Filtrer par statut, date, pays...

Filtrer par total, moyenne, count...

Impact si mal placé

Erreur SQL (agrégat) ou résultat faux (lignes exclues avant agrégation)

Résultat faux (colonnes non agrégées dans HAVING)


✅ Checklist finale

  • ☐ Je comprends que WHERE s'exécute avant GROUP BY et HAVING après

  • ☐ Je n'utilise jamais COUNT(), SUM(), AVG() dans un WHERE

  • ☐ Je n'utilise jamais une colonne brute dans un HAVING par erreur

  • ☐ Je répète l'expression dans HAVING plutôt que d'utiliser l'alias du SELECT

  • ☐ Je sais combiner WHERE et HAVING dans la même requête avec intention

  • ☐ Je peux expliquer pourquoi un filtre mal placé fausse le résultat


🚀 Pratiquer sur Modai Lab

WHERE vs HAVING est testé dans presque tous les entretiens data. La seule façon de ne plus jamais se tromper : pratiquer sur des cas réels avec du feedback immédiat.

👉 modai-lab.com/test-sql/ — Exercices calibrés · Feedback senior · Parcours entretien


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