Step 1 Understand it
Why you need it. One total is rarely enough. Managers ask for "revenue per month", "headcount per department", "orders per customer". gives you a total for every group in one , instead of one query per group.
New words in this topic (tap one to see what it means)
Watch it happen
Read the animation as text
- Eight employees, in no particular order.
- GROUP BY dept_id puts rows with the same dept_id into one pile. All the NULLs (just Gus) share a pile of their own.
- Each pile becomes ONE result row, and COUNT(*) and AVG(salary) are worked out separately for each pile: department 1 has 4 people averaging 78750.
- The golden rule: SELECT may only show grouped columns and aggregates. Department 1 has four names — the database cannot pick one for you. And HR has no employees, so no pile and no row.
Picture it
flowchart LR E["8 employee rows"] --> G1["dept 1: Asha, Ben, Chen, Hana"] E --> G2["dept 2: Dara, Eli"] E --> G3["dept 3: Fatima"] E --> G0["dept NULL: Gus"] G1 --> R1["1 · 4 people · avg 78750"] G2 --> R2["2 · 2 people · avg 52500"] G3 --> R3["3 · 1 person · avg 52000"] G0 --> R0["NULL · 1 person · avg 38000"]
The idea
GROUP BY dept_id puts all rows with the same dept_id into one pile. Each pile becomes one result row, and every in is worked out separately for each pile. The golden rule: every in SELECT must either be in GROUP BY or be inside an aggregate — because a group of 4 employees has 4 names, and the can't pick one for you. All NULLs fall into one group of their own.
The shape of it
SELECT group_column, COUNT(*), SUM(column)
FROM table_name
GROUP BY group_column;Words in lower case are the parts you fill in.
How it works, step by step
- takes all 8 employees.
- GROUP BY dept_id makes piles: department 1 (Asha, Ben, Chen, Hana), department 2 (Dara, Eli), department 3 (Fatima), and a NULL pile (Gus).
- For each pile,
COUNT(*)andAVG(salary)are worked out: department 1 has 4 people averaging 78750. - SELECT returns one row per pile: dept_id, plus the aggregates.
- Department 4 (HR) has no employees, so there is no pile and no row for it. Getting zero rows to show needs a (Phase 3).
Analogy
Sorting a bag of coins into piles by type — all the 10p together, all the £1 together — and then counting each pile. GROUP BY makes the piles; and count them.
Where the analogy breaks
With coins you can still see each coin in its pile. After GROUP BY, the separate rows are gone from the result: you only see one row per pile, so you can't ask for "the name" of a group unless you aggregate it.
Worked examples
Headcount and average salary per department
SELECT dept_id, COUNT(*) AS headcount, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept_id
ORDER BY dept_id;4 rows
| dept_id | headcount | avg_salary |
|---|---|---|
| NULL | 1 | 38000 |
| 1 | 4 | 78750 |
| 2 | 2 | 52500 |
| 3 | 1 | 52000 |
Gus forms the NULL group. HR (department 4) is missing because no rows belong to it.
Revenue per month
SELECT strftime('%Y-%m', order_date) AS month,
COUNT(*) AS orders,
SUM(amount) AS revenue
FROM orders
GROUP BY strftime('%Y-%m', order_date)
ORDER BY month;3 rows
| month | orders | revenue |
|---|---|---|
| 2024-01 | 4 | 445 |
| 2024-02 | 3 | 210 |
| 2024-03 | 3 | 138 |
You can group by a calculation, not just a column. February has 3 orders but only 2 amounts, because order 106 is NULL.
Group by two columns
SELECT customer_id, status, COUNT(*) AS orders
FROM orders
GROUP BY customer_id, status
ORDER BY customer_id, status;6 rows
| customer_id | status | orders |
|---|---|---|
| 1 | delivered | 4 |
| 1 | shipped | 1 |
| 2 | delivered | 2 |
| 3 | cancelled | 1 |
| 3 | delivered | 1 |
| 5 | pending | 1 |
One row per pair of customer and status that actually appears in the data.
Must-know points
- GROUP BY makes one result row per distinct combination of the grouping columns.
- Every column in SELECT must be in GROUP BY or inside an aggregate.
- All NULLs in a grouping column form one group.
- Groups with no rows don't appear at all. To show zeros (like HR with 0 people), start from the full list and (Phase 3).
- Grouped results come back in no particular order. Add .
Common mistakes
- Selecting a column that is neither grouped nor aggregated, like
SELECT dept_id, name, COUNT(*) … GROUP BY dept_id. - Expecting groups with zero rows to appear with a count of 0.
- Grouping by too many columns (such as the id), so every group has one row and nothing is summarised.
In other databases
- and older let you select a column that isn't grouped and silently pick a value. , and modern MySQL reject it. Write it the strict way.
- GROUP BY an (
GROUP BY month) works in PostgreSQL, MySQL and SQLite, but not in SQL Server — repeat the there.
Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.
-
Read
W3Schools — SQL GROUP BY Statement
short page with a try-it editor, free
-
Practice
SQLBolt — Lesson 11: Queries with aggregates (Pt. 2)
interactive exercises in the browser, free
-
Practice
free exercises
Step 3 Practise
Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.
-
ExplainSay what it does and when to use it, in plain words, with no notes.
Explain the golden rule of GROUP BY, using the employees .
Show answer
Every column in SELECT must either be in the GROUP BY, or be inside an aggregate.
SELECT dept_id, COUNT(*) FROM employees GROUP BY dept_idis fine: one dept_id per group, and one count per group.SELECT dept_id, name, COUNT(*) … GROUP BY dept_idbreaks the rule. Department 1 has four people (Asha, Ben, Chen and Hana), but the group has room for only one name. Which one? PostgreSQL, SQL Server and (by default) MySQL refuse the query. SQLite runs it and picks one name, which is worse: it looks like an answer but means nothing.Fix it by grouping by name too, or by aggregating it, for example
MAX(name)orGROUP_CONCAT(name). -
Writefrom a blank page, then run it
Write a query for the number of products and the average price in each category, sorted by category.
Show answer
Group by category and aggregate the rest. Electronics averages the £40 headphones and the £15 phone case: 27.5.
sqlSELECT category, COUNT(*) AS products, AVG(price) AS avg_price FROM products GROUP BY category ORDER BY category;5 rows
category products avg_price Bags 1 30 Electronics 2 27.5 Home 1 25 Kitchen 1 8 Stationery 2 3.5 -
Predictsay it before you run it
How many rows does
SELECT city, COUNT(*) FROM customers GROUP BY city;return, and what is the count for Leeds?Show answer
4 rows: Bristol, Leeds, London and one group for NULL. Leeds has 2: both of Tom's sign-ups. London also has 2 (Priya and Lena), Bristol 1 (Omar), and the NULL group 1 (Zoe).
GROUP BY puts all the NULLs together in one group. (ORDER BY is added below so the order is fixed.)
sqlSELECT city, COUNT(*) FROM customers GROUP BY city ORDER BY city;4 rows
city COUNT(*) NULL 1 Bristol 1 Leeds 2 London 2 -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Write a query that returns each customer_id with their total spend, where a customer whose only orders have NULL amounts shows 0, not NULL.
Show answer
SUM skips NULLs, but if every amount in a group is NULL, there is nothing to add and SUM returns NULL. Wrap it:
COALESCE(SUM(amount), 0).No customer here has only NULL amounts (customer 3 has one NULL and one £60, so their total is 60). To see the work, add
WHERE order_id = 106and customer 3 shows 0.Note that Omar, who has no orders at all, is not in the result: he has no rows in orders to group. Showing him needs a LEFT JOIN from customers (Phase 3).
sqlSELECT customer_id, COALESCE(SUM(amount), 0) AS total_spend FROM orders GROUP BY customer_id ORDER BY customer_id;4 rows
customer_id total_spend 1 425 2 275 3 60 5 33
Practice box: write and run your answers here
Your queries in this box
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud.
GROUP BY the filtered rows into groups by the grouping columns, and aggregates are computed once per group, giving one output row per group. The rule is that every selected column must be grouped or aggregated. Two details I mention: NULLs form a single group, and groups with no rows simply don't appear — so to report zeros I drive the query from the complete dimension table with a LEFT JOIN.
Follow-up questions
Why must non-aggregated columns be in GROUP BY?
Each group becomes one row. A column that isn't grouped could have many values in the group, and won't guess which one to show.
How do you show departments with zero employees?
SELECT d.dept_name, COUNT(e.emp_id) FROM departments d LEFT JOIN employees e ON e.dept_id = d.dept_id GROUP BY d.dept_name — COUNT of a column gives 0 when nothing matched.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.
- Explain Say what it does and when to use it, in plain words, with no notes.
- Write Write the query from a blank page, then run it to check.
- Predict Read a query and say which rows it returns before you run it.
- Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.