GROUP BY et HAVING en SQL : guide complet avec exercices
Maîtrisez GROUP BY et HAVING en SQL : syntaxe, fonctions d'agrégation, erreurs courantes et 10 exercices corrigés pour l'entretien technique.
Thomas Leroy
GROUP BY et HAVING sont les deux clauses SQL que chaque candidat doit maîtriser sur le bout des doigts. Ce sont les premières questions posées en entretien technique, et pourtant une majorité de candidats butent sur les subtilités — agrégation partielle, différence entre WHERE et HAVING, ou encore GROUP BY avec plusieurs colonnes.
Ce guide complet, rédigé par Thomas Leroy, couvre tout ce que vous devez savoir : de la syntaxe de base aux cas avancés, avec 10 exercices corrigés pour valider vos acquis.
📌 Ce qu'il faut retenir
WHERE filtre les lignes, HAVING filtre les groupes — c'est la question la plus posée
Chaque colonne dans SELECT doit être dans GROUP BY ou dans une agrégation
COUNT(*) vs COUNT(col) vs COUNT(DISTINCT col) : trois comportements différents
GROUP BY sur plusieurs colonnes crée des sous-groupes
Le pivot avec CASE WHEN + SUM est un pattern fondamental
L'ordre d'exécution SQL (FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY) explique tout
Les fondamentaux de GROUP BY
Qu'est-ce que GROUP BY ?
GROUP BY regroupe les lignes qui partagent les mêmes valeurs dans une ou plusieurs colonnes, puis applique une fonction d'agrégation (SUM, COUNT, AVG, etc.) à chaque groupe.
Prenons une table commandes :
id
client
produit
montant
date_commande
1
Alice
Widget A
150
2026-01-05
2
Alice
Widget B
200
2026-01-12
3
Bob
Widget A
300
2026-01-08
4
Bob
Widget A
250
2026-01-20
5
Claire
Widget B
175
2026-01-15
6
Alice
Widget A
180
2026-02-01
7
Bob
Widget B
220
2026-02-05
8
Claire
Widget A
310
2026-02-10
SELECT client, SUM(montant) AS total
FROM commandes
GROUPBY client;
<div style="overflow-x:auto;margin:20px 0;"><table><thead style="background-color:#0F172A;color:white;"><tr><th style="padding:12px 16px;text-align:left">client</th><th style="padding:12px 16px;text-align:left">total</th></tr></thead><tbody><tr style="background:#f8fafc"><td style="padding:12px 16px">Alice</td><td style="padding:12px 16px">530</td></tr><tr><td style="padding:12px 16px">Bob</td><td style="padding:12px 16px">770</td></tr><tr style="background:#f8fafc"><td style="padding:12px 16px">Claire</td><td style="padding:12px 16px">485</td></tr></tbody></table></div>
Les 8 lignes sont réduites à 3 groupes (un par client). Pour chaque groupe, `SUM(montant)` calcule le total.
### L'ordre d'exécution SQL
Pour comprendre GROUPBY et HAVING, il faut connaître l'ordre d'exécution d'une requête SQL. Ce n'est **pas** l'ordre dans lequel vous écrivez les clauses :
1. **FROM** — sélection des tables et JOINs
2. **WHERE** — filtre les lignes individuelles
3. **GROUP BY** — regroupe les lignes
4. **HAVING** — filtre les groupes
5. **SELECT** — calcul des expressions et alias
6. **ORDER BY** — tri du résultat
7. **LIMIT** — restriction du nombre de lignes
<div class="callout callout-tip">
<p class="callout-title">💡 Bon à savoir</p>
<p>C'est pourquoi vous ne pouvez pas utiliser un alias défini dans SELECT à l'intérieur de WHERE ou HAVING (sauf dans certains SGBD comme MySQL qui le tolèrent).</p>
</div>
### GROUP BY sur plusieurs colonnes
Vous pouvez grouper sur plusieurs colonnes pour obtenir des sous-groupes plus fins :
```sql
SELECT client, produit, SUM(montant) AS total
FROM commandes
GROUP BY client, produit;
<div style="overflow-x:auto;margin:20px 0;"><table>
<thead style="background-color:#0F172A;color:white;">
<tr><th style="padding:12px 16px;text-align:left">client</th><th style="padding:12px 16px;text-align:left">produit</th><th style="padding:12px 16px;text-align:left">total</th></tr>
</thead><tbody>
<tr style="background:#f8fafc"><td style="padding:12px 16px">Alice</td><td style="padding:12px 16px">Widget A</td><td style="padding:12px 16px">330</td></tr>
<tr><td style="padding:12px 16px">Alice</td><td style="padding:12px 16px">Widget B</td><td style="padding:12px 16px">200</td></tr>
<tr style="background:#f8fafc"><td style="padding:12px 16px">Bob</td><td style="padding:12px 16px">Widget A</td><td style="padding:12px 16px">550</td></tr>
<tr><td style="padding:12px 16px">Bob</td><td style="padding:12px 16px">Widget B</td><td style="padding:12px 16px">220</td></tr>
<tr style="background:#f8fafc"><td style="padding:12px 16px">Claire</td><td style="padding:12px 16px">Widget A</td><td style="padding:12px 16px">310</td></tr>
<tr><td style="padding:12px 16px">Claire</td><td style="padding:12px 16px">Widget B</td><td style="padding:12px 16px">175</td></tr>
</tbody></table></div>
Chaque combinaison unique (client, produit) forme un groupe.
## Les fonctions d'agrégation
### Les 5 fonctions essentielles
<div style="overflow-x:auto;margin:20px 0;"><table><thead style="background-color:#0F172A;color:white;"><tr><th style="padding:12px 16px;text-align:left">Fonction</th><th style="padding:12px 16px;text-align:left">Rôle</th><th style="padding:12px 16px;text-align:left">Traitement des NULL</th></tr></thead><tbody><tr style="background:#f8fafc"><td style="padding:12px 16px">COUNT(*)</td><td style="padding:12px 16px">Nombre de lignes</td><td style="padding:12px 16px">Compte toutes les lignes</td></tr><tr><td style="padding:12px 16px">COUNT(col)</td><td style="padding:12px 16px">Nombre de valeurs non-NULL</td><td style="padding:12px 16px">Ignore les NULL</td></tr><tr style="background:#f8fafc"><td style="padding:12px 16px">SUM(col)</td><td style="padding:12px 16px">Somme</td><td style="padding:12px 16px">Ignore les NULL</td></tr><tr><td style="padding:12px 16px">AVG(col)</td><td style="padding:12px 16px">Moyenne</td><td style="padding:12px 16px">Ignore les NULL</td></tr><tr style="background:#f8fafc"><td style="padding:12px 16px">MIN(col)</td><td style="padding:12px 16px">Valeur minimale</td><td style="padding:12px 16px">Ignore les NULL</td></tr><tr><td style="padding:12px 16px">MAX(col)</td><td style="padding:12px 16px">Valeur maximale</td><td style="padding:12px 16px">Ignore les NULL</td></tr></tbody></table></div>
### COUNT(*) vs COUNT(col) vs COUNT(DISTINCT col)
C'est une question classique d'entretien. Voici la différence :
```sqlSELECTCOUNT(*) AS nb_lignes, -- toutes les lignesCOUNT(produit) AS nb_produits, -- lignes où produit n'est pas NULLCOUNT(DISTINCT produit) AS nb_produits_uniques -- valeurs uniques de produitFROM commandes;
nb_lignes
nb_produits
nb_produits_uniques
8
8
2
Les fonctions conditionnelles
Combiner une agrégation avec CASE WHEN est un pattern très puissant :
SELECT
client,
COUNT(*) AS nb_commandes,
SUM(CASEWHEN produit ='Widget A'THEN montant ELSE0END) AS total_widget_a,
SUM(CASEWHEN produit ='Widget B'THEN montant ELSE0END) AS total_widget_b
FROM commandes
GROUPBY client;
client
nb_commandes
total_widget_a
total_widget_b
Alice
3
330
200
Bob
3
550
220
Claire
2
310
175
Ce pattern est souvent appelé « pivot » et revient fréquemment en entretien.
HAVING : filtrer les groupes
Différence entre WHERE et HAVING
C'est LA question qui revient dans tous les entretiens SQL :
WHERE filtre les lignes individuelles avant le regroupement
HAVING filtre les groupes après le regroupement
-- WHERE : filtre les commandes de janvier avant le GROUP BYSELECT client, SUM(montant) AS total
FROM commandes
WHERE date_commande >='2026-01-01'AND date_commande <'2026-02-01'GROUPBY client;
client
total
Alice
350
Bob
550
Claire
175
-- HAVING : filtre les groupes dont le total dépasse 400SELECT client, SUM(montant) AS total
FROM commandes
GROUPBY client
HAVINGSUM(montant) >400;
client
total
Alice
530
Bob
770
Combiner WHERE et HAVING
On peut combiner les deux : WHERE filtre d'abord les lignes, GROUP BY regroupe, puis HAVING filtre les groupes :
SELECT client, SUM(montant) AS total
FROM commandes
WHERE produit ='Widget A'GROUPBY client
HAVINGSUM(montant) >200;
Étape par étape :
WHERE ne garde que les lignes avec produit = 'Widget A' (5 lignes)
GROUP BY regroupe par client
HAVING ne garde que les groupes dont le total > 200
client
total
Alice
330
Bob
550
Claire
310
HAVING avec COUNT
Pattern très courant — trouver les clients ayant passé plus de 2 commandes :
SELECT client, COUNT(*) AS nb_commandes
FROM commandes
GROUPBY client
HAVINGCOUNT(*) >2;
client
nb_commandes
Alice
3
Bob
3
HAVING sans GROUP BY
Oui, HAVING peut fonctionner sans GROUP BY explicite. Dans ce cas, toute la table est considérée comme un seul groupe :
-- Vérifier si la table contient plus de 5 lignesSELECTCOUNT(*) AS nb_total
FROM commandes
HAVINGCOUNT(*) >5;
GROUP BY : cas avancés
GROUP BY avec des expressions
Vous pouvez grouper sur une expression calculée :
-- Ventes par moisSELECT
DATE_TRUNC('month', date_commande) AS mois,
SUM(montant) AS total
FROM commandes
GROUPBY DATE_TRUNC('month', date_commande)
ORDERBY mois;
Ou en MySQL :
SELECT
DATE_FORMAT(date_commande, '%Y-%m') AS mois,
SUM(montant) AS total
FROM commandes
GROUPBY DATE_FORMAT(date_commande, '%Y-%m')
ORDERBY mois;
GROUP BY avec ROLLUP
ROLLUP ajoute des lignes de sous-totaux et un grand total :
SELECT
client,
produit,
SUM(montant) AS total
FROM commandes
GROUPBYROLLUP(client, produit);
client
produit
total
Alice
Widget A
330
Alice
Widget B
200
Alice
NULL
530
Bob
Widget A
550
Bob
Widget B
220
Bob
NULL
770
Claire
Widget A
310
Claire
Widget B
175
Claire
NULL
485
NULL
NULL
1785
Les lignes avec NULL dans produit sont les sous-totaux par client. La dernière ligne est le grand total.
GROUP BY avec CUBE
CUBE génère toutes les combinaisons possibles de sous-totaux :
SELECT
client,
produit,
SUM(montant) AS total
FROM commandes
GROUPBYCUBE(client, produit);
Cela ajoute aussi les sous-totaux par produit (tous clients confondus), en plus de ce que ROLLUP fournit.
GROUP BY avec GROUPING SETS
GROUPING SETS permet de spécifier exactement quels niveaux d'agrégation vous voulez :
SELECT
client,
produit,
SUM(montant) AS total
FROM commandes
GROUPBYGROUPING SETS (
(client, produit), -- détail
(client), -- total par client
() -- grand total
);
Les erreurs courantes avec GROUP BY
Erreur 1 : colonne non agrégée dans SELECT
C'est l'erreur la plus fréquente :
-- ERREUR : date_commande n'est ni dans GROUP BY ni agrégéeSELECT client, date_commande, SUM(montant)
FROM commandes
GROUPBY client;
⚠️ Attention
Chaque colonne dans SELECT doit être soit dans GROUP BY, soit dans une fonction d'agrégation. PostgreSQL, SQL Server et SQLite rejettent cette requête. MySQL en mode non-strict la tolère mais renvoie une valeur arbitraire pour date_commande.
Erreur 2 : HAVING au lieu de WHERE
-- Mauvaise pratique : utiliser HAVING pour filtrer des lignesSELECT client, SUM(montant) AS total
FROM commandes
GROUPBY client
HAVING client ='Alice';
-- Bonne pratique : utiliser WHERESELECT client, SUM(montant) AS total
FROM commandes
WHERE client ='Alice'GROUPBY client;
HAVING fonctionne dans les deux cas, mais le filtre dans WHERE est appliqué avant le regroupement, ce qui est plus performant.
Erreur 3 : Confondre COUNT(*) et COUNT(col) avec les JOINs
-- Avec un LEFT JOIN, COUNT(*) compte aussi les lignes NULLSELECT
c.nom,
COUNT(*) AS nb_commandes -- INCORRECT : vaut 1 même sans commandeFROM clients c
LEFTJOIN commandes co ON c.id = co.client_id
GROUPBY c.nom;
-- Correct : compter sur une colonne de la table de droiteSELECT
c.nom,
COUNT(co.id) AS nb_commandes -- 0 si pas de commandeFROM clients c
LEFTJOIN commandes co ON c.id = co.client_id
GROUPBY c.nom;
Erreur 4 : Oublier DISTINCT dans COUNT
-- Nombre de clients distincts qui ont commandé chaque produitSELECT
produit,
COUNT(client) AS nb_clients, -- compte les doublonsCOUNT(DISTINCT client) AS nb_clients_uniques -- correctFROM commandes
GROUPBY produit;
Erreur 5 : Mauvaise gestion des NULL en agrégation
-- Piège classique avec AVGSELECT
client,
AVG(remise) AS remise_moyenne -- NULL si tous sont NULLFROM commandes
GROUPBY client;
-- Plus robuste avec COALESCESELECT
client,
AVG(COALESCE(remise, 0)) AS remise_moyenne
FROM commandes
GROUPBY client;
Erreur 6 : GROUP BY avec des colonnes calculées
-- ERREUR : l'expression doit être répétéeSELECTYEAR(date_commande) *100+MONTH(date_commande) AS periode,
SUM(montant) AS total
FROM commandes
GROUPBY periode; -- certains SGBD l'acceptent, d'autres non-- Solution portableSELECTYEAR(date_commande) *100+MONTH(date_commande) AS periode,
SUM(montant) AS total
FROM commandes
GROUPBYYEAR(date_commande) *100+MONTH(date_commande);
Erreur 7 : Mélanger agrégation et non-agrégation dans HAVING
-- ERREUR courante dans certains SGBDSELECT client, SUM(montant) AS total
FROM commandes
GROUPBY client
HAVING montant >200; -- montant n'est pas agrégé !-- Correct : utiliser WHERE pour filtrer les lignesSELECT client, SUM(montant) AS total
FROM commandes
WHERE montant >200GROUPBY client;
-- Ou utiliser HAVING avec agrégationSELECT client, SUM(montant) AS total
FROM commandes
GROUPBY client
HAVINGMAX(montant) >200;
Les bonnes pratiques et patterns avancés
Gestion des valeurs NULL dans les fonctions d'agrégation
Les fonctions d'agrégation ignorent les valeurs NULL, ce qui peut parfois créer des surprises :
-- Si certains montants sont NULLSELECT
client,
COUNT(*) AS nb_lignes, -- compte toutes les lignesCOUNT(montant) AS nb_montants, -- ignore les NULLSUM(montant) AS total, -- ignore les NULLAVG(montant) AS moyenne -- moyenner sur les non-NULL uniquementFROM commandes
GROUPBY client;
💡 Bon à savoir
Pour traiter les NULL comme des zéros, utilisez COALESCE : SUM(COALESCE(montant, 0)). Pour considérer les NULL dans le calcul de moyenne : AVG(COALESCE(montant, 0)).
Pattern de classification par tranche
Un pattern très courant en analyse de données : classer les clients par tranche de dépenses.
SELECTCASEWHENSUM(montant) <200THEN'Petit client'WHENSUM(montant) <500THEN'Client moyen'ELSE'Gros client'ENDAS categorie,
COUNT(*) AS nb_clients
FROM commandes
GROUPBYCASEWHENSUM(montant) <200THEN'Petit client'WHENSUM(montant) <500THEN'Client moyen'ELSE'Gros client'END;
Analyser les tendances avec LAG et GROUP BY
Combiner GROUP BY avec les fonctions fenêtre pour analyser l'évolution :
SELECT
mois,
total_mois,
LAG(total_mois) OVER (ORDERBY mois) AS total_mois_precedent,
total_mois -LAG(total_mois) OVER (ORDERBY mois) AS evolution
FROM (
SELECT
DATE_TRUNC('month', date_commande) AS mois,
SUM(montant) AS total_mois
FROM commandes
GROUPBY DATE_TRUNC('month', date_commande)
) monthly_sales
ORDERBY mois;
Identifier les clients à risque
Pattern métier : clients qui n'ont pas commandé depuis longtemps.
SELECT
client,
MAX(date_commande) AS derniere_commande,
CURRENT_DATE-MAX(date_commande) AS jours_sans_commande
FROM commandes
GROUPBY client
HAVINGCURRENT_DATE-MAX(date_commande) >30ORDERBY jours_sans_commande DESC;
Calculs de percentiles avec GROUP BY
Pattern avancé pour calculer des percentiles par groupe :
SELECT
produit,
COUNT(*) AS nb_commandes,
PERCENTILE_CONT(0.5) WITHINGROUP (ORDERBY montant) AS mediane,
PERCENTILE_CONT(0.9) WITHINGROUP (ORDERBY montant) AS p90
FROM commandes
GROUPBY produit;
GROUP BY selon le type de profil
Pour les débutants en SQL
Si vous débutez en SQL, concentrez-vous sur ces patterns de base :
Pattern
Cas d'usage
Exemple typique
GROUP BY + COUNT(*)
Compter par catégorie
Nombre de commandes par client
GROUP BY + SUM()
Totaliser par groupe
Chiffre d'affaires par produit
GROUP BY + HAVING
Filtrer après agrégation
Clients ayant commandé > 3 fois
GROUP BY multiple
Sous-groupes détaillés
Ventes par mois et par région
Pour les data analysts
En tant que data analyst, vous devez maîtriser ces techniques avancées fréquemment utilisées :
-- Analyse de cohorte simplifiéeSELECT
DATE_TRUNC('month', premiere_commande) AS cohorte,
COUNT(*) AS nb_clients_acquis,
AVG(total_depense) AS panier_moyen_cohorte
FROM (
SELECT
client,
MIN(date_commande) AS premiere_commande,
SUM(montant) AS total_depense
FROM commandes
GROUPBY client
) client_stats
GROUPBY DATE_TRUNC('month', premiere_commande)
ORDERBY cohorte;
Pour les développeurs
Les développeurs doivent optimiser les performances et gérer les cas edge :
-- Utilisation d'index sur les colonnes GROUP BYCREATE INDEX idx_commandes_client_date ON commandes (client, date_commande);
-- GROUP BY avec gestion des cas NULLSELECTCOALESCE(region, 'Non spécifié') AS region,
COUNT(*) AS nb_commandes,
SUM(COALESCE(montant, 0)) AS total
FROM commandes
GROUPBYCOALESCE(region, 'Non spécifié')
HAVINGCOUNT(*) >=10; -- Éviter les groupes avec peu de données
Optimisation et performance
Indexation pour GROUP BY
Pour optimiser vos requêtes GROUP BY, créez des index composites :
-- Si vous groupez souvent par (client, produit)CREATE INDEX idx_commandes_group BYON commandes (client, produit);
-- Include des colonnes souvent agrégéesCREATE INDEX idx_commandes_optimized ON commandes (client, produit) INCLUDE (montant, date_commande);
Éviter les GROUP BY coûteux
⚠️ Attention
Grouper sur des expressions complexes ou des fonctions est coûteux. Privilégiez le précalcul ou l'utilisation de colonnes indexées quand possible.
-- Coûteux : calcul à chaque ligneSELECT
CONCAT(UPPER(LEFT(nom, 1)), LOWER(SUBSTRING(nom, 2))) AS nom_formate,
COUNT(*)
FROM clients
GROUPBY CONCAT(UPPER(LEFT(nom, 1)), LOWER(SUBSTRING(nom, 2)));
-- Mieux : précalculer dans une colonneALTER TABLE clients ADDCOLUMN nom_formate VARCHAR(100);
UPDATE clients SET nom_formate = CONCAT(UPPER(LEFT(nom, 1)), LOWER(SUBSTRING(nom, 2)));
CREATE INDEX ON clients (nom_formate);
SELECT nom_formate, COUNT(*)
FROM clients
GROUPBY nom_formate;
10 exercices corrigés GROUP BY et HAVING
Exercice 1 : Ventes par trimestre
Énoncé : Calculez le chiffre d'affaires par trimestre pour l'année 2026.
SELECT
CONCAT('Q', QUARTER(date_vente), ' ', YEAR(date_vente)) AS trimestre,
SUM(montant) AS ca_trimestre,
COUNT(*) AS nb_ventes
FROM ventes
WHEREYEAR(date_vente) =2026GROUPBY QUARTER(date_vente), YEAR(date_vente)
ORDERBYYEAR(date_vente), QUARTER(date_vente);
Points clés :
GROUP BY sur une fonction (QUARTER)
WHERE pour filtrer l'année avant regroupement
ORDER BY pour tri chronologique
Exercice 2 : TOP 5 des clients
Énoncé : Trouvez les 5 clients ayant généré le plus de chiffre d'affaires, avec au moins 3 commandes.
Solution
SELECT
client,
COUNT(*) AS nb_commandes,
SUM(montant) AS ca_total,
AVG(montant) AS panier_moyen
FROM commandes
GROUPBY client
HAVINGCOUNT(*) >=3ORDERBYSUM(montant) DESC
LIMIT 5;
Points clés :
HAVING avec COUNT pour filtrer les groupes
ORDER BY sur une agrégation
LIMIT pour le TOP 5
Exercice 3 : Analyse des doublons
Énoncé : Identifiez les combinaisons (client, produit, date) qui apparaissent plusieurs fois.
Solution
SELECT
client,
produit,
date_commande,
COUNT(*) AS nb_occurrences
FROM commandes
GROUPBY client, produit, date_commande
HAVINGCOUNT(*) >1ORDERBYCOUNT(*) DESC, client;
Points clés :
GROUP BY sur plusieurs colonnes
HAVING > 1 pour trouver les doublons
Tri par nombre d'occurrences décroissant
Exercice 4 : Produits jamais vendus
Énoncé : À partir des tables produits et commandes, trouvez les produits qui n'ont jamais été commandés.
Solution
SELECT
p.nom_produit,
p.categorie,
COUNT(c.id) AS nb_commandes
FROM produits p
LEFTJOIN commandes c ON p.id = c.produit_id
GROUPBY p.id, p.nom_produit, p.categorie
HAVINGCOUNT(c.id) =0ORDERBY p.categorie, p.nom_produit;
Alternative avec NOT EXISTS :
SELECT nom_produit, categorie
FROM produits p
WHERENOTEXISTS (
SELECT1FROM commandes c WHERE c.produit_id = p.id
);
Points clés :
LEFT JOIN pour garder tous les produits
COUNT sur colonne de la table de droite (attention à COUNT(*) vs COUNT(col))
HAVING = 0 pour les produits non vendus
Exercice 5 : Croissance mensuelle
Énoncé : Calculez la croissance du CA mensuel par rapport au mois précédent.
Solution
WITH ca_mensuel AS (
SELECT
DATE_TRUNC('month', date_commande) AS mois,
SUM(montant) AS ca
FROM commandes
GROUPBY DATE_TRUNC('month', date_commande)
)
SELECT
mois,
ca,
LAG(ca) OVER (ORDERBY mois) AS ca_precedent,
ca -LAG(ca) OVER (ORDERBY mois) AS croissance_absolue,
ROUND(
(ca -LAG(ca) OVER (ORDERBY mois)) *100.0/LAG(ca) OVER (ORDERBY mois),
2
) AS croissance_pct
FROM ca_mensuel
ORDERBY mois;
Fonction fenêtre LAG pour comparer avec la période précédente
Exercice 6 : Analyse ABC des produits
Énoncé : Classez les produits en catégories A (80% du CA), B (15%) et C (5%).
Solution
WITH ca_produits AS (
SELECT
produit,
SUM(montant) AS ca_produit
FROM commandes
GROUPBY produit
),
ca_cumule AS (
SELECT
produit,
ca_produit,
SUM(ca_produit) OVER (ORDERBY ca_produit DESC) AS ca_cumule,
SUM(ca_produit) OVER () AS ca_total
FROM ca_produits
)
SELECT
produit,
ca_produit,
ROUND(ca_produit *100.0/ ca_total, 2) AS pct_ca,
ROUND(ca_cumule *100.0/ ca_total, 2) AS pct_cumule,
CASEWHEN ca_cumule *100.0/ ca_total <=80THEN'A'WHEN ca_cumule *100.0/ ca_total <=95THEN'B'ELSE'C'ENDAS categorie_abc
FROM ca_cumule
ORDERBY ca_produit DESC;
Points clés :
Plusieurs CTEs pour structurer la logique
Fonction fenêtre SUM() OVER pour le cumul
Classification avec CASE WHEN
Exercice 7 : Clients fidèles vs nouveaux
Énoncé : Pour chaque mois, comptez les clients nouveaux vs récurrents.
Solution
WITH premieres_commandes AS (
SELECT
client,
MIN(date_commande) AS premiere_commande
FROM commandes
GROUPBY client
),
commandes_enrichies AS (
SELECT
c.*,
DATE_TRUNC('month', c.date_commande) AS mois,
DATE_TRUNC('month', pc.premiere_commande) AS mois_acquisition
FROM commandes c
JOIN premieres_commandes pc ON c.client = pc.client
)
SELECT
mois,
COUNT(DISTINCTCASEWHEN mois = mois_acquisition THEN client END) AS nouveaux_clients,
COUNT(DISTINCTCASEWHEN mois != mois_acquisition THEN client END) AS clients_recurrents,
COUNT(DISTINCT client) AS total_clients_actifs
FROM commandes_enrichies
GROUPBY mois
ORDERBY mois;
Points clés :
Identification de la première commande par client
Jointure pour enrichir les données
COUNT DISTINCT avec CASE WHEN pour compter conditionnellement
Exercice 8 : Détection d'anomalies
Énoncé : Trouvez les jours où le CA a été exceptionnellement élevé (> 2x la moyenne).
Solution
WITH ca_quotidien AS (
SELECTDATE(date_commande) AS jour,
SUM(montant) AS ca_jour,
COUNT(*) AS nb_commandes
FROM commandes
GROUPBYDATE(date_commande)
),
stats_globales AS (
SELECTAVG(ca_jour) AS ca_moyen,
STDDEV(ca_jour) AS ca_stddev
FROM ca_quotidien
)
SELECT
cq.jour,
cq.ca_jour,
cq.nb_commandes,
ROUND(cq.ca_jour / sg.ca_moyen, 2) AS ratio_vs_moyenne,
ROUND((cq.ca_jour - sg.ca_moyen) / sg.ca_stddev, 2) AS z_score
FROM ca_quotidien cq
CROSSJOIN stats_globales sg
WHERE cq.ca_jour > sg.ca_moyen *2ORDERBY cq.ca_jour DESC;
Points clés :
GROUP BY par jour
Calcul de statistiques globales dans une CTE
CROSS JOIN pour comparer chaque jour aux stats globales
Z-score pour mesurer l'écart à la normale
Exercice 9 : Retention mensuelle
Énoncé : Calculez le taux de retention mensuel (clients actifs le mois M qui sont encore actifs le mois M+1).
Solution
WITH clients_mensuels AS (
SELECTDISTINCT
DATE_TRUNC('month', date_commande) AS mois,
client
FROM commandes
),
retention AS (
SELECT
cm1.mois,
COUNT(cm1.client) AS clients_actifs,
COUNT(cm2.client) AS clients_revenus,
ROUND(COUNT(cm2.client) *100.0/COUNT(cm1.client), 2) AS taux_retention
FROM clients_mensuels cm1
LEFTJOIN clients_mensuels cm2 ON cm1.client = cm2.client
AND cm2.mois = cm1.mois +INTERVAL'1 month'GROUPBY cm1.mois
)
SELECT*FROM retention
ORDERBY mois;
Points clés :
DISTINCT pour éviter les doublons par client/mois
Self-join avec décalage temporel
Calcul du ratio de rétention
Exercice 10 : Analyse de panier complexe
Énoncé : Créez un rapport de segmentation client basé sur RFM (Récence, Fréquence, Montant).
Solution
WITH rfm_base AS (
SELECT
client,
MAX(date_commande) AS derniere_commande,
COUNT(*) AS frequence,
SUM(montant) AS montant_total,
AVG(montant) AS panier_moyen
FROM commandes
WHERE date_commande >=CURRENT_DATE-INTERVAL'12 months'GROUPBY client
),
rfm_scores AS (
SELECT
client,
derniere_commande,
frequence,
montant_total,
panier_moyen,
CURRENT_DATE- derniere_commande AS jours_depuis,
NTILE(5) OVER (ORDERBYMAX(date_commande) DESC) AS score_r,
NTILE(5) OVER (ORDERBYCOUNT(*)) AS score_f,
NTILE(5) OVER (ORDERBYSUM(montant)) AS score_m
FROM commandes
WHERE date_commande >=CURRENT_DATE-INTERVAL'12 months'GROUPBY client
),
segments AS (
SELECT*,
CASEWHEN score_r >=4AND score_f >=4AND score_m >=4THEN'Champions'WHEN score_r >=3AND score_f >=3AND score_m >=3THEN'Loyal Customers'WHEN score_r >=4AND score_f <=2THEN'New Customers'WHEN score_r <=2AND score_f >=3THEN'At Risk'WHEN score_r <=2AND score_f <=2THEN'Lost'ELSE'Regular Customers'ENDAS segment
FROM rfm_scores
)
SELECT
segment,
COUNT(*) AS nb_clients,
AVG(frequence) AS freq_moyenne,
AVG(montant_total) AS ca_moyen,
AVG(jours_depuis) AS recence_moyenne
FROM segments
GROUPBY segment
ORDERBY nb_clients DESC;
Points clés :
Segmentation RFM avec NTILE pour créer des quintiles
Classification business avec CASE WHEN
Analyse finale par segment
Récapitulatif des différences SGBD
Les implémentations de GROUP BY varient selon les systèmes de gestion de base de données :
SGBD
Particularités GROUP BY
Extensions spéciales
PostgreSQL
Strict sur les colonnes SELECT
GROUPING SETS, ROLLUP, CUBE
MySQL
Mode strict configurable
WITH ROLLUP
SQL Server
Strict, supporte ROLLUP
GROUPING(), ROLLUP, CUBE
Oracle
Fonctionnalités avancées
GROUPING SETS, CONNECT BY
SQLite
Standard SQL basique
Fonctionnalités limitées
Exemples concrets par SGBD
MySQL avec WITH ROLLUP :
SELECT region, produit, SUM(montant)
FROM ventes
GROUPBY region, produit WITHROLLUP;
WHERE filtre les lignes individuelles avant le regroupement, tandis que HAVING filtre les groupes après l'agrégation. WHERE s'exécute en premier et est plus performant pour les filtres simples. HAVING ne peut filtrer que sur des colonnes présentes dans GROUP BY ou des fonctions d'agrégation.
Pourquoi COUNT(*) et COUNT(colonne) donnent-ils des résultats différents ?
COUNT(*) compte toutes les lignes du groupe, y compris celles avec des valeurs NULL. COUNT(colonne) ne compte que les lignes où la colonne spécifiée n'est pas NULL. COUNT(DISTINCT colonne) compte uniquement les valeurs distinctes non-NULL.
Peut-on utiliser GROUP BY sans fonction d'agrégation ?
Techniquement oui, mais c'est généralement inutile. GROUP BY sans agrégation équivaut à un DISTINCT sur les colonnes spécifiées. Il est plus lisible et performant d'utiliser SELECT DISTINCT dans ce cas.
Comment gérer les colonnes calculées dans GROUP BY ?
Vous devez répéter l'expression complète dans GROUP BY, car l'alias défini dans SELECT n'est pas encore disponible. Certains SGBD comme MySQL tolèrent l'utilisation de l'alias, mais ce n'est pas portable. Alternativement, utilisez une CTE pour clarifier le code.
ROLLUP, CUBE et GROUPING SETS : quelles différences ?
ROLLUP génère des sous-totaux hiérarchiques (A+B, A, total). CUBE génère toutes les combinaisons possibles de sous-totaux (A+B, A, B, total). GROUPING SETS permet de spécifier exactement quels niveaux d'agrégation vous voulez, offrant plus de contrôle.
Pourquoi ma requête GROUP BY est-elle lente ?
Les causes principales sont : absence d'index sur les colonnes GROUP BY, regroupement sur des expressions complexes, trop de groupes distincts, ou utilisation de ROLLUP/CUBE sur de gros volumes. Créez des index composites et évitez les fonctions coûteuses dans GROUP BY.
Comment optimiser GROUP BY avec JOIN ?
Placez les conditions de filtre dans WHERE plutôt que HAVING quand possible. Créez des index sur les colonnes de jointure et de regroupement. Considérez filtrer avant le JOIN avec des sous-requêtes. Pour les LEFT JOIN, attention à COUNT(*) vs COUNT(colonne_droite).
Prêt à vous entraîner ?
50 exercices SQL interactifs avec éditeur en ligne, chronomètre et feedback IA.