Un nombre par groupe
GROUP BY rassemble les lignes qui partagent une valeur, et les fonctions d’agrégation réduisent chaque tas à un seul nombre.
1
SELECT status, COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC;
Résultat — 5 premières lignes sur 7
| status | orders |
|---|---|
| delivered | 1351 |
| paid | 695 |
| shipped | 641 |
| pending | 349 |
| refunded | 327 |
Planche s’ouvre dans un onglet séparé, la base d’exemple déjà chargée et cette requête prête dans l’éditeur. Tous les exemples de la page réutilisent cet onglet, et rien ne s’exécute tant que vous n’appuyez pas sur F9.
Sept statuts, sept lignes. Chaque colonne de la liste SELECT est soit quelque chose sur quoi vous avez regroupé, soit quelque chose que vous avez agrégé — c’est la règle qui rend le résultat bien défini. Demandez order_number ici et la question perd son sens : lequel des 1 351 numéros de commandes livrées faudrait-il choisir ?
COUNT(*) n’est pas COUNT(colonne)
2
SELECT COUNT(*) AS customers,
COUNT(phone) AS with_phone,
COUNT(*) - COUNT(phone) AS without_phone
FROM customers;
Résultat — 1 ligne
| customers | with_phone | without_phone |
|---|---|---|
| 1500 | 1221 | 279 |
COUNT(*) compte les lignes. COUNT(phone) compte les lignes où phone n’est pas nul. L’écart, ce sont les 279 clients de la leçon sur le filtrage, et c’est une astuce réellement utile : soustraire les deux comptes donne ce qui manque sans seconde requête.
Tous les autres agrégats ignorent aussi les NULL, ce qui compte surtout pour AVG. La moyenne de 10, 20 et NULL vaut 15, pas 10 — le NULL n’est pas traité comme un zéro, il n’est pas compté du tout. Quand vous voulez qu’il compte pour zéro, dites-le avec COALESCE(colonne, 0).
Plusieurs agrégats à la fois
3
SELECT cat.name AS category,
COUNT(*) AS products,
ROUND(MIN(p.price), 2) AS cheapest,
ROUND(AVG(p.price), 2) AS average,
ROUND(MAX(p.price), 2) AS dearest
FROM products p
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
ORDER BY average DESC
LIMIT 20;
Résultat — 5 premières lignes sur 20
| category | products | cheapest | average | dearest |
|---|---|---|---|---|
| Wearables | 11 | 417.3 | 1672.29 | 2433.73 |
| Programming | 10 | 6.89 | 1585.67 | 2420.02 |
| Tablets | 17 | 314.76 | 1529.45 | 2412.93 |
| Audio | 11 | 593.8 | 1482.89 | 2334.28 |
| Kitchen | 12 | 670.54 | 1443.21 | 2293.8 |
Un seul passage sur la table répond à cinq questions par catégorie. Faire la même chose côté application, c’est transférer les 400 lignes de produits et boucler dessus ; la base calcule les cinq nombres là où les lignes se trouvent déjà et en renvoie 20.
Regrouper à travers une jointure
Agrégats et jointures se combinent. On joint d’abord, puis on regroupe les lignes jointes.
4
SELECT cat.name AS category,
COUNT(DISTINCT o.id) AS orders,
SUM(oi.quantity) AS units,
ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
WHERE o.status IN ('paid', 'shipped', 'delivered')
GROUP BY cat.id
ORDER BY revenue DESC;
Résultat — 5 premières lignes sur 27
| category | orders | units | revenue |
|---|---|---|---|
| Watches | 395 | 815 | 933900.92 |
| Board Games | 395 | 801 | 929534.55 |
| Laptops | 366 | 768 | 900778.09 |
| Science | 340 | 709 | 888542.78 |
| Cycling | 405 | 823 | 858478.88 |
WHERE avant, HAVING après
Les deux filtrent. Ils filtrent à des moments différents, et c’est là leur seule différence.
5
SELECT cat.name AS category,
ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
HAVING SUM(oi.quantity * oi.unit_price) > 1000000
ORDER BY revenue DESC;
Résultat — 5 premières lignes sur 8
| category | revenue |
|---|---|
| Board Games | 1377454.99 |
| Tablets | 1348572.94 |
| Science | 1347046.09 |
| Watches | 1325717.58 |
| Cycling | 1288143.46 |
WHERE s’exécute avant le regroupement et décide quelles lignes y participent. HAVING s’exécute après et décide quels groupes survivent. Une condition sur une colonne brute va donc dans WHERE, et une condition sur un agrégat ne peut aller que dans HAVING — l’agrégat n’existe pas encore pendant l’exécution de WHERE.
Placez à côté la séquence des clauses vue dans la leçon sur SELECT, et la règle s’explique d’elle-même :
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT
Regrouper sur une valeur calculée
On peut regrouper sur une expression, pas seulement sur une colonne. Couper les sept premiers caractères d’un horodatage donne 2025-01 — un mois.
6
SELECT substr(ordered_at, 1, 7) AS month,
COUNT(*) AS orders,
ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
AND ordered_at >= '2025-01-01'
GROUP BY month
ORDER BY month;
Résultat — 5 premières lignes sur 12
| month | orders | revenue |
|---|---|---|
| 2025-01 | 52 | 311707.73 |
| 2025-02 | 53 | 350775.62 |
| 2025-03 | 58 | 351184.96 |
| 2025-04 | 52 | 374465.94 |
| 2025-05 | 53 | 381541.09 |
Regrouper sur une valeur dérivée ainsi, c’est la façon dont s’écrit presque tout rapport « par mois » ou « par semaine ». Notez que GROUP BY month réutilise l’alias du SELECT, ce que tous les moteurs autorisent dans GROUP BY et ORDER BY — seuls WHERE et HAVING sont stricts.
Ce que vous savez faire maintenant
Vous savez produire un rapport. Ce que vous ne savez pas encore faire, c’est comparer une ligne à un résumé — « les produits au-dessus du prix moyen », « les trois premiers de chaque catégorie » — car cela demande deux requêtes en même temps. C’est le sujet de la prochaine leçon.