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 :
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 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:
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 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;
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.
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 ?
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).É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).
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;
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.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).Vos calculs s'arrêtent au bord d'une seule table. La prochaine leçon fait tomber ce mur : les jointures relient plusieurs tables via les clés étrangères, avec INNER JOIN, LEFT JOIN et la clause ON.
Leçon 5 : Les jointures →