SQL Tutorial › Functions & Grouping › GROUP BY

SQL GROUP BY

IntermediateLesson 24 of 6912 min read9 queries run on MySQL 8.4

GROUP BY splits rows into groups that share a value and runs an aggregate on each group: headcount per department, revenue per month, orders per customer. It is the single most important clause for reporting, and this lesson shows exactly how MySQL builds the groups.

Advertisement
In this lesson
  • Group rows and aggregate each group
  • Group by several columns and by expressions
  • Combine GROUP BY with joins and ORDER BY
  • Add subtotals with WITH ROLLUP

GROUP BY syntax

MySQL
SELECT group_column, AGGREGATE(column)FROM table_name[WHERE row_condition]GROUP BY group_column[HAVING group_condition][ORDER BY ...];

Sample data

employees · 12 rows (some columns)
emp_idfirst_namedept_idjob_titlesalarycity
100MeeraNULLCEO250000Mumbai
101Asha1Engineering Manager125000Bengaluru
102Rahul1Senior Developer98000Bengaluru
103Priya1QA Engineer72000Pune
104Karan1Developer72000Bengaluru
105Vikram2Sales Manager105000Mumbai
106Neha2Sales Executive54000Mumbai
107Arjun2Sales Executive48000Kolkata
108Kavya3Marketing Lead88000Pune
109Rohan3Content Writer45000Pune
110Sneha4HR Manager78000Delhi
111Amit1Intern25000Bengaluru

Group by one column

Headcount and payroll per department:

SQL
SELECT dept_id,       COUNT(*)    AS staff,       SUM(salary) AS payrollFROM employeesGROUP BY dept_idORDER BY dept_id;
ResultMySQL 8.4
dept_idstaffpayroll
NULL1250000
15392000
23207000
32133000
4178000
5 rows in set
How this query works, step by step
  1. FROM employees reads all 12 rows.
  2. GROUP BY dept_id sorts the rows into piles with the same dept_id: NULL (the CEO), 1, 2, 3 and 4.
  3. COUNT(*) and SUM(salary) run once per pile.
  4. The result has one row per group: 5 rows.

Picture the piles for departments 2 and 3:

dept_idrows in the groupCOUNT(*)SUM(salary)
2Vikram 105000, Neha 54000, Arjun 480003207000
3Kavya 88000, Rohan 450002133000

Show names instead of ids

Join the departments table and group by the name:

SQL
SELECT d.dept_name,       COUNT(*)           AS staff,       ROUND(AVG(e.salary)) AS avg_salaryFROM employees eJOIN departments d ON d.dept_id = e.dept_idGROUP BY d.dept_nameORDER BY staff DESC;
ResultMySQL 8.4
dept_namestaffavg_salary
Engineering578400
Sales369000
Marketing266500
HR178000
4 rows in set
How this query works, step by step
  1. The inner join drops the CEO (no department) and adds dept_name to each employee row.
  2. GROUP BY d.dept_name makes one group per department name.
  3. ORDER BY staff DESC lists the largest department first.

Group by several columns

One group for every combination of department and city:

SQL
SELECT dept_id, city, COUNT(*) AS staffFROM employeesWHERE dept_id IS NOT NULLGROUP BY dept_id, cityORDER BY dept_id, city;
ResultMySQL 8.4
dept_idcitystaff
1Bengaluru4
1Pune1
2Kolkata1
2Mumbai2
3Pune2
4Delhi1
6 rows in set
How this query works, step by step
  1. WHERE removes the CEO before grouping.
  2. Rows with the same dept_id and the same city form one group.
  3. Engineering is split in two: 4 people in Bengaluru and 1 in Pune.

Group by an expression

Orders per month. DATE_FORMAT turns each date into a 'YYYY-MM' label, and the label is the group:

MySQL
SELECT DATE_FORMAT(order_date, '%Y-%m') AS month,       COUNT(*) AS ordersFROM ordersGROUP BY monthORDER BY month;
ResultMySQL 8.4
monthorders
2025-012
2025-022
2025-032
2025-042
2025-051
2025-061
6 rows in set

Revenue per product category:

SQL
SELECT p.category,       SUM(oi.quantity)           AS units,       SUM(oi.quantity * p.price) AS revenueFROM order_items oiJOIN products p ON p.product_id = oi.product_idGROUP BY p.categoryORDER BY revenue DESC;
ResultMySQL 8.4
categoryunitsrevenue
Furniture750493
Electronics716093
Stationery245676
3 rows in set
How this query works, step by step
  1. The join attaches the category and price to each of the 16 order lines.
  2. The lines are grouped into Electronics, Furniture and Stationery.
  3. For each category, SUM adds up units and quantity × price.

The ONLY_FULL_GROUP_BY rule

Every column in SELECT must either be in GROUP BY or be inside an aggregate. Otherwise MySQL cannot know which row’s value to show:

MySQL
SELECT dept_id, first_name, COUNT(*)FROM employeesGROUP BY dept_id;
ResultMySQL 8.4
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'company.employees.first_name' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
How this query works, step by step
  1. Department 1 has five employees, so “the” first_name of the group is undefined.
  2. MySQL 8.4 refuses the query (error 1055). Older versions silently picked a random name.
  3. Fix it by aggregating (GROUP_CONCAT(first_name)) or by grouping by first_name too.
SQL
SELECT dept_id, COUNT(*) AS staff, GROUP_CONCAT(first_name ORDER BY first_name) AS namesFROM employeesGROUP BY dept_id;
ResultMySQL 8.4
dept_idstaffnames
NULL1Meera
15Amit,Asha,Karan,Priya,Rahul
23Arjun,Neha,Vikram
32Kavya,Rohan
41Sneha
5 rows in set

WHERE vs HAVING

  • WHERE filters rows before grouping.
  • HAVING filters groups after grouping.
SQL
SELECT dept_id, COUNT(*) AS staffFROM employeesWHERE salary >= 50000GROUP BY dept_idHAVING COUNT(*) >= 2;
ResultMySQL 8.4
dept_idstaff
14
22
2 rows in set

Subtotals with WITH ROLLUP

WITH ROLLUP adds a total row after the groups:

MySQL
SELECT category, COUNT(*) AS products, SUM(stock) AS stockFROM productsGROUP BY category WITH ROLLUP;
ResultMySQL 8.4
categoryproductsstock
Electronics3165
Furniture465
Stationery2550
NULL9780
4 rows in set
How this query works, step by step
  1. The first three rows are the normal groups.
  2. The extra last row has NULL in category and the totals over all products.
  3. Use GROUPING(category) to tell a rollup NULL from a real NULL.

Logical order with GROUP BY

FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Because SELECT runs after GROUP BY, aggregates are available to SELECT, HAVING and ORDER BY, but not to WHERE.

Exercise: Orders per customer

For each customer_id in orders, show the number of orders and the date of the latest order. Sort by the number of orders, highest first.

Expected resultMySQL 8.4
customer_idorderslast_order
132025-04-28
222025-04-10
322025-05-15
522025-06-01
412025-03-08
5 rows in set
Hint 1

COUNT(*) and MAX(order_date) grouped by customer_id.

Exercise: Stock value per category

For each product category, show the total stock value (price * stock).

Expected resultMySQL 8.4
categorystock_value
Furniture298335
Electronics253335
Stationery124450
3 rows in set
Hint 1

SUM(price * stock) grouped by category.

Quick quiz

1. How many rows does SELECT dept_id, COUNT(*) FROM employees GROUP BY dept_id return?

2. Which clause filters groups by an aggregate value?

3. Why does SELECT dept_id, first_name, COUNT(*) ... GROUP BY dept_id fail in MySQL 8?

4. What does WITH ROLLUP add?

GROUP BY FAQ

What does GROUP BY do in SQL?

It groups rows that have the same values in the listed columns, so aggregate functions such as COUNT and SUM return one result per group.

Can I GROUP BY multiple columns?

Yes. GROUP BY dept_id, city creates one group for each distinct combination of the two columns.

What is the difference between GROUP BY and ORDER BY?

GROUP BY combines rows into groups for aggregation. ORDER BY only sorts the final result. They are often used together.

Does GROUP BY sort the results?

Not reliably. MySQL 8 no longer sorts grouped results implicitly. Add ORDER BY when you need a specific order.