MySQL GROUP BY
GROUP BY puts rows that share a value into groups, then aggregate functions such as COUNT, SUM and AVG calculate one result per group. It turns detailed data into summaries: sales per region, orders per month, students per class.
1. How GROUP BY Works
Table: sales (9 rows)
| sale_id | region | rep | product | qty | amount | sold_on |
|---|---|---|---|---|---|---|
| 1 | North | Asif | Kettle | 2 | 1200.00 | 2026-01-05 |
| 2 | South | Bela | Toaster | 1 | 800.00 | 2026-01-07 |
| 3 | North | Asif | Mug | 3 | 450.00 | 2026-01-19 |
| 4 | West | Chris | Kettle | 1 | 990.00 | 2026-02-02 |
| 5 | South | Bela | Kettle | 2 | 1500.00 | 2026-02-11 |
| 6 | North | Divya | Mug | 2 | 300.00 | 2026-02-14 |
| 7 | West | Chris | Toaster | 1 | 610.00 | 2026-03-03 |
| 8 | South | Elan | Mug | 4 | 520.00 | 2026-03-09 |
| 9 | North | Divya | Kettle | 1 | 640.00 | 2026-03-21 |
2. Syntax
SELECT group_columns, AGG(column) ...
FROM table
[WHERE row_condition]
GROUP BY group_columns
[HAVING group_condition]
[ORDER BY ...];
| Aggregate | Per group it returns |
|---|---|
COUNT(*) | number of rows |
COUNT(col) / COUNT(DISTINCT col) | non-NULL / distinct values |
SUM(col) | total |
AVG(col) | average |
MIN(col), MAX(col) | smallest / largest |
GROUP_CONCAT(col) | values joined into one string |
3. Grouping by One Column
SELECT region, COUNT(*) AS sales, SUM(amount) AS revenue, ROUND(AVG(amount)) AS avg_sale
FROM sales
GROUP BY region
ORDER BY revenue DESC;
Result: 3 rows
| region | sales | revenue | avg_sale |
|---|---|---|---|
| South | 3 | 2820.00 | 940 |
| North | 4 | 2590.00 | 648 |
| West | 2 | 1600.00 | 800 |
4. Grouping by Several Columns
Each unique combination becomes a group:
SELECT region, product, SUM(qty) AS units, SUM(amount) AS revenue
FROM sales
GROUP BY region, product
ORDER BY region, revenue DESC;
Result: 7 rows
| region | product | units | revenue |
|---|---|---|---|
| North | Kettle | 3 | 1840.00 |
| North | Mug | 5 | 750.00 |
| South | Kettle | 2 | 1500.00 |
| South | Toaster | 1 | 800.00 |
| South | Mug | 4 | 520.00 |
| West | Kettle | 1 | 990.00 |
| West | Toaster | 1 | 610.00 |
5. Filtering Rows First with WHERE
WHERE removes rows before grouping. Revenue per rep, counting only kettles:
SELECT rep, COUNT(*) AS kettles_sold, SUM(amount) AS kettle_revenue
FROM sales
WHERE product = 'Kettle'
GROUP BY rep;
Result: 4 rows
| rep | kettles_sold | kettle_revenue |
|---|---|---|
| Asif | 1 | 1200.00 |
| Chris | 1 | 990.00 |
| Bela | 1 | 1500.00 |
| Divya | 1 | 640.00 |
To filter groups after aggregation, use HAVING.
6. Grouping by Expressions and Dates
Group by a calculated value such as the month:
SELECT DATE_FORMAT(sold_on, '%Y-%m') AS month, COUNT(*) AS sales, SUM(amount) AS revenue
FROM sales
GROUP BY month
ORDER BY month;
Result: 3 rows
| month | sales | revenue |
|---|---|---|
| 2026-01 | 3 | 2450.00 |
| 2026-02 | 3 | 2790.00 |
| 2026-03 | 3 | 1770.00 |
Group into price bands with CASE:
SELECT CASE WHEN amount < 500 THEN 'small' WHEN amount < 1000 THEN 'medium' ELSE 'large' END AS size_band,
COUNT(*) AS sales
FROM sales
GROUP BY size_band
ORDER BY MIN(amount);
Result: 3 rows
| size_band | sales |
|---|---|
| small | 2 |
| medium | 5 |
| large | 2 |
7. The ONLY_FULL_GROUP_BY Rule
Every column in the SELECT list must either be in GROUP BY or be inside an aggregate. Otherwise MySQL can't know which row's value to show, and (with the default ONLY_FULL_GROUP_BY mode) it refuses:
SELECT region, rep, SUM(amount) FROM sales GROUP BY region;
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'retail.sales.rep' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
Fix it by grouping by rep as well, aggregating it, or using ANY_VALUE() when any value will do:
SELECT region, GROUP_CONCAT(DISTINCT rep ORDER BY rep) AS reps, SUM(amount) AS revenue
FROM sales
GROUP BY region;
Result: 3 rows
| region | reps | revenue |
|---|---|---|
| North | Asif,Divya | 2590.00 |
| South | Bela,Elan | 2820.00 |
| West | Chris | 1600.00 |
8. Subtotals with WITH ROLLUP
WITH ROLLUP adds subtotal rows and a grand total (shown with NULL in the grouped columns). GROUPING() tells you which rows are totals:
SELECT IF(GROUPING(region), 'ALL REGIONS', region) AS region,
IF(GROUPING(product), 'all products', product) AS product,
SUM(amount) AS revenue
FROM sales
GROUP BY region, product WITH ROLLUP;
Result: 11 rows
| region | product | revenue |
|---|---|---|
| North | Kettle | 1840.00 |
| North | Mug | 750.00 |
| North | all products | 2590.00 |
| South | Kettle | 1500.00 |
| South | Mug | 520.00 |
| South | Toaster | 800.00 |
| South | all products | 2820.00 |
| West | Kettle | 990.00 |
| West | Toaster | 610.00 |
| West | all products | 1600.00 |
| ALL REGIONS | all products | 7010.00 |
9. COUNT(DISTINCT) per Group
SELECT region, COUNT(*) AS sales, COUNT(DISTINCT product) AS products, COUNT(DISTINCT rep) AS reps
FROM sales
GROUP BY region;
Result: 3 rows
| region | sales | products | reps |
|---|---|---|---|
| North | 4 | 2 | 2 |
| South | 3 | 3 | 2 |
| West | 2 | 2 | 1 |
10. Sorting Groups
MySQL 8 doesn't sort GROUP BY results implicitly. Add ORDER BY, which may use aliases and aggregates:
SELECT rep, SUM(amount) AS revenue FROM sales GROUP BY rep ORDER BY revenue DESC LIMIT 3;
Result: 3 rows
| rep | revenue |
|---|---|
| Bela | 2300.00 |
| Asif | 1650.00 |
| Chris | 1600.00 |
11. Practice Exercises
Exercise 1: Units sold per product, most popular first.
SELECT product, SUM(qty) AS units FROM sales GROUP BY product ORDER BY units DESC;
Result: 3 rows
| product | units |
|---|---|
| Mug | 9 |
| Kettle | 6 |
| Toaster | 2 |
Exercise 2: For each rep, their first and latest sale dates.
SELECT rep, MIN(sold_on) AS first_sale, MAX(sold_on) AS latest_sale FROM sales GROUP BY rep ORDER BY rep;
Result: 5 rows
| rep | first_sale | latest_sale |
|---|---|---|
| Asif | 2026-01-05 | 2026-01-19 |
| Bela | 2026-01-07 | 2026-02-11 |
| Chris | 2026-02-02 | 2026-03-03 |
| Divya | 2026-02-14 | 2026-03-21 |
| Elan | 2026-03-09 | 2026-03-09 |
Exercise 3: Monthly revenue for the South region only.
SELECT MONTH(sold_on) AS m, SUM(amount) AS revenue FROM sales WHERE region = 'South' GROUP BY m ORDER BY m;
Result: 3 rows
| m | revenue |
|---|---|
| 1 | 800.00 |
| 2 | 1500.00 |
| 3 | 520.00 |
12. Summary
- GROUP BY makes one result row per distinct value (or combination of values).
- Aggregates (COUNT, SUM, AVG, MIN, MAX, GROUP_CONCAT) summarise each group.
- WHERE filters rows before grouping; HAVING filters groups after.
- Selected columns must be grouped or aggregated (ONLY_FULL_GROUP_BY).
- WITH ROLLUP adds subtotals; add ORDER BY to sort the groups.