SQL GROUP BY: compute per group
Aggregate functions summarize a whole table into a single value. But you often want a result per category: the number of customers per city, the revenue per month, the average salary per department. That is the job of GROUP BY.
Syntax
SELECT group_column, FUNCTION(column)
FROM table
GROUP BY group_column;How it works
GROUP BY gathers the rows that have the same value in the given column, then applies the aggregate function to each group. The result contains one row per group.
The golden rule
Every column in the SELECT must either appear in the GROUP BY or be inside an aggregate function. Otherwise, SQL would not know which value to show for a group that contains several different rows.
The execution order of a query
SQL does not run the clauses in the order you write them:
FROM(and theJOINs): start from the tables;WHERE: filter the rows;GROUP BY: build the groups;HAVING: filter the groups;SELECT: compute the columns;ORDER BY, thenLIMIT.
Understanding this order explains most errors: for example, you cannot use a COUNT in the WHERE, because the groups do not exist yet at that point.
Diagram
| prenom | ville |
|---|---|
| Léa | Lyon |
| Inès | Lyon |
| Hugo | Paris |
| Adam | Lyon |
| Zoé | Paris |
| ville | COUNT(*) |
|---|---|
| Lyon | 3 |
| Paris | 2 |
Simplified diagram to show the idea. The examples below use the real SQL Quest practice database (a fictional online shop: see the full schema).
Glossary: the sample database uses French names
clients- customers
commandes- orders
lignes_commande- order lines
produits- products
fournisseurs- suppliers
employes- employees
prenom / nom- first name / last name (or name)
ville- city
pays- country
prix- price
prix_unitaire- unit price
quantite- quantity
categorie- category
date_commande- order date
poste- job title
departement- department
salaire- salary
statut- status: livrée = delivered, expédiée = shipped, en cours = in progress, annulée = cancelled
Examples with their results
1. Count per group
The number of customers per city, from the largest to the smallest.
SELECT ville, COUNT(*) AS nb_clients
FROM clients
GROUP BY ville
ORDER BY nb_clients DESC; Try it in the sandbox| ville | nb_clients |
|---|---|
| Bordeaux | 14 |
| Paris | 13 |
| Lyon | 12 |
| Strasbourg | 11 |
| Nantes | 10 |
| Montréal | 10 |
| Marseille | 10 |
| Lille | 10 |
| Casablanca | 10 |
| Bruxelles | 9 |
12 rows in total · 10 shown
2. Several aggregates per group
For each category: the number of products and the average price.
SELECT categorie, COUNT(*) AS nb_produits, ROUND(AVG(prix), 2) AS prix_moyen
FROM produits
GROUP BY categorie; Try it in the sandbox| categorie | nb_produits | prix_moyen |
|---|---|---|
| Alimentation | 5 | 24.74 |
| Informatique | 7 | 146.43 |
| Livres | 5 | 18.17 |
| Maison | 6 | 36.45 |
| Sport | 6 | 27.81 |
5 rows in total
3. Group on an expression
The number of orders per year, using strftime to extract the year from a date.
SELECT strftime('%Y', date_commande) AS annee, COUNT(*) AS nb_commandes
FROM commandes
GROUP BY annee; Try it in the sandbox| annee | nb_commandes |
|---|---|
| 2023 | 190 |
| 2024 | 214 |
| 2025 | 196 |
3 rows in total
4. Group on two columns
The number of orders per year and per status.
SELECT strftime('%Y', date_commande) AS annee, statut, COUNT(*) AS nb
FROM commandes
GROUP BY annee, statut
ORDER BY annee, nb DESC
LIMIT 8; Try it in the sandbox| annee | statut | nb |
|---|---|---|
| 2023 | livrée | 85 |
| 2023 | annulée | 39 |
| 2023 | en cours | 34 |
| 2023 | expédiée | 32 |
| 2024 | livrée | 117 |
| 2024 | expédiée | 35 |
| 2024 | en cours | 34 |
| 2024 | annulée | 28 |
8 rows in total
Common mistakes
Showing a column that is not grouped
nom is neither in the GROUP BY nor in an aggregate: SQLite shows a random name from the group, other databases return an error.
SELECT ville, nom, COUNT(*) FROM clients GROUP BY ville;SELECT ville, COUNT(*) FROM clients GROUP BY ville;Forgetting the GROUP BY
Without GROUP BY, the query returns a single row for the whole table instead of one row per city.
SELECT ville, COUNT(*) FROM clients;SELECT ville, COUNT(*) FROM clients GROUP BY ville;Grouping on a column that is too precise
Grouping by id creates one group per row: the grouping is useless.
SELECT id, ville, COUNT(*) FROM clients GROUP BY id;SELECT ville, COUNT(*) FROM clients GROUP BY ville;Exercises
Try writing the query yourself before opening the solution.
Challenge 1. Show the average salary per department, rounded to the euro.
Show a solution
SELECT departement, ROUND(AVG(salaire)) AS salaire_moyen FROM employes GROUP BY departement;| departement | salaire_moyen |
|---|---|
| Direction | 9,500 |
| IT | 4,760 |
| RH | 4,233 |
| Ventes | 4,554 |
4 rows in total
Try it in the sandboxChallenge 2. Show the number of products per supplier (fournisseur_id, count).
Show a solution
SELECT fournisseur_id, COUNT(*) AS nb FROM produits GROUP BY fournisseur_id;| fournisseur_id | nb |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 4 |
| 4 | 13 |
| 5 | 1 |
| 6 | 5 |
| 7 | 3 |
7 rows in total
Try it in the sandboxThe interactive exercises are checked automatically, with a tutor who explains your mistakes. A short lesson stays next to the editor.
Frequently asked questions
What is the difference between GROUP BY and ORDER BY?
GROUP BY gathers rows to compute one value per group (the number of rows goes down). ORDER BY only sorts the rows, without changing how many there are.
Can I use an alias in GROUP BY?
SQLite, MySQL and PostgreSQL accept it (GROUP BY annee). Standard SQL and SQL Server require you to repeat the expression.
How do I filter groups?
With HAVING, placed after GROUP BY: HAVING COUNT(*) > 10 only keeps the groups with more than 10 rows.