data / sql
Agrégations : GROUP BY et HAVING
Explication
Ce que vous allez apprendre
- Utiliser les fonctions d'agrégation
COUNT,SUM,AVG,MIN,MAX - Regrouper des lignes par catégorie avec
GROUP BY - Filtrer des groupes déjà agrégés avec
HAVING, après leGROUP BY - Distinguer précisément le rôle de
WHERE(avant) etHAVING(après) - Construire des agrégations conditionnelles et des sous-totaux avec
ROLLUP
Dans quel contexte ?
Un analyste doit produire un rapport mensuel du chiffre d'affaires par client à partir de la table commandes, en ne gardant que les clients ayant dépensé plus de 1 000 euros sur la période. Cette question - "combien PAR client, mais seulement ceux au-dessus d'un seuil" - est exactement le duo GROUP BY + HAVING que cette leçon explique pas à pas.
D'abord, une question toute simple
Imagine que tu veuilles juste connaître le nombre total de commandes. Pas besoin de lister chaque ligne : COUNT(*) prend toutes les lignes et en fait une seule valeur de synthèse. C'est ça, une fonction d'agrégation.
Il existe plusieurs façons de résumer
SUM additionne, AVG fait une moyenne, MIN/MAX trouvent les extrêmes. Toutes fonctionnent sur le même principe : plusieurs lignes en entrée, une seule valeur en sortie, un peu comme additionner à la main tous les tickets de caisse d'une journée.
Mais un total global ne suffit pas toujours
La vraie question posée est souvent plus fine : pas "combien au total ?" mais "combien PAR client ?". Un simple COUNT(*) sur toute la table ne peut pas répondre à ça, il faut d'abord découper les lignes en paquets.
GROUP BY : découper avant de résumer
GROUP BY client_id regroupe les lignes qui partagent la même valeur dans cette colonne, puis applique l'agrégation séparément à l'intérieur de chaque paquet. Résultat : une ligne de synthèse par client, au lieu d'une seule ligne pour tout le monde.
| Fonction | Rôle | Exemple |
|---|---|---|
COUNT(*) | Nombre de lignes | COUNT(*) = 340 commandes |
COUNT(DISTINCT col) | Nombre de valeurs distinctes | clients uniques ayant commandé |
SUM(col) | Somme | chiffre d'affaires total |
AVG(col) | Moyenne | panier moyen |
MIN(col) / MAX(col) | Extrêmes | commande la moins/plus chère |
Prérequis
Cette leçon suppose que tu es à l'aise avec WHERE et le filtrage de lignes (voir la leçon 2). GROUP BY s'appuie directement sur cette notion de ligne filtrée.
Un nouveau besoin apparaît : filtrer les groupes
Maintenant qu'on a un total par client, on veut parfois ne garder que les clients ayant dépensé plus de 1000 euros. Le problème, c'est que WHERE filtre les lignes brutes AVANT que les groupes existent : on ne peut donc pas y écrire WHERE SUM(montant) > 1000.
La solution : HAVING filtre après coup
HAVING filtre les groupes déjà calculés, une fois l'agrégation faite. L'ordre logique à retenir est donc : d'abord WHERE (sur les lignes brutes), puis GROUP BY (le regroupement et le calcul), puis HAVING (le filtre sur les résultats).
Piège fréquent
Écrire WHERE SUM(montant) > 1000 provoque une erreur de syntaxe dans la plupart des SGBD, car au moment où WHERE s'exécute, les groupes n'existent pas encore. Il faut impérativement utiliser HAVING SUM(montant) > 1000 à la place.
Et la suite
Cette logique de regroupement prépare directement la leçon sur les jointures, où l'on combine souvent plusieurs tables avant d'en agréger les données ensemble.
Commandes & code
Agrégations : GROUP BY et HAVING
-- Fonctions d'agrégation de base
SELECT COUNT(*) FROM commandes; -- nombre total de lignes
SELECT COUNT(DISTINCT client_id) FROM commandes; -- nombre de clients distincts
SELECT SUM(montant) FROM commandes;
SELECT AVG(montant) FROM commandes;
SELECT MIN(montant), MAX(montant) FROM commandes;
-- GROUP BY : agréger par groupe
SELECT client_id, COUNT(*) AS nb_commandes, SUM(montant) AS total_depense
FROM commandes
GROUP BY client_id;
-- Toute colonne non agrégée dans le SELECT DOIT figurer dans le GROUP BY
SELECT categorie, fournisseur, COUNT(*) AS nb_produits, AVG(prix) AS prix_moyen
FROM produits
GROUP BY categorie, fournisseur;
-- HAVING filtre APRÈS l'agrégation (WHERE filtre AVANT)
SELECT client_id, SUM(montant) AS total_depense
FROM commandes
GROUP BY client_id
HAVING SUM(montant) > 1000;
-- Combiner WHERE (filtre les lignes brutes) et HAVING (filtre les groupes)
SELECT categorie, COUNT(*) AS nb, AVG(prix) AS prix_moyen
FROM produits
WHERE stock > 0 -- exclut les produits en rupture avant agrégation
GROUP BY categorie
HAVING COUNT(*) >= 5 -- ne garde que les catégories avec 5+ produits en stock
ORDER BY prix_moyen DESC;
-- Agrégations conditionnelles (pivot manuel)
SELECT
client_id,
COUNT(*) FILTER (WHERE statut = 'livree') AS nb_livrees, -- PostgreSQL
COUNT(*) FILTER (WHERE statut = 'annulee') AS nb_annulees,
SUM(CASE WHEN statut = 'livree' THEN montant ELSE 0 END) AS ca_livre -- portable
FROM commandes
GROUP BY client_id;
-- GROUPING SETS / ROLLUP / CUBE : sous-totaux multi-niveaux
SELECT categorie, fournisseur, SUM(prix * stock) AS valeur
FROM produits
GROUP BY ROLLUP (categorie, fournisseur);
-- produit : (categorie, fournisseur), (categorie, NULL), (NULL, NULL) -> total généralRésumé
WHEREfiltre les lignes avant regroupement,HAVINGfiltre les groupes après agrégation.- Toute colonne du
SELECTnon agrégée doit apparaître dansGROUP BY. COUNT(*)compte les lignes,COUNT(colonne)ignore les NULL de cette colonne.FILTER (WHERE ...)ouCASE WHENpermettent des agrégations conditionnelles.ROLLUP/CUBEgénèrent des sous-totaux hiérarchiques en une seule requête.
Exercices pratiques
Mission : produire le rapport mensuel des clients à forte valeur
Objectif : Construire une requête d'agrégation qui filtre les lignes brutes puis les groupes, et interpréter un ROLLUP.
Contexte
Un analyste te demande le chiffre d'affaires par client sur la table commandes, en ne gardant que les clients ayant dépensé plus de 1000 euros sur des commandes déjà livrées (les commandes annulées ne doivent pas compter dans ce total). Il te fournit aussi une requête ROLLUP dont une ligne le déroute.