SQL GROUP BY
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.
- 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
SELECT group_column, AGGREGATE(column)FROM table_name[WHERE row_condition]GROUP BY group_column[HAVING group_condition][ORDER BY ...];Sample data
| emp_id | first_name | dept_id | job_title | salary | city |
|---|---|---|---|---|---|
| 100 | Meera | NULL | CEO | 250000 | Mumbai |
| 101 | Asha | 1 | Engineering Manager | 125000 | Bengaluru |
| 102 | Rahul | 1 | Senior Developer | 98000 | Bengaluru |
| 103 | Priya | 1 | QA Engineer | 72000 | Pune |
| 104 | Karan | 1 | Developer | 72000 | Bengaluru |
| 105 | Vikram | 2 | Sales Manager | 105000 | Mumbai |
| 106 | Neha | 2 | Sales Executive | 54000 | Mumbai |
| 107 | Arjun | 2 | Sales Executive | 48000 | Kolkata |
| 108 | Kavya | 3 | Marketing Lead | 88000 | Pune |
| 109 | Rohan | 3 | Content Writer | 45000 | Pune |
| 110 | Sneha | 4 | HR Manager | 78000 | Delhi |
| 111 | Amit | 1 | Intern | 25000 | Bengaluru |
Group by one column
Headcount and payroll per department:
SELECT dept_id, COUNT(*) AS staff, SUM(salary) AS payrollFROM employeesGROUP BY dept_idORDER BY dept_id;| dept_id | staff | payroll |
|---|---|---|
| NULL | 1 | 250000 |
| 1 | 5 | 392000 |
| 2 | 3 | 207000 |
| 3 | 2 | 133000 |
| 4 | 1 | 78000 |
FROM employeesreads all 12 rows.GROUP BY dept_idsorts the rows into piles with the samedept_id: NULL (the CEO), 1, 2, 3 and 4.COUNT(*)andSUM(salary)run once per pile.- The result has one row per group: 5 rows.
Picture the piles for departments 2 and 3:
| dept_id | rows in the group | COUNT(*) | SUM(salary) |
|---|---|---|---|
| 2 | Vikram 105000, Neha 54000, Arjun 48000 | 3 | 207000 |
| 3 | Kavya 88000, Rohan 45000 | 2 | 133000 |
Show names instead of ids
Join the departments table and group by the name:
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;| dept_name | staff | avg_salary |
|---|---|---|
| Engineering | 5 | 78400 |
| Sales | 3 | 69000 |
| Marketing | 2 | 66500 |
| HR | 1 | 78000 |
- The inner join drops the CEO (no department) and adds
dept_nameto each employee row. GROUP BY d.dept_namemakes one group per department name.ORDER BY staff DESClists the largest department first.
Group by several columns
One group for every combination of department and city:
SELECT dept_id, city, COUNT(*) AS staffFROM employeesWHERE dept_id IS NOT NULLGROUP BY dept_id, cityORDER BY dept_id, city;| dept_id | city | staff |
|---|---|---|
| 1 | Bengaluru | 4 |
| 1 | Pune | 1 |
| 2 | Kolkata | 1 |
| 2 | Mumbai | 2 |
| 3 | Pune | 2 |
| 4 | Delhi | 1 |
WHEREremoves the CEO before grouping.- Rows with the same
dept_idand the samecityform one group. - 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:
SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, COUNT(*) AS ordersFROM ordersGROUP BY monthORDER BY month;| month | orders |
|---|---|
| 2025-01 | 2 |
| 2025-02 | 2 |
| 2025-03 | 2 |
| 2025-04 | 2 |
| 2025-05 | 1 |
| 2025-06 | 1 |
Revenue per product category:
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;| category | units | revenue |
|---|---|---|
| Furniture | 7 | 50493 |
| Electronics | 7 | 16093 |
| Stationery | 24 | 5676 |
- The join attaches the category and price to each of the 16 order lines.
- The lines are grouped into Electronics, Furniture and Stationery.
- For each category,
SUMadds up units andquantity × 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:
SELECT dept_id, first_name, COUNT(*)FROM employeesGROUP BY dept_id;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
- Department 1 has five employees, so “the”
first_nameof the group is undefined. - MySQL 8.4 refuses the query (error 1055). Older versions silently picked a random name.
- Fix it by aggregating (
GROUP_CONCAT(first_name)) or by grouping byfirst_nametoo.
SELECT dept_id, COUNT(*) AS staff, GROUP_CONCAT(first_name ORDER BY first_name) AS namesFROM employeesGROUP BY dept_id;| dept_id | staff | names |
|---|---|---|
| NULL | 1 | Meera |
| 1 | 5 | Amit,Asha,Karan,Priya,Rahul |
| 2 | 3 | Arjun,Neha,Vikram |
| 3 | 2 | Kavya,Rohan |
| 4 | 1 | Sneha |
WHERE vs HAVING
WHEREfilters rows before grouping.- HAVING filters groups after grouping.
SELECT dept_id, COUNT(*) AS staffFROM employeesWHERE salary >= 50000GROUP BY dept_idHAVING COUNT(*) >= 2;| dept_id | staff |
|---|---|
| 1 | 4 |
| 2 | 2 |
Subtotals with WITH ROLLUP
WITH ROLLUP adds a total row after the groups:
SELECT category, COUNT(*) AS products, SUM(stock) AS stockFROM productsGROUP BY category WITH ROLLUP;| category | products | stock |
|---|---|---|
| Electronics | 3 | 165 |
| Furniture | 4 | 65 |
| Stationery | 2 | 550 |
| NULL | 9 | 780 |
- The first three rows are the normal groups.
- The extra last row has
NULLincategoryand the totals over all products. - 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.
| customer_id | orders | last_order |
|---|---|---|
| 1 | 3 | 2025-04-28 |
| 2 | 2 | 2025-04-10 |
| 3 | 2 | 2025-05-15 |
| 5 | 2 | 2025-06-01 |
| 4 | 1 | 2025-03-08 |
Hint 1
COUNT(*) and MAX(order_date) grouped by customer_id.
SELECT customer_id, COUNT(*) AS orders, MAX(order_date) AS last_orderFROM ordersGROUP BY customer_idORDER BY orders DESC, customer_id;| customer_id | orders | last_order |
|---|---|---|
| 1 | 3 | 2025-04-28 |
| 2 | 2 | 2025-04-10 |
| 3 | 2 | 2025-05-15 |
| 5 | 2 | 2025-06-01 |
| 4 | 1 | 2025-03-08 |
Exercise: Stock value per category
For each product category, show the total stock value (price * stock).
| category | stock_value |
|---|---|
| Furniture | 298335 |
| Electronics | 253335 |
| Stationery | 124450 |
Hint 1
SUM(price * stock) grouped by category.
SELECT category, SUM(price * stock) AS stock_valueFROM productsGROUP BY categoryORDER BY stock_value DESC;| category | stock_value |
|---|---|
| Furniture | 298335 |
| Electronics | 253335 |
| Stationery | 124450 |
Quick quiz
1. How many rows does SELECT dept_id, COUNT(*) FROM employees GROUP BY dept_id return?
Each group becomes one result row.
2. Which clause filters groups by an aggregate value?
HAVING runs after grouping.
3. Why does SELECT dept_id, first_name, COUNT(*) ... GROUP BY dept_id fail in MySQL 8?
ONLY_FULL_GROUP_BY requires every selected column to be grouped or aggregated.
4. What does WITH ROLLUP add?
ROLLUP adds summary rows with NULL in the rolled-up columns.
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.