Blog
3 min read

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

  1. FROM — get the rows
  2. WHERE — throw away rows that don't match
  3. GROUP BY — combine remaining rows into groups
  4. HAVING — throw away groups that don't match
  5. SELECT — calculate the output
  6. ORDER BY, LIMIT

(SQL GROUP BY explained)

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;
  • WHERE removes unpaid and old orders before adding them up.
  • HAVING keeps 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