HAVING vs WHERE
BeginnerWHERE filters individual rows before grouping; HAVING filters groups after aggregation — only HAVING can use aggregate functions as filter conditions.
Overview
Understanding when SQL filters rows versus groups is fundamental. The logical execution order is: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. WHERE operates at the row level, before any grouping or aggregation takes place, so it cannot reference aggregate functions. HAVING operates at the group level, after GROUP BY has been applied, so it can filter groups based on aggregated values like AVG(salary) or COUNT(*). A common performance mistake is using HAVING to filter on non-aggregated columns — this wastes work because those rows should have been eliminated by WHERE before grouping. Push every non-aggregate condition into WHERE to reduce the rows the grouping engine must process.
WHERE Filters Rows, HAVING Filters Groups
The correct version uses WHERE to remove individual rows early, then HAVING to filter the resulting groups. The wrong version uses HAVING for both — it forces all rows through the GROUP BY before filtering.
-- Goal: find departments where active employees' average salary exceeds 70 000
-- WRONG: using HAVING to filter non-aggregated conditions (status check)
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 70000
AND status = 'active'; -- ERROR or logical mistake: status is a row-level filter
-- In PostgreSQL this actually raises an error because status is not in GROUP BY.
-- CORRECT: WHERE eliminates inactive rows first, HAVING filters groups
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
WHERE status = 'active' -- row-level filter: runs BEFORE GROUP BY
GROUP BY department_id
HAVING AVG(salary) > 70000; -- group-level filter: runs AFTER GROUP BY
-- The WHERE version is also faster: fewer rows enter the GROUP BY.Combined WHERE + HAVING and Execution Order
WHERE and HAVING can coexist in the same query. Execution order: FROM → WHERE → GROUP BY → HAVING. Internalising this order prevents both correctness bugs and unnecessary performance overhead.
-- Find departments where the average salary of employees hired after 2019
-- exceeds the company-wide average salary.
SELECT
d.name AS department,
COUNT(e.id) AS headcount,
AVG(e.salary) AS avg_salary
FROM employees e
JOIN departments d ON d.id = e.department_id
WHERE e.hire_date > '2019-12-31' -- WHERE: filter rows before grouping
GROUP BY d.name
HAVING AVG(e.salary) > ( -- HAVING: filter groups after grouping
SELECT AVG(salary) FROM employees WHERE hire_date > '2019-12-31'
)
ORDER BY avg_salary DESC;
-- Execution order reminder (logical, not necessarily physical):
-- 1. FROM employees JOIN departments → combined row set
-- 2. WHERE hire_date > '2019-12-31' → rows eliminated early
-- 3. GROUP BY d.name → groups formed
-- 4. HAVING AVG(salary) > (subquery) → groups eliminated
-- 5. SELECT d.name, COUNT, AVG → projection
-- 6. ORDER BY avg_salary DESC → sort outputCommon Mistakes
Two frequent errors: trying to use a SELECT alias in HAVING (not valid in most databases), and using HAVING without GROUP BY to filter the single implicit group.
-- Mistake 1: referencing a SELECT alias in HAVING (invalid in most DBs)
-- because HAVING executes before SELECT in the logical order.
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
GROUP BY department_id
HAVING avg_sal > 70000; -- ERROR in PostgreSQL/MySQL strict mode
-- Fix: repeat the expression:
HAVING AVG(salary) > 70000;
-- Mistake 2: using WHERE to filter an aggregate (syntax error)
SELECT department_id, AVG(salary)
FROM employees
WHERE AVG(salary) > 70000 -- SYNTAX ERROR: aggregate not allowed in WHERE
GROUP BY department_id;
-- Acceptable: HAVING without GROUP BY (filters the single grand-total group)
SELECT COUNT(*), AVG(salary)
FROM employees
HAVING COUNT(*) > 100; -- returns the row only if total headcount > 100Key Points to Remember
- 1WHERE executes before GROUP BY (row-level filter); HAVING executes after GROUP BY (group-level filter).
- 2Aggregate functions (AVG, COUNT, SUM) are only valid in HAVING or SELECT — never in WHERE.
- 3Push all non-aggregate conditions to WHERE to reduce the number of rows the GROUP BY engine processes.
- 4Referencing a SELECT alias in HAVING is invalid in most databases because HAVING precedes SELECT in logical order.
- 5HAVING without GROUP BY filters the single implicit group formed by aggregating all rows.
- 6Logical execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY.
Interview Questions
Sign in to ask AriaWhat is the difference between WHERE and HAVING? Can you use aggregate functions in WHERE?
Write a query to find departments with more than 5 active employees and an average salary above 60 000.
Why is it more performant to filter on a non-aggregated column in WHERE rather than HAVING?
What is the logical execution order of a SQL SELECT statement and how does it explain the WHERE vs HAVING distinction?
Ask Aria about HAVING vs WHERE
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.