Compare

WHERE vs HAVING

Short answer: WHERE filters rows before grouping; HAVING filters groups after grouping. Use WHERE for conditions on raw columns, HAVING for conditions on aggregates (SUM, COUNT, AVG, etc.). The same filter expression cannot live in both places: WHERE doesn't see aggregates, and HAVING runs too late to filter individual rows.

Quick comparison #

WHEREHAVING
Runsbefore GROUP BYafter GROUP BY
Seesindividual rowsgrouped rows + aggregates
Can reference aggregates?noyes
Can reference non-grouped columns?yesonly via aggregates
Use for"exclude rows where status = 'cancelled'""exclude regions where SUM(total) < 10000"

Same query, both filters #

Loading SQL editor...

When to use which #

WHERE is for anything that can be evaluated on a single row. WHERE status = 'paid', WHERE created_at >= DATE '2026-01-01', WHERE region IN ('EU', 'US'). Filtering early is also a performance win, since fewer rows enter the GROUP BY.

HAVING is for anything that depends on an aggregate. HAVING COUNT(*) > 5, HAVING SUM(total) > 10000, HAVING AVG(rating) >= 4.5. There is no other place in the query where these conditions are legal.

Performance: WHERE is essentially always cheaper than HAVING for the same logical condition, because it shrinks the row set before the (often expensive) grouping. If a condition can live in WHERE, put it there.

Common mistakes #

  • Putting a row-level filter in HAVING: works, but wastes work. GROUP BY ... HAVING status = 'paid' groups every row first, then throws away the cancelled ones. Move it to WHERE.
  • Trying to filter aggregates in WHERE: hard error. WHERE SUM(total) > 100 doesn't compile, because WHERE runs before SUM is computed.
  • Referencing the SELECT alias in HAVING: works in MySQL and SQLite, fails in standard ANSI SQL and Postgres. Re-state the aggregate (HAVING SUM(total) > 100) instead of the alias (HAVING lifetime_spend > 100) for portability.

FAQ #

What's the main difference between WHERE and HAVING?

WHERE filters individual rows before grouping. HAVING filters groups after aggregation. WHERE cannot reference aggregates (the aggregates haven't been computed yet). HAVING can.

Can I use WHERE and HAVING in the same query?

Yes, and it's common. WHERE filters rows entering the grouping (e.g., only paid orders); HAVING filters the resulting groups (e.g., regions with > $10K revenue). Each filter goes in its right place.

Is HAVING slower than WHERE?

For row-level conditions: yes. Putting a row filter in HAVING forces the engine to group every row first, then drop the unwanted ones. WHERE filters before grouping, shrinking the set early. For aggregate-based conditions, HAVING is the only option; there's no WHERE equivalent.

Can HAVING reference SELECT aliases?

On MySQL and SQLite: yes. On Postgres and standard ANSI SQL: no, so repeat the aggregate expression. For portability, repeat: HAVING SUM(total) > 1000 (not HAVING revenue > 1000).

Can I use HAVING without GROUP BY?

Yes, but only with aggregate filters that operate on the whole table. SELECT COUNT() FROM orders HAVING COUNT() > 100 returns a row only if there are more than 100 orders total. Rare in practice; usually GROUP BY accompanies HAVING.

Why does WHERE COUNT(*) > 5 fail?

COUNT() is an aggregate. WHERE runs before aggregation, so the count doesn't exist yet. Move the predicate to HAVING: GROUP BY user_id HAVING COUNT() > 5.

Tip

The shape of an analytics query is almost always: WHERE for slice, GROUP BY for grain, HAVING for thresholds, ORDER BY for presentation. Use that order as a checklist when writing one. If a filter feels like it could go in either WHERE or HAVING, ask: "does it reference an aggregate?" If yes, HAVING; if no, WHERE. Performance follows the rule for free.