Retour au cours

data / sql

Agrégations : GROUP BY et HAVING

Leçon 41 exercice

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 le GROUP BY
  • Distinguer précisément le rôle de WHERE (avant) et HAVING (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.

FonctionRôleExemple
COUNT(*)Nombre de lignesCOUNT(*) = 340 commandes
COUNT(DISTINCT col)Nombre de valeurs distinctesclients uniques ayant commandé
SUM(col)Sommechiffre d'affaires total
AVG(col)Moyennepanier moyen
MIN(col) / MAX(col)Extrêmescommande 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

sql
-- 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éral

Résumé

  • WHERE filtre les lignes avant regroupement, HAVING filtre les groupes après agrégation.
  • Toute colonne du SELECT non agrégée doit apparaître dans GROUP BY.
  • COUNT(*) compte les lignes, COUNT(colonne) ignore les NULL de cette colonne.
  • FILTER (WHERE ...) ou CASE WHEN permettent des agrégations conditionnelles.
  • ROLLUP/CUBE génèrent des sous-totaux hiérarchiques en une seule requête.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →