SQL interview question: WHERE vs HAVING
Filter Rows and Groups With WHERE and HAVING
HR wants to see which teams have been growing lately, counting only people hired from 2020 onward.
For employees with a hire_date on or after 2020-01-01, return department, new_hires (the number of those employees) and avg_salary (their average salary, rounded to a whole number). Keep only departments with at least 4 such hires. Order by new_hires descending, then department.
Skills: WHERE, GROUP BY, HAVING and the difference between filtering rows and filtering groups
Tables
employees 37 rows
- id integer PK
- first_name text
- last_name text
- email text
- department_id integer → departments
- salary integer
- hire_date text
- manager_id integer → employees
- title text
| id | first_name | last_name | department_id | salary | hire_date | manager_id | title | |
|---|---|---|---|---|---|---|---|---|
| 1 | Alice | Chen | [email protected] | 1 | 250000 | 2015-01-15 | NULL | CEO |
| 2 | Robert | Martinez | [email protected] | 2 | 180000 | 2016-03-20 | NULL | CEO |
| 3 | Sarah | Johnson | [email protected] | 1 | 150000 | 2017-06-10 | 1 | VP Engineering |
departments 5 rows
- id integer PK
- name text
- location text
| id | name | location |
|---|---|---|
| 1 | Engineering | San Francisco |
| 2 | Sales | New York |
| 3 | Marketing | Los Angeles |
How to solve it
WHERE filters individual rows before they're grouped, and HAVING filters groups after GROUP BY has built them. So a condition on a column like hire_date goes in WHERE, and a condition on an aggregate like COUNT(*) >= 4 goes in HAVING.
The order the database works in
A grouped query is processed in roughly this order: FROM and the joins, then WHERE, then GROUP BY, then HAVING, then SELECT, then ORDER BY. That order explains every rule about the two clauses.
WHERE sees one row at a time. It can test hire_date or salary, but it can't test a count, because no groups exist yet. HAVING sees one group at a time, after the aggregates are computed, so it's the place for COUNT(*), SUM(...) or AVG(...).
The query that fails
The most common mistake is putting the aggregate in WHERE:
SELECT department_id, COUNT(*)
FROM employees
WHERE COUNT(*) >= 4
GROUP BY department_id;SQLite stops with misuse of aggregate: COUNT(), and other databases give a similar error. The fix is to move that condition into HAVING.
The query that runs but answers a different question
The quieter mistake is dropping the row filter. Without the WHERE, the count covers every employee ever hired:
SELECT d.name AS department, COUNT(*) AS new_hires, ROUND(AVG(e.salary)) AS avg_salary
FROM employees e
JOIN departments d ON d.id = e.department_id
GROUP BY d.name
HAVING COUNT(*) >= 4
ORDER BY new_hires DESC, department;That returns all five departments, from Engineering with 13 people down to HR with 4. No error, just the wrong answer. With the WHERE in place, only 19 employees hired from 2020 onward are counted. Engineering has 6 of them, Sales and Marketing have 4 each, Finance has 3 and HR has 2, so the right result is three departments.
This is why the two clauses aren't interchangeable. The same COUNT(*) >= 4 gives a different result depending on which rows reach GROUP BY.
Using both in one query
Most real questions need both. Here's the same report with a different group filter, keeping departments where recent hires average more than 100000:
SELECT d.name AS department, COUNT(*) AS new_hires, ROUND(AVG(e.salary)) AS avg_salary
FROM employees e
JOIN departments d ON d.id = e.department_id
WHERE e.hire_date >= '2020-01-01'
GROUP BY d.name
HAVING AVG(e.salary) > 100000;Engineering and Sales stay, both at 107500. Compare that with putting the salary test in WHERE:
SELECT d.name AS department, COUNT(*) AS new_hires
FROM employees e
JOIN departments d ON d.id = e.department_id
WHERE e.hire_date >= '2020-01-01' AND e.salary > 100000
GROUP BY d.name;Now the question is "how many recent hires earn over 100000 in each department", and four departments come back (Engineering 4, Sales 3, Finance 1, Marketing 1). A salary test on single rows and an average test on groups are different questions.
When either clause works
A condition on a grouping column can go in either place. These two return the same four departments without HR:
SELECT d.name AS department, COUNT(*) AS headcount
FROM employees e
JOIN departments d ON d.id = e.department_id
WHERE d.name <> 'HR'
GROUP BY d.name;SELECT d.name AS department, COUNT(*) AS headcount
FROM employees e
JOIN departments d ON d.id = e.department_id
GROUP BY d.name
HAVING d.name <> 'HR';Prefer WHERE here. It removes rows before the grouping work, and it tells the reader the condition is about rows, not totals. Many optimizers will move such a HAVING condition into WHERE for you, but you shouldn't count on it.
What to say in the interview
Give the one-line rule first: WHERE filters rows before grouping, HAVING filters groups after it. Then mention the execution order, and say that aggregates can only be filtered in HAVING. If you have time, point out that non-aggregate conditions belong in WHERE even when HAVING would accept them.
For more practice, try Departments With More Than 5 Employees, the Conditional Count With SUM and CASE question, or the GROUP BY interview questions.
Common questions
- Can I use HAVING without GROUP BY?
- Yes. The whole result is treated as one group.
SELECT COUNT(*) AS new_hires FROM employees WHERE hire_date >= '2020-01-01' HAVING COUNT(*) >= 4returns one row with 19, and it would return no rows if the count were below 4. - Can I use a column alias in HAVING?
- SQLite and MySQL accept
HAVING new_hires >= 4. PostgreSQL and SQL Server don't, so repeating the aggregate (HAVING COUNT(*) >= 4) is the portable choice. - Is WHERE faster than HAVING?
- For conditions on plain columns, WHERE is usually the better choice because it removes rows before they're grouped. Conditions on aggregates have to go in HAVING, so there's no choice to make there.
- Can a query have both WHERE and HAVING?
- Yes, and many interview questions need both. WHERE runs first and decides which rows get grouped, then HAVING decides which groups are kept, as in the practice question on this page.
Practice on your iPhone
Query Dojo for iPhone has 85 interview questions, timed mock interviews and a tile builder, so you can write queries without a keyboard.
Other SQL dialects
- PostgreSQL You can't use the output alias in HAVING (`HAVING new_hires >= 4` fails). Repeat the aggregate: `HAVING COUNT(*) >= 4`. ROUND(AVG(salary)) returns a numeric value there.
- SQL Server Aliases are not allowed in HAVING either, so write `HAVING COUNT(*) >= 4`. AVG of an integer column returns an integer, so cast first with `AVG(CAST(e.salary AS DECIMAL(12,2)))` if you need the exact average.