WHERE vs HAVING in SQL: What's the Difference?
WHERE filters individual rows before they're grouped; HAVING filters groups after GROUP BY. Examples of each, why you can't use COUNT() in WHERE, using both in one query, and the performance reason to filter in WHERE whenever you can.
Both WHERE and HAVING filter results. The difference is when they filter:
- WHERE filters rows, before grouping.
- HAVING filters groups, after
GROUP BY.
That's it. Everything else follows from the order SQL runs a query in.
The order SQL runs in
FROM— get the rowsWHERE— throw away rows that don't matchGROUP BY— combine remaining rows into groupsHAVING— throw away groups that don't matchSELECT— calculate the outputORDER BY,LIMIT
WHERE: filter rows
SELECT *
FROM orders
WHERE status = 'paid' AND total > 20;
Each row is checked on its own. Works with or without GROUP BY.
HAVING: filter groups
"Customers who have placed more than 3 orders":
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 3;
You can't know a customer's order count until the rows are grouped — so this condition has to go in HAVING.
Why COUNT() doesn't work in WHERE
SELECT customer_id, COUNT(*)
FROM orders
WHERE COUNT(*) > 3 -- ERROR
GROUP BY customer_id;
ERROR: aggregate functions are not allowed in WHERE
At the WHERE step, groups don't exist yet, so there's nothing to count. Aggregates (COUNT, SUM, AVG, MIN, MAX) belong in HAVING.
Using both together
"Customers who spent more than $100 on paid orders this year":
SELECT customer_id, SUM(total) AS spent
FROM orders
WHERE status = 'paid'
AND created_at >= '2026-01-01'
GROUP BY customer_id
HAVING SUM(total) > 100
ORDER BY spent DESC;
WHEREremoves unpaid and old orders before adding them up.HAVINGkeeps only customers whose total passes $100.
Put them in the wrong place and you get a different answer — for example, filtering status in HAVING would require it to be grouped and change what's being summed.
Filter in WHERE whenever you can
You can write non-aggregate conditions in HAVING:
SELECT customer_id, COUNT(*)
FROM orders
GROUP BY customer_id
HAVING customer_id = 42; -- works, but wasteful
But that groups every customer first and then discards all but one. WHERE customer_id = 42 filters first, so there's far less to group — and it can use an index. On big tables the difference is large. (Database indexes)
Rule of thumb: row conditions → WHERE; conditions on aggregates → HAVING.
Quick reference
| WHERE | HAVING | |
|---|---|---|
| Filters | Rows | Groups |
| Runs | Before GROUP BY |
After GROUP BY |
Can use COUNT, SUM… |
No | Yes |
Needs GROUP BY |
No | Usually (otherwise the whole result is one group) |
| Can use indexes | Yes | Not for the aggregate condition |
EasySpawn gives each app its own Postgres database and a terminal with Claude Code, which can write — and explain — queries like these against your real data. See how it works or join the waitlist.
Related: SQL GROUP BY Explained · SQL for Beginners · SQL Joins Explained · Postgres Window Functions
Keep reading
What Is PostgreSQL? The Database Behind Most New Apps, Explained
PostgreSQL — usually just Postgres — is a free, open-source relational database known for reliability and features. What it is, why AI app builders and platforms like Supabase and Neon are built on it, what it's good at, how to connect to it, and where to run it.
What Is MongoDB? A Beginner's Guide to Document Databases
MongoDB stores data as flexible JSON-like documents instead of tables. How it works, what collections and documents are, where it shines, where Postgres is the better pick, and why AI tools sometimes reach for it.