00:00

MySQL GROUP BY

Last updated: 5 min read

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

GROUP BY collapses rows into one row per groupSeven sales rows for three regions are sorted into groups North, South and West. Each group becomes one result row with the count and total of its sales.salesregionamountNorth1200South800North450West990South1500North300West610North1200 + 450 + 300South800 + 1500West990 + 610SELECT region, COUNT(*), SUM(amount) FROM sales GROUP BY region;Result: one row per regionregionCOUNTSUMNorth31950South22300West21600
Figure 1: rows with the same region form a group; aggregate functions summarise each group into one row.

Table: sales (9 rows)

sale_idregionrepproductqtyamountsold_on
1NorthAsifKettle21200.002026-01-05
2SouthBelaToaster1800.002026-01-07
3NorthAsifMug3450.002026-01-19
4WestChrisKettle1990.002026-02-02
5SouthBelaKettle21500.002026-02-11
6NorthDivyaMug2300.002026-02-14
7WestChrisToaster1610.002026-03-03
8SouthElanMug4520.002026-03-09
9NorthDivyaKettle1640.002026-03-21

2. Syntax

syntax.sql
SELECT group_columns, AGG(column) ...
FROM table
[WHERE row_condition]
GROUP BY group_columns
[HAVING group_condition]
[ORDER BY ...];
AggregatePer 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

query.sql
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

regionsalesrevenueavg_sale
South32820.00940
North42590.00648
West21600.00800

4. Grouping by Several Columns

Each unique combination becomes a group:

query.sql
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

regionproductunitsrevenue
NorthKettle31840.00
NorthMug5750.00
SouthKettle21500.00
SouthToaster1800.00
SouthMug4520.00
WestKettle1990.00
WestToaster1610.00

5. Filtering Rows First with WHERE

WHERE removes rows before grouping. Revenue per rep, counting only kettles:

query.sql
SELECT rep, COUNT(*) AS kettles_sold, SUM(amount) AS kettle_revenue
FROM sales
WHERE product = 'Kettle'
GROUP BY rep;

Result: 4 rows

repkettles_soldkettle_revenue
Asif11200.00
Chris1990.00
Bela11500.00
Divya1640.00

To filter groups after aggregation, use HAVING.

6. Grouping by Expressions and Dates

Group by a calculated value such as the month:

query.sql
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

monthsalesrevenue
2026-0132450.00
2026-0232790.00
2026-0331770.00

Group into price bands with CASE:

query.sql
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_bandsales
small2
medium5
large2

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:

query.sql
SELECT region, rep, SUM(amount) FROM sales GROUP BY region;
Output
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:

query.sql
SELECT region, GROUP_CONCAT(DISTINCT rep ORDER BY rep) AS reps, SUM(amount) AS revenue
FROM sales
GROUP BY region;

Result: 3 rows

regionrepsrevenue
NorthAsif,Divya2590.00
SouthBela,Elan2820.00
WestChris1600.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:

query.sql
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

regionproductrevenue
NorthKettle1840.00
NorthMug750.00
Northall products2590.00
SouthKettle1500.00
SouthMug520.00
SouthToaster800.00
Southall products2820.00
WestKettle990.00
WestToaster610.00
Westall products1600.00
ALL REGIONSall products7010.00

9. COUNT(DISTINCT) per Group

query.sql
SELECT region, COUNT(*) AS sales, COUNT(DISTINCT product) AS products, COUNT(DISTINCT rep) AS reps
FROM sales
GROUP BY region;

Result: 3 rows

regionsalesproductsreps
North422
South332
West221

10. Sorting Groups

MySQL 8 doesn't sort GROUP BY results implicitly. Add ORDER BY, which may use aliases and aggregates:

query.sql
SELECT rep, SUM(amount) AS revenue FROM sales GROUP BY rep ORDER BY revenue DESC LIMIT 3;

Result: 3 rows

reprevenue
Bela2300.00
Asif1650.00
Chris1600.00

11. Practice Exercises

Exercise 1: Units sold per product, most popular first.
solution.sql
SELECT product, SUM(qty) AS units FROM sales GROUP BY product ORDER BY units DESC;

Result: 3 rows

productunits
Mug9
Kettle6
Toaster2
Exercise 2: For each rep, their first and latest sale dates.
solution.sql
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

repfirst_salelatest_sale
Asif2026-01-052026-01-19
Bela2026-01-072026-02-11
Chris2026-02-022026-03-03
Divya2026-02-142026-03-21
Elan2026-03-092026-03-09
Exercise 3: Monthly revenue for the South region only.
solution.sql
SELECT MONTH(sold_on) AS m, SUM(amount) AS revenue FROM sales WHERE region = 'South' GROUP BY m ORDER BY m;

Result: 3 rows

mrevenue
1800.00
21500.00
3520.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.