Développement Web

GROUP BY et HAVING : agréger et résumer en SQL

12 دقائق للقراءة
📍 À lire d abord : Apprendre le SQL : le guide complet pour débuter. Ce tutoriel approfondit l agrégation, troisième brique du parcours.

Jusqu ici, chaque requête renvoyait des lignes brutes. Mais une question métier ressemble plutôt à « combien de commandes par client ? » ou « quel est le chiffre d affaires par catégorie ? ». Répondre exige de résumer plusieurs lignes en un seul chiffre. C est le rôle des fonctions d agrégation, de la clause GROUP BY qui définit les groupes, et de HAVING qui filtre ces groupes.

Ce tutoriel vous apprend à passer des données détaillées aux indicateurs synthétiques qui alimentent un tableau de bord. Nous restons sur la boutique TechStock et ses commandes déjà saisies.

🎯 Ce que vous allez apprendre

  • Utiliser les fonctions d agrégation COUNT, SUM, AVG, MIN et MAX.
  • Distinguer COUNT(*), COUNT(colonne) et COUNT(DISTINCT colonne).
  • Regrouper les lignes avec GROUP BY pour calculer un agrégat par catégorie.
  • Respecter la règle qui lie les colonnes du SELECT au GROUP BY.
  • Filtrer les groupes avec HAVING et comprendre sa différence avec WHERE.
  • Combiner WHERE, GROUP BY, HAVING et ORDER BY dans une même requête.

🛠️ Ce que vous allez construire

Vous allez produire les indicateurs clés de la boutique : le nombre de commandes par client, le chiffre d affaires par catégorie, le panier moyen, et la liste des meilleures catégories. Ces requêtes sont le cœur de tout tableau de bord de gestion.

Prérequis

  • Connaître SELECT, WHERE et tri (interroger une table).
  • Savoir joindre des tables (les jointures).
  • La base TechStock avec produits, clients, commandes et lignes_commande remplis.
  • ⏱️ Temps estimé : environ 45 minutes.

Étape 1 — Les agrégats globaux

Une fonction d agrégation prend un ensemble de lignes et le condense en une seule valeur. Les cinq fonctions de base couvrent presque tous les besoins : COUNT compte les lignes, SUM additionne, AVG calcule la moyenne, MIN et MAX donnent les extrêmes. Appliquées sans GROUP BY, elles résument toute la table en une ligne.

SELECT COUNT(*)      AS nb_produits,
       AVG(prix)     AS prix_moyen,
       MIN(prix)     AS moins_cher,
       MAX(prix)     AS plus_cher
FROM produits;

Cette requête renvoie une seule ligne résumant le catalogue entier. Remarquez qu on ne peut pas mélanger dans ce SELECT une colonne brute comme nom avec ces agrégats : il n y aurait pas de sens à afficher un seul nom à côté d une moyenne portant sur tous. Cette contrainte deviendra une règle précise à l étape 4.

Point d étape — Une seule ligne doit apparaître, avec le compte total de produits et les statistiques de prix. Si vous obtenez plusieurs lignes, vous avez sans doute laissé une colonne non agrégée.

Étape 2 — Les subtilités de COUNT

COUNT mérite une attention particulière car il a trois formes au comportement différent. COUNT(*) compte toutes les lignes, y compris celles contenant des NULL. COUNT(colonne) ne compte que les lignes où cette colonne n est pas NULL. COUNT(DISTINCT colonne) compte les valeurs distinctes non NULL.

SELECT COUNT(*)                 AS lignes_totales,
       COUNT(stock)             AS avec_stock_connu,
       COUNT(DISTINCT categorie_id) AS nb_categories
FROM produits;

Sur nos données, lignes_totales vaut sept, mais avec_stock_connu vaut six, car l écran 27 pouces a un stock NULL qui n est pas compté. Et nb_categories révèle combien de catégories distinctes sont effectivement utilisées. Cette distinction est une source classique d erreurs de comptage : choisissez consciemment la forme adaptée à votre question.

Point d étape — Vous devez observer un écart d une unité entre COUNT(*) et COUNT(stock). Cet écart correspond exactement au produit dont le stock est NULL.

Étape 3 — Regrouper avec GROUP BY

L intérêt de l agrégation apparaît pleinement quand on calcule un indicateur non pas pour toute la table, mais pour chaque groupe. GROUP BY rassemble les lignes partageant une même valeur, et l agrégat est alors calculé séparément pour chaque groupe. Pour compter les produits par catégorie :

SELECT categorie_id,
       COUNT(*) AS nb_produits,
       AVG(prix) AS prix_moyen
FROM produits
GROUP BY categorie_id
ORDER BY categorie_id;

Le résultat comporte une ligne par catégorie, chacune avec son nombre de produits et son prix moyen. Le moteur a regroupé les sept produits selon leur categorie_id, puis appliqué COUNT et AVG à l intérieur de chaque groupe. C est le passage du détail au résumé, catégorie par catégorie.

Point d étape — Autant de lignes que de catégories représentées. La somme des nb_produits sur toutes les lignes doit redonner sept, le total du catalogue.

Étape 4 — La règle d or du GROUP BY

Voici la règle qui déroute le plus les débutants, et qu il faut graver une fois pour toutes : dans une requête avec GROUP BY, chaque colonne du SELECT doit être soit présente dans le GROUP BY, soit enveloppée dans une fonction d agrégation. La raison est logique : si vous regroupez par catégorie, chaque groupe contient plusieurs produits de noms différents ; le moteur ne saurait pas lequel afficher.

-- INCORRECT : nom n est ni groupé ni agrégé
SELECT categorie_id, nom, COUNT(*)
FROM produits
GROUP BY categorie_id;

PostgreSQL rejette cette requête avec un message clair indiquant que la colonne nom doit apparaître dans le GROUP BY ou être agrégée. Certains moteurs comme MySQL tolèrent parfois ce flou et renvoient une valeur arbitraire, ce qui est pire car l erreur passe inaperçue. La bonne pratique est universelle : n affichez que des colonnes groupées ou agrégées.

Point d étape — Reproduisez l erreur volontairement pour voir le message du moteur. Savoir le reconnaître vous fera gagner un temps considérable plus tard.

Étape 5 — Agréger sur des tables jointes

Les agrégats les plus utiles combinent plusieurs tables. Le chiffre d affaires par catégorie, par exemple, vit dans les lignes de commande (quantité fois prix unitaire), reliées aux produits, eux-mêmes reliés aux catégories. On joint d abord, on regroupe ensuite, on agrège enfin.

SELECT cat.nom AS categorie,
       SUM(lc.quantite * lc.prix_unitaire) AS chiffre_affaires,
       SUM(lc.quantite) AS unites_vendues
FROM lignes_commande AS lc
JOIN produits AS p     ON p.id = lc.produit_id
JOIN categories AS cat ON cat.id = p.categorie_id
GROUP BY cat.nom
ORDER BY chiffre_affaires DESC;

Chaque ligne de commande contribue à son groupe de catégorie. SUM additionne les montants ligne à ligne, et le résultat classe les catégories de la plus rentable à la moins rentable. On regroupe ici par cat.nom ; on pourrait aussi regrouper par l identifiant pour plus de robustesse, en ajoutant le nom au GROUP BY.

Point d étape — Le résultat doit classer les catégories par chiffre d affaires. Vérifiez qu une catégorie sans aucune vente n apparaît pas : la jointure interne l exclut, ce qui est attendu ici.

Étape 6 — Filtrer les groupes avec HAVING

WHERE filtre les lignes avant le regroupement ; il ne peut donc pas filtrer sur un agrégat, qui n existe pas encore à ce stade. Pour ne garder que les catégories dépassant un certain chiffre d affaires, on utilise HAVING, qui s applique après le GROUP BY, sur les agrégats calculés.

SELECT cat.nom AS categorie,
       SUM(lc.quantite * lc.prix_unitaire) AS chiffre_affaires
FROM lignes_commande AS lc
JOIN produits AS p     ON p.id = lc.produit_id
JOIN categories AS cat ON cat.id = p.categorie_id
GROUP BY cat.nom
HAVING SUM(lc.quantite * lc.prix_unitaire) > 50
ORDER BY chiffre_affaires DESC;

HAVING agit comme un WHERE pour les groupes. La différence est fondamentale et très demandée en entretien : WHERE filtre des lignes individuelles avant agrégation, HAVING filtre des groupes après agrégation. On peut d ailleurs utiliser les deux dans la même requête, chacun à son étage.

Point d étape — Seules les catégories au-dessus du seuil restent. Abaissez ou relevez le seuil pour voir des catégories apparaître et disparaître, ce qui confirme l effet de HAVING.

Étape 7 — Tout combiner

Une requête analytique complète empile souvent toutes les clauses. Calculons le chiffre d affaires par client, en ne considérant que les commandes validées, et en ne gardant que les clients ayant dépensé plus de cent unités. WHERE filtre les commandes, GROUP BY regroupe par client, HAVING filtre sur le total, ORDER BY classe.

SELECT c.nom AS client,
       SUM(lc.quantite * lc.prix_unitaire) AS total_depense
FROM clients AS c
JOIN commandes AS cmd      ON cmd.client_id = c.id
JOIN lignes_commande AS lc ON lc.commande_id = cmd.id
WHERE cmd.statut = 'validee'
GROUP BY c.nom
HAVING SUM(lc.quantite * lc.prix_unitaire) > 100
ORDER BY total_depense DESC;

Cette requête résume tout le tutoriel. Le moteur l évalue dans l ordre logique : les tables sont jointes, WHERE écarte les commandes non validées, GROUP BY rassemble par client, HAVING élimine les petits clients, SELECT calcule la somme, et ORDER BY classe. Tenir cet ordre en tête, c est maîtriser l agrégation.

🐞 Pièges fréquents

Symptôme Cause probable Correctif
Erreur colonne non groupée Colonne brute hors GROUP BY L agréger ou l ajouter au GROUP BY
Filtrer un agrégat dans WHERE échoue L agrégat n existe pas au stade WHERE Utiliser HAVING
COUNT plus petit que prévu COUNT(colonne) ignore les NULL Utiliser COUNT(*) si besoin de tout compter
Moyenne faussée AVG ignore les NULL, pas les zéros Choisir le bon traitement des absences
Groupe manquant Jointure interne exclut les sans-vente Passer en LEFT JOIN si besoin

🌍 Réalités du terrain

Les requêtes d agrégation alimentent les tableaux de bord, souvent consultés en mobilité sur des connexions modestes. Deux principes gardent ces écrans rapides. D abord, faites faire le calcul au moteur : agréger côté base, qui renvoie quelques lignes de synthèse, est infiniment plus léger que de transférer des milliers de lignes brutes pour les additionner dans l application. Ensuite, lorsqu un indicateur est consulté en permanence, pensez à le précalculer périodiquement plutôt qu à chaque affichage, surtout si le volume de commandes grandit. Les colonnes servant aux regroupements et aux filtres gagnent par ailleurs à être indexées, ce qui accélère nettement les agrégations sur de grandes tables.

✅ Récapitulatif

Vous savez désormais résumer des données : appliquer les fonctions d agrégation, distinguer les formes de COUNT, regrouper avec GROUP BY en respectant sa règle d or, agréger sur des tables jointes, et filtrer les groupes avec HAVING. Vous tenez l ordre logique complet d une requête analytique. L étape suivante introduit les sous-requêtes et les CTE, qui permettent de bâtir des analyses encore plus fines.

🧾 Aide-mémoire

Élément Rôle
COUNT(*) Compter toutes les lignes
COUNT(DISTINCT col) Compter les valeurs distinctes
SUM / AVG / MIN / MAX Additionner, moyenner, extrêmes
GROUP BY col Définir les groupes
HAVING agrégat Filtrer les groupes
WHERE Filtrer les lignes avant regroupement

💪 À vous de jouer

1. Comptez le nombre de commandes par client (y compris zéro pour les inactifs).
2. Calculez le panier moyen, c est-à-dire le montant moyen d une commande.
3. Affichez les catégories ayant vendu plus de trois unités au total.

Voir une solution
-- 1
SELECT c.nom, COUNT(cmd.id) AS nb_commandes
FROM clients AS c
LEFT JOIN commandes AS cmd ON cmd.client_id = c.id
GROUP BY c.nom
ORDER BY nb_commandes DESC;

-- 2
SELECT AVG(total) AS panier_moyen FROM (
  SELECT cmd.id, SUM(lc.quantite * lc.prix_unitaire) AS total
  FROM commandes AS cmd
  JOIN lignes_commande AS lc ON lc.commande_id = cmd.id
  GROUP BY cmd.id
) AS totaux;

-- 3
SELECT cat.nom, SUM(lc.quantite) AS unites
FROM lignes_commande AS lc
JOIN produits AS p     ON p.id = lc.produit_id
JOIN categories AS cat ON cat.id = p.categorie_id
GROUP BY cat.nom
HAVING SUM(lc.quantite) > 3;

Tutoriels associés

Pour aller plus loin

FAQ

Quand utiliser WHERE plutôt que HAVING ? WHERE pour filtrer les lignes avant tout regroupement, HAVING pour filtrer les groupes sur un agrégat. On les combine souvent dans une même requête.

Les NULL faussent-ils les agrégats ? Les fonctions d agrégation ignorent les NULL, sauf COUNT(*). Une moyenne porte donc sur les seules valeurs connues, ce qui est généralement souhaitable mais doit être conscient.

Peut-on regrouper sur plusieurs colonnes ? Oui, GROUP BY accepte plusieurs colonnes ; un groupe correspond alors à chaque combinaison distincte de ces colonnes.

Pourquoi MySQL accepte une requête que PostgreSQL refuse ? MySQL tolère historiquement des colonnes non groupées et renvoie une valeur arbitraire. C est un comportement à éviter ; écrivez vos requêtes comme si elles devaient passer sur PostgreSQL.

Mots-clés : GROUP BY, HAVING, fonctions d agrégation, COUNT SUM AVG, résumé SQL, chiffre d affaires SQL.

مشاركة