Your principal walks in and asks: "How many students are there in each class?" Notice what she did not ask. She doesn't want 5,000 names — she wants maybe 40 rows, one per class, with a count. Every dashboard you've ever seen — orders per city on Swiggy, videos per channel on YouTube, average salary per department — is this same shape: many rows in, one row per group out. This lesson teaches you that shape.
GROUP BY — sorting rows into buckets
SELECT department, COUNT(*) AS emp_count
FROM employees
GROUP BY department;Result
Engineering | 4 Sales | 4 Marketing | 2
Picture it physically: SQL reads every row and drops it into a bucket based on its department. Then each bucket collapses into exactly one output row. The individual employees are gone from the result — only bucket-level summaries remain. That single sentence explains half the errors people hit with GROUP BY.
Group by two columns — GROUP BY department, city — and a bucket forms per unique combination: (Sales, Delhi) and (Sales, Bangalore) are different buckets. It never means "group by department AND separately by city" — that would be two different queries.
Aggregate functions — summarising a bucket
Once rows are in buckets, aggregate functions compute one value per bucket. The big five: COUNT (how many), SUM (total), AVG (average), MIN / MAX (extremes).
SELECT department,
COUNT(*) AS emp_count,
SUM(salary) AS total_salary,
ROUND(AVG(salary), 2) AS avg_salary,
MAX(salary) AS top_salary
FROM employees
GROUP BY department
ORDER BY total_salary DESC;Result
Engineering | 4 | 350000 | 87500.00 | 95000 Sales | 4 | 247000 | 61750.00 | 72000 Marketing | 2 | 120000 | 60000.00 | 60000
Fun fact: aggregates work even without GROUP BY — then the whole table is one big bucket. SELECT COUNT(*) FROM employees; returns a single number.
The COUNT family — three cousins, three answers
On the same data, these can return three different numbers:
COUNT(*)— how many rows are in the bucket. NULLs don't matter; rows are rows.COUNT(email)— how many rows have a non-NULL email. Blanks are skipped.COUNT(DISTINCT email)— how many unique non-NULL emails.
A 100-row table with 10 missing emails and 5 people sharing a family email: COUNT(*) = 100, COUNT(email) = 90, COUNT(DISTINCT email) = 86. Three questions, three answers — and interviewers love asking you to trace exactly this.
Aggregates and NULL — the silent skip
Remember from lesson 1: NULL means unknown. Aggregates deal with it by ignoring NULLs entirely — on both sides of the maths. A bonus column holding (5000, NULL, 3000, NULL, 2000):
SELECT COUNT(*), COUNT(bonus), AVG(bonus)
FROM payouts;Result
5 | 3 | 3333.33
AVG divides 10000 by 3 (the non-NULL count), not by 5. If a blank should count as zero, say so: AVG(COALESCE(bonus, 0)).
WHERE vs HAVING — the question you will definitely face
The principal upgrades her demand: "only show classes with more than 60 students." Can you check that condition before forming the buckets? Think about it — the count doesn't exist until the bucket is complete! That's the entire story:
| WHERE | HAVING | |
|---|---|---|
| Filters | individual rows | finished groups |
| Runs | before GROUP BY | after GROUP BY |
| Can use SUM/COUNT? | never — they don't exist yet | yes — that's its whole job |
| Think of it as | the guard at the gate | quality-check after packing |
-- WRONG: WHERE runs before grouping — no SUM exists yet
SELECT department, SUM(salary)
FROM employees
WHERE SUM(salary) > 200000 -- ERROR
GROUP BY department;
-- RIGHT: filter the groups with HAVING
SELECT department, SUM(salary) AS total_salary
FROM employees
GROUP BY department
HAVING SUM(salary) > 200000;Result
Engineering | 350000 Sales | 247000
Which columns are you allowed to SELECT?
Here's a query that looks innocent:
SELECT name, department, COUNT(*)
FROM employees
GROUP BY department; -- ERROR in modern MySQLThe Engineering bucket holds 4 employees — 4 different names. Which name should SQL print? There is no right answer, and that's exactly the point. Old MySQL silently picked one at random (a legendary source of production bugs). Since version 5.7.5, the ONLY_FULL_GROUP_BY mode is on by default and rejects the query. The rule: every selected column is either in the GROUP BY or wrapped in an aggregate. Want a name alongside a group's max salary? That needs a join back or a window function — both covered in later lessons, and one appears in this Practice Zone.
Putting it all together
A full pipeline question: "Among employees hired from 2020 onwards, which departments pay an average above 65,000?" Watch each clause do its one job:
SELECT department, ROUND(AVG(salary), 2) AS avg_salary
FROM employees
WHERE hire_date >= '2020-01-01' -- 1. guard the gate (rows)
GROUP BY department -- 2. form buckets
HAVING AVG(salary) > 65000 -- 3. quality-check buckets
ORDER BY avg_salary DESC; -- 4. sort the survivorsThis WHERE → GROUP BY → HAVING → ORDER BY flow is the same execution order you saw in lesson 1's diagram — grouping questions are just that diagram wearing a different shirt.
Common mistakes
- Putting an aggregate condition in
WHERE— aggregate conditions live inHAVING. - Selecting a column that's neither grouped nor aggregated — modern MySQL errors out; older MySQL silently guessed.
- Using
HAVINGfor plain row filters — it works, but it's slower and confusing; row filters belong inWHERE. - Forgetting that
AVGskips NULLs — decide explicitly whether a blank means "unknown" or "zero". - Joining one-to-many before aggregating and inflating the numbers — the fan-out trap from lesson 2.
Quick recap
| Piece | Job | Remember |
|---|---|---|
GROUP BY | rows → buckets, one output row per bucket | multi-column = unique combinations |
COUNT(*) vs COUNT(col) | rows vs non-NULL values | add DISTINCT for unique values |
SUM / AVG / MIN / MAX | summarise a bucket | all of them skip NULLs |
WHERE | filter rows, before grouping | can't see aggregates |
HAVING | filter groups, after grouping | the only home for aggregate conditions |
Practice Zone — PYQs from real selection rounds
Ten employees, three departments, three cities — and the exact GROUP BY questions companies ask, ending with the classic: highest-paid per department, ties included. Attempt first, reveal second.
In a 100-row table where the email column is NULL in 10 rows, what does COUNT(email) return?
Asked in


I want only the departments whose total salary crosses ₹10,00,000. Where does that condition go?
Asked in


GROUP BY department, city — what forms one group?
Asked in


A bonus column holds (5000, NULL, 3000, NULL, 2000) across 5 rows. What does AVG(bonus) return?
Asked in


Fundamentally, why is WHERE SUM(salary) > 100000 illegal?
Asked in


In modern MySQL (5.7.5+, default settings), what happens to SELECT name, department, COUNT(*) FROM employees GROUP BY department; — where name is neither grouped nor aggregated?
Asked in


Given the employees table below, write a query to count how many employees work in each department, sorted by employee count descending.
Asked in


employees10 rows
| emp_id | name | department | city | salary | hire_date |
|---|---|---|---|---|---|
| 1 | Alice Sharma | Engineering | Mumbai | 95000 | 2019-03-12 |
| 2 | Bob Iyer | Engineering | Mumbai | 72000 | 2020-07-01 |
| 3 | Chitra Rao | Sales | Delhi | 65000 | 2018-11-20 |
| 4 | David Paul | Sales | Delhi | 58000 | 2021-01-15 |
| 5 | Esha Nair | Engineering | Bangalore | 88000 | 2019-09-05 |
| 6 | Farhan Khan | Marketing | Mumbai | 60000 | 2020-02-28 |
| 7 | Gita Menon | Marketing | Mumbai | 60000 | 2022-04-10 |
| 8 | Harish Verma | Sales | Bangalore | 72000 | 2022-06-18 |
| 9 | Amit Verma | Sales | Delhi | 52000 | 2021-08-01 |
| 10 | Isha Kapoor | Engineering | Mumbai | 95000 | 2023-01-10 |
Using the same employees table, write a query to list departments that have more than 3 employees, along with their employee count.
Asked in


employees10 rows
| emp_id | name | department | city | salary | hire_date |
|---|---|---|---|---|---|
| 1 | Alice Sharma | Engineering | Mumbai | 95000 | 2019-03-12 |
| 2 | Bob Iyer | Engineering | Mumbai | 72000 | 2020-07-01 |
| 3 | Chitra Rao | Sales | Delhi | 65000 | 2018-11-20 |
| 4 | David Paul | Sales | Delhi | 58000 | 2021-01-15 |
| 5 | Esha Nair | Engineering | Bangalore | 88000 | 2019-09-05 |
| 6 | Farhan Khan | Marketing | Mumbai | 60000 | 2020-02-28 |
| 7 | Gita Menon | Marketing | Mumbai | 60000 | 2022-04-10 |
| 8 | Harish Verma | Sales | Bangalore | 72000 | 2022-06-18 |
| 9 | Amit Verma | Sales | Delhi | 52000 | 2021-08-01 |
| 10 | Isha Kapoor | Engineering | Mumbai | 95000 | 2023-01-10 |
Using the same employees table, write a query to find, for each department, the employee(s) with the highest salary in that department — including ties, if more than one employee shares the department's maximum salary.
Asked in
employees10 rows
| emp_id | name | department | city | salary | hire_date |
|---|---|---|---|---|---|
| 1 | Alice Sharma | Engineering | Mumbai | 95000 | 2019-03-12 |
| 2 | Bob Iyer | Engineering | Mumbai | 72000 | 2020-07-01 |
| 3 | Chitra Rao | Sales | Delhi | 65000 | 2018-11-20 |
| 4 | David Paul | Sales | Delhi | 58000 | 2021-01-15 |
| 5 | Esha Nair | Engineering | Bangalore | 88000 | 2019-09-05 |
| 6 | Farhan Khan | Marketing | Mumbai | 60000 | 2020-02-28 |
| 7 | Gita Menon | Marketing | Mumbai | 60000 | 2022-04-10 |
| 8 | Harish Verma | Sales | Bangalore | 72000 | 2022-06-18 |
| 9 | Amit Verma | Sales | Delhi | 52000 | 2021-08-01 |
| 10 | Isha Kapoor | Engineering | Mumbai | 95000 | 2023-01-10 |
FAQ
Can I use HAVING without GROUP BY?
Yes — then the whole table is treated as one group. SELECT COUNT(*) FROM employees HAVING COUNT(*) > 5; returns the count only if it exceeds 5. Rarely useful, commonly asked.
Does GROUP BY sort the result?
Don't rely on it. Older MySQL used to sort as a side effect; modern versions don't promise anything. If you want order, write ORDER BY — it runs after HAVING and can sort by your aggregate aliases.
Can I filter on an alias in HAVING?
In MySQL, yes — HAVING emp_count > 3 works because MySQL extends the standard. In strict-standard databases you'd repeat the aggregate: HAVING COUNT(*) > 3. The repeated form works everywhere, so prefer it in interviews.
Next lesson: what if the filter itself needs a query to compute? Lesson 4: Subqueries →


