Résumer des lignes : GROUP BY

L’agrégation résume plusieurs lignes en nombres, sommes ou moyennes. Vous pouvez obtenir un résultat par statut, client ou catégorie plutôt qu’un résultat par enregistrement.

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;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 7

statusorders
delivered1351
paid695
shipped641
pending349
refunded327

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;
Ouvrir dans Planche

Résultat — 1 ligne

customerswith_phonewithout_phone
15001221279

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;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 20

categoryproductscheapestaveragedearest
Wearables11417.31672.292433.73
Programming106.891585.672420.02
Tablets17314.761529.452412.93
Audio11593.81482.892334.28
Kitchen12670.541443.212293.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;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 27

categoryordersunitsrevenue
Watches395815933900.92
Board Games395801929534.55
Laptops366768900778.09
Science340709888542.78
Cycling405823858478.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;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 8

categoryrevenue
Board Games1377454.99
Tablets1348572.94
Science1347046.09
Watches1325717.58
Cycling1288143.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;
Ouvrir dans Planche

Résultat — 5 premières lignes sur 12

monthordersrevenue
2025-0152311707.73
2025-0253350775.62
2025-0358351184.96
2025-0452374465.94
2025-0553381541.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.

À vous d’essayer

  1. Combien de clients par pays ? N’affichez que les pays d’au moins 100 clients, le plus grand d’abord.

    Voir la réponse

    Voir la réponse

    SELECT country, COUNT(*) AS customers
    FROM customers
    GROUP BY country
    HAVING COUNT(*) >= 100
    ORDER BY customers DESC;
    Ouvrir dans Planche

    Résultat — 5 premières lignes sur 12

    countrycustomers
    KR155
    AU132
    IT130
    ES126
    GB126

    GROUP BY produit une ligne par pays, et la condition porte sur le compte : elle appartient donc à HAVING plutôt qu’à WHERE. L’expression y est répétée au lieu d’utiliser l’alias, ce qui fonctionne sur tous les moteurs.

  2. Les 15 produits les mieux notés, en ne comptant que ceux qui ont au moins 8 avis.

    Voir la réponse

    Voir la réponse

    SELECT p.name,
           COUNT(r.id) AS reviews,
           ROUND(AVG(r.rating), 2) AS avg_rating
    FROM products p
    JOIN reviews r ON r.product_id = p.id
    GROUP BY p.id
    HAVING COUNT(r.id) >= 8
    ORDER BY avg_rating DESC
    LIMIT 15;
    Ouvrir dans Planche

    Résultat — 5 premières lignes sur 15

    namereviewsavg_rating
    Pulse Yoga Mat84.63
    A Winter Letter (3rd ed.)94.56
    Terra Planter84.5
    Nimbus Cosmos84.5
    Ivory Toaster94.33

    Deux agrégats sur le même groupe : l’un compte les avis, l’autre fait la moyenne des notes. Le filtre HAVING est ce qui empêche un unique avis cinq étoiles de prendre la tête du classement.