GROUP BY, HAVING et les fonctions d'agrégation en SQL
Combien de commandes par client ? Quel chiffre d'affaires par catégorie de produits ? Ces questions de gestion exigent d'agréger des lignes : c'est le rôle des fonctions d'agrégation et de la clause GROUP BY. Et la question qui revient sans cesse à l'épreuve de l'UE 8 du DCG est la différence entre WHERE et HAVING en SQL avec GROUP BY : une confusion classique qui coûte des points faciles à éviter.
Les fonctions d'agrégation : transformer des lignes en indicateurs
Une fonction d'agrégation calcule une valeur unique à partir d'un ensemble de lignes. Cinq fonctions sont au programme :
- COUNT(*) : le nombre de lignes, par exemple
SELECT COUNT(*) FROM CLIENT pour compter les clients.
- COUNT(attribut) : le nombre de valeurs non NULL de la colonne (les cases vides ne sont pas comptées).
- SUM(attribut) : la somme, par exemple
SUM(PrixHT * Quantite) pour un montant total.
- AVG(attribut) : la moyenne, par exemple
AVG(PrixHT) pour le prix moyen.
- MIN(attribut) et MAX(attribut) : le minimum et le maximum.
Variante précieuse : COUNT(DISTINCT colonne) compte les valeurs distinctes. Pour savoir combien de clients différents ont commandé, COUNT(DISTINCT NumClient) sur la table COMMANDE ne compte chaque client qu'une fois, même s'il a passé dix commandes ; COUNT(NumClient) compterait dix lignes.
GROUP BY : calculer par catégorie
Sans GROUP BY, l'agrégation porte sur toute la table et renvoie une seule ligne. Avec GROUP BY, les lignes ayant la même valeur sont regroupées, et l'agrégat est calculé pour chaque groupe :
SELECT Ville, COUNT(*) AS NbClients
FROM CLIENT
GROUP BY Ville ;
Résultat : une ligne par ville, avec le nombre de clients de chacune. La règle d'or à mémoriser : dans un SELECT avec GROUP BY, chaque colonne affichée doit être soit dans le GROUP BY, soit à l'intérieur d'une fonction d'agrégation. Afficher le nom du client sans le grouper ni l'agréger provoque une erreur.
La différence entre WHERE et HAVING : le point qui départage les copies
C'est la confusion la plus fréquente, et elle se résout avec l'ordre d'exécution logique d'une requête : FROM, puis WHERE, puis GROUP BY, puis HAVING, puis SELECT, puis ORDER BY.
- WHERE filtre les lignes avant le regroupement. Il ne peut donc pas contenir de fonction d'agrégation : au moment où il s'exécute, aucun COUNT ni SUM n'a encore été calculé.
- HAVING filtre les groupes après le GROUP BY. Il porte généralement sur un agrégat :
HAVING SUM(Montant) > 1000.
Repère pratique : « les commandes de 2026 » est une condition sur des lignes individuelles, donc WHERE. « Les clients dont le total commandé dépasse 1 000 euros » est une condition sur un résultat agrégé, donc HAVING. Les deux clauses peuvent coexister dans la même requête : le WHERE réduit d'abord les lignes, le GROUP BY regroupe ce qui reste, le HAVING trie les groupes.
SELECT Ville, COUNT(*) AS NbClients
FROM CLIENT
WHERE CodePostal LIKE '69%'
GROUP BY Ville
HAVING COUNT(*) > 5 ;
Un exemple chiffré : la société Fournitex
Prenons le cas de la société Fournitex, qui vend des fournitures de bureau. Sa base contient CLIENT, COMMANDE, LIGNECOMMANDE(NumCommande, CodeProduit, Quantite) et PRODUIT(CodeProduit, Libelle, PrixHT). La direction commerciale veut identifier les clients ayant dépensé plus de 1 000 euros au total, pour leur proposer un programme de fidélité.
SELECT C.Nom, SUM(P.PrixHT * LC.Quantite) AS Total
FROM CLIENT C
INNER JOIN COMMANDE CO ON C.NumClient = CO.NumClient
INNER JOIN LIGNECOMMANDE LC ON CO.NumCommande = LC.NumCommande
INNER JOIN PRODUIT P ON LC.CodeProduit = P.CodeProduit
GROUP BY C.NumClient, C.Nom
HAVING SUM(P.PrixHT * LC.Quantite) > 1000 ;
Lisons la mécanique : les trois jointures reconstituent le détail de chaque achat (client, commande, ligne, prix) ; le GROUP BY crée un groupe par client ; SUM(PrixHT × Quantite) calcule le montant total de chaque groupe ; le HAVING ne conserve que les groupes dépassant 1 000 euros. Si un client a acheté 300 ramettes à 4,50 euros (1 350 euros), il est retenu ; un client à 800 euros est écarté. Filtrer ce montant avec WHERE serait impossible : la somme n'existe pas encore au moment où WHERE s'exécute.
Les erreurs fréquentes
- Mettre une fonction d'agrégation dans le WHERE :
WHERE COUNT(*) > 5 est invalide ; les agrégats se filtrent avec HAVING.
- Afficher une colonne hors GROUP BY et hors agrégation : chaque colonne du SELECT doit être groupée ou agrégée, sinon erreur.
- Confondre COUNT(*) et COUNT(colonne) : le premier compte toutes les lignes, le second ignore les NULL. Sur une colonne facultative, les résultats diffèrent.
- Oublier COUNT(DISTINCT) quand le besoin parle de « clients différents » : compter les lignes de COMMANDE donne le nombre de commandes, pas le nombre de clients.
- Utiliser un INNER JOIN quand le comptage doit inclure « ceux qui n'ont rien » : pour afficher 0 commande pour les clients inactifs, il faut un LEFT JOIN et un COUNT sur la colonne de droite.
- Croire que HAVING remplace WHERE : utiliser HAVING pour une condition de ligne simple fonctionne parfois, mais c'est moins performant et conceptuellement faux ; chaque clause a son rôle.
FAQ
Peut-on utiliser WHERE et HAVING dans la même requête ?
Oui, et c'est même fréquent : le WHERE filtre d'abord les lignes (par exemple les commandes de l'année 2026), puis le GROUP BY regroupe, puis le HAVING filtre les groupes (par exemple les clients dont le total annuel dépasse un seuil). Les deux clauses sont complémentaires, pas concurrentes.
Pourquoi ne peut-on pas écrire WHERE COUNT(*) > 5 ?
À cause de l'ordre d'exécution logique : le WHERE s'exécute avant le GROUP BY, donc avant tout calcul d'agrégat. Au moment où le WHERE filtre les lignes, COUNT(*) n'a aucune valeur. Seul le HAVING, exécuté après le regroupement, peut tester un agrégat.
GROUP BY est-il obligatoire avec une fonction d'agrégation ?
Non. Sans GROUP BY, l'agrégat porte sur l'ensemble des lignes retenues et le résultat tient en une seule ligne : SELECT AVG(PrixHT) FROM PRODUIT renvoie le prix moyen global. Le GROUP BY ne devient nécessaire que pour décliner le calcul par catégorie.
Entraînez-vous
Sur la base PRODUIT(CodeProduit, Libelle, PrixHT, CodeCategorie) et CATEGORIE(CodeCategorie, NomCategorie) : 1) comptez le nombre de produits par catégorie (code seul) ; 2) calculez le prix moyen de l'ensemble des produits ; 3) affichez le nom des catégories comptant plus de 5 produits, avec ce nombre.
Afficher le corrigé
Question 1 : un regroupement simple sur le code catégorie.
SELECT CodeCategorie, COUNT(*) AS NbProduits
FROM PRODUIT
GROUP BY CodeCategorie ;
Un groupe par catégorie ; COUNT(*) compte les produits de chaque groupe.
Question 2 : une agrégation sans GROUP BY, le résultat tient en une ligne.
SELECT AVG(PrixHT) AS PrixMoyen
FROM PRODUIT ;
Question 3 : jointure pour obtenir le nom, regroupement, puis filtre de groupes avec HAVING.
SELECT CAT.NomCategorie, COUNT(*) AS NbProduits
FROM PRODUIT P
INNER JOIN CATEGORIE CAT ON P.CodeCategorie = CAT.CodeCategorie
GROUP BY CAT.NomCategorie
HAVING COUNT(*) > 5 ;
La jointure permet d'afficher le nom de la catégorie plutôt que son code ; le GROUP BY crée un groupe par catégorie ; le HAVING, exécuté après le regroupement, ne conserve que les catégories de plus de 5 produits. Écrire cette condition dans un WHERE serait une erreur de syntaxe : c'est exactement la distinction WHERE / HAVING que l'examinateur cherche à vérifier.