HAVING vs WHERE in SQL: What's the Difference?
Both filter rows, but at different stages. WHERE filters rows before grouping; HAVING filters groups after. Learn exactly when to use each, why it matters, and the order SQL runs in.
WHERE and HAVING both filter, which is why they get confused. The difference is timing: WHERE filters individual rows before they are grouped, and HAVING filters the groups after aggregation. Get this right and a whole class of "aggregate function not allowed here" errors disappears. Keep the SQL cheat sheet open as you read.
The Short Answer
Use WHERE to filter rows on their raw column values. Use HAVING to filter groups on an aggregate like COUNT, SUM, or AVG. If your condition mentions an aggregate function, it belongs in HAVING; otherwise it belongs in WHERE.
The Order SQL Runs In
SQL clauses are written in one order but executed in another. The logical order is: FROM, then WHERE, then GROUP BY, then HAVING, then SELECT, then ORDER BY. Because WHERE comes before GROUP BY, it filters the raw rows; because HAVING comes after, it filters the assembled groups.
-- Written order vs execution order
SELECT status, COUNT(*) -- 5. select
FROM orders -- 1. from
WHERE total > 0 -- 2. where (rows)
GROUP BY status -- 3. group by
HAVING COUNT(*) > 10 -- 4. having (groups)
ORDER BY COUNT(*) DESC; -- 6. order by
Where each filter runs — WHERE filters individual rows before GROUP BY — so it cannot reference aggregates. HAVING filters the assembled groups after, so it can.
WHERE: Filtering Rows
WHERE looks at one row at a time and decides whether it survives into the grouping step. It can only reference the columns of that row, never an aggregate.
-- Exclude cancelled orders before counting anything
SELECT user_id, COUNT(*) AS order_count
FROM orders
WHERE status <> 'cancelled'
GROUP BY user_id;
HAVING: Filtering Groups
HAVING runs after GROUP BY has built each group and computed its aggregates. That is the only place you can filter on a SUM or COUNT.
-- Only customers with more than 5 orders
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;
Using Both Together
Most real reports use both. WHERE narrows the rows first (which also makes the query faster, since fewer rows reach the grouping step), then HAVING keeps only the groups you care about.
-- High-value customers in 2026 only
SELECT user_id, SUM(total) AS lifetime_value
FROM orders
WHERE created_at >= '2026-01-01' -- filter rows first
GROUP BY user_id
HAVING SUM(total) > 1000 -- then filter groups
ORDER BY lifetime_value DESC;
Common Mistakes & Tips
- Putting an aggregate condition in WHERE — it must go in HAVING
- Putting a plain row filter in HAVING that could have run in WHERE — slower, because it filters too late
- Forgetting that WHERE runs before GROUP BY, so aggregates do not exist yet
- Referencing a SELECT alias in HAVING (works in PostgreSQL, but not all databases) — repeat the aggregate to be safe
- Assuming HAVING needs GROUP BY — it can filter a single whole-table aggregate too
The WHERE-then-HAVING pattern is everywhere in analytics. Practice it on real e-commerce and banking data until the order feels obvious.