Lesson 4/7 8 min

Aggregating and grouping

Compute totals and averages with COUNT, SUM, AVG, MIN, MAX, group with GROUP BY and filter with HAVING.

FR EN

Le problème : « combien on a vendu ce mois-ci ? »

Jusqu'ici, vous savez sortir des lignes. Mais le patron ne veut pas voir 4 000 commandes une par une. Il veut un chiffre : le total des ventes, le panier moyen, le nombre de commandes par mois.

Passer de « lister les lignes » à « calculer un résumé », c'est le rôle des fonctions d'agrégation. On travaille sur une table commandes :

id | client_id | mois     | montant
---+-----------+----------+--------
1  | 1         | 2026-01  | 89.90
2  | 2         | 2026-01  | 42.00
3  | 1         | 2026-02  | 120.50
4  | 3         | 2026-02  | 19.90

COUNT, SUM, AVG, MIN, MAX

Cinq fonctions résument toute une colonne en une seule valeur :

COUNT() Nombre de lignes SUM() Somme des valeurs AVG() Moyenne MIN() Valeur la plus petite MAX() Valeur la plus grande
SELECT COUNT(*)     AS nb_commandes,
       SUM(montant) AS total,
       AVG(montant) AS panier_moyen
FROM commandes;

Le mot-clé AS renomme la colonne de résultat (un « alias ») pour la rendre lisible. Sans agrégation ni GROUP BY, ces fonctions renvoient une seule ligne pour toute la table.

GROUP BY : un résumé par groupe

Un total global, c'est bien. Un total par mois, c'est mieux. GROUP BY découpe les lignes en paquets selon une colonne, et applique l'agrégation à chaque paquet.

GROUP BY rassemble les lignes qui partagent la même valeur de mois, puis calcule une somme par groupe : 4 lignes deviennent 2 résultats. commandes mois montant 2026-0150 2026-0130 2026-0280 2026-0220 GROUP BY mois mois SUM(montant) 2026-0180 2026-02100
GROUP BY regroupe les lignes par valeur, puis calcule un agrégat (ici SUM) pour chaque groupe : 4 lignes → 2 résultats.
SELECT mois,
       COUNT(*)     AS nb_commandes,
       SUM(montant) AS total_ventes
FROM commandes
GROUP BY mois
ORDER BY mois;

Résultat : une ligne par mois, avec son nombre de commandes et son chiffre d'affaires.

Règle d'or : toute colonne du SELECT qui n'est pas dans une fonction d'agrégation doit apparaître dans le GROUP BY. Sinon, le moteur ne sait pas quelle valeur afficher pour le groupe.

HAVING : filtrer sur un agrégat

Vous voulez seulement les mois où le total dépasse 100 €. Tentation : WHERE SUM(montant) > 100. Ça échoue. WHERE filtre les lignes avant le regroupement, quand les totaux n'existent pas encore.

Pour filtrer après l'agrégation, on utilise HAVING :

SELECT mois, SUM(montant) AS total_ventes
FROM commandes
GROUP BY mois
HAVING SUM(montant) > 100
ORDER BY total_ventes DESC;

Ne confondez pas WHERE et HAVING. WHERE filtre les lignes brutes avant le GROUP BY (et ne peut pas utiliser COUNT/SUM/AVG). HAVING filtre les groupes après l'agrégation. Mélanger les deux est l'erreur de débutant la plus fréquente.

On peut combiner les deux : WHERE pour pré-filtrer les lignes, HAVING pour filtrer les groupes obtenus.

The problem: "how much did we sell this month?"

So far, you can pull rows out. But the boss does not want to see 4,000 orders one by one. He wants a number: total sales, average basket, number of orders per month.

Going from "list the rows" to "compute a summary" is the job of aggregate functions. We work on a commandes table:

id | client_id | mois     | montant
---+-----------+----------+--------
1  | 1         | 2026-01  | 89.90
2  | 2         | 2026-01  | 42.00
3  | 1         | 2026-02  | 120.50
4  | 3         | 2026-02  | 19.90

COUNT, SUM, AVG, MIN, MAX

Five functions summarize a whole column into a single value:

COUNT() Number of rows SUM() Sum of values AVG() Average MIN() Smallest value MAX() Largest value
SELECT COUNT(*)     AS nb_commandes,
       SUM(montant) AS total,
       AVG(montant) AS panier_moyen
FROM commandes;

The AS keyword renames the result column (an "alias") to make it readable. Without grouping, these functions return a single row for the whole table.

GROUP BY: one summary per group

A global total is fine. A total per month is better. GROUP BY splits rows into buckets by a column, and applies the aggregation to each bucket.

GROUP BY collects rows that share the same month value, then computes a sum per group: 4 rows become 2 results. orders month amount 2026-0150 2026-0130 2026-0280 2026-0220 GROUP BY month month SUM(amount) 2026-0180 2026-02100
GROUP BY groups rows by value, then computes an aggregate (here SUM) for each group: 4 rows → 2 results.
SELECT mois,
       COUNT(*)     AS nb_commandes,
       SUM(montant) AS total_ventes
FROM commandes
GROUP BY mois
ORDER BY mois;

Result: one row per month, with its number of orders and revenue.

Golden rule: every column in the SELECT that is not inside an aggregate function must appear in the GROUP BY. Otherwise, the engine does not know which value to show for the group.

HAVING: filter on an aggregate

You only want months where the total exceeds 100 EUR. Temptation: WHERE SUM(montant) > 100. It fails. WHERE filters rows before grouping, when the totals do not exist yet.

To filter after aggregation, use HAVING:

SELECT mois, SUM(montant) AS total_ventes
FROM commandes
GROUP BY mois
HAVING SUM(montant) > 100
ORDER BY total_ventes DESC;

Do not confuse WHERE and HAVING. WHERE filters raw rows before the GROUP BY (and cannot use COUNT/SUM/AVG). HAVING filters groups after aggregation. Mixing them up is the most common beginner mistake.

You can combine both: WHERE to pre-filter rows, HAVING to filter the resulting groups.

À vous d'essayer — la base est déjà remplie :

CREATE TABLE commandes (id INTEGER, ville TEXT, montant REAL);
INSERT INTO commandes VALUES (1, 'Dijon', 120.50);
INSERT INTO commandes VALUES (2, 'Lyon', 45.00);
INSERT INTO commandes VALUES (3, 'Dijon', 230.90);
INSERT INTO commandes VALUES (4, 'Lyon', 89.00);

SELECT ville, COUNT(*) AS nb, SUM(montant) AS total
FROM commandes
GROUP BY ville
HAVING SUM(montant) > 100
ORDER BY total DESC;
Avec l'IA

Demandez une requête d'agrégation à l'IA et vérifiez qu'elle place bien le filtre dans HAVING et non dans WHERE :

Table commandes (id, client_id, mois, montant). Écris une requête SQL qui calcule le total des ventes par mois, et ne garde que les mois dont le total dépasse 100. Trie du total le plus élevé au plus bas. Important : explique pourquoi le filtre sur le total doit aller dans HAVING et pas dans WHERE.
Ré-explique sans regarder

Sans relire la réponse de l'IA : avec tes mots, pourquoi un filtre sur SUM(montant) doit aller dans HAVING et pas dans WHERE ?

Une bonne explication dit : WHERE s'exécute avant le GROUP BY, donc le total SUM(montant) n'existe pas encore et ne peut pas être filtré. HAVING s'exécute après le regroupement, quand chaque groupe a son total calculé. Bonus : on combine souvent les deux (WHERE pour pré-filtrer les lignes brutes, HAVING pour filtrer les groupes obtenus).
Exercice : Ventes par mois

Écrivez une requête qui calcule, par mois, le nombre de commandes (COUNT) et le total des montants (SUM), regroupé par mois (GROUP BY), en ne gardant que les mois dont le total dépasse 50 (HAVING).

Accepter ou rejeter le code de l'IA

Tu as demandé à l'IA : « le total des ventes par mois, seulement les mois au-dessus de 100 ». Elle répond ce code. Ton rôle de relecteur : l'accepter ou le rejeter, et dire pourquoi.

SELECT mois, SUM(montant) AS total
FROM commandes
WHERE SUM(montant) > 100
GROUP BY mois;
À rejeter : c'est le piège classique. Le filtre sur SUM(montant) est dans WHERE, qui s'exécute avant le GROUP BY : le total n'existe pas encore, le moteur renvoie une erreur (impossible d'utiliser une fonction d'agrégation dans WHERE). La correction : déplacer la condition dans HAVING SUM(montant) > 100, placé après le GROUP BY.
Rappel libre

Sans remonter dans la leçon : à quoi sert GROUP BY, et quelle clause filtre les groupes une fois les totaux calculés ?

GROUP BY découpe les lignes en groupes selon une colonne (ex. mois) et applique les fonctions d'agrégation (COUNT, SUM, AVG…) à chaque groupe : on obtient une ligne de résumé par valeur. Pour filtrer ces groupes selon leur agrégat, c'est HAVING (pas WHERE, qui agit avant le regroupement).
Quelle fonction compte le nombre de lignes ?
Pour filtrer sur un total (SUM), faut-il utiliser WHERE ou HAVING ?
Que fait GROUP BY mois ?
Next step

Your calculations stop at the edge of a single table. The next lesson breaks that wall: joins link several tables through foreign keys, with INNER JOIN, LEFT JOIN and the ON clause.

Lesson 5: Joins →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement