35 HAVING
WHERE filters individual rows before grouping; HAVING filters groups after the aggregates have been computed. If you want “customers with more than three orders,” that is a HAVING — you cannot test count(*) > 3 until the count exists. If you want “customers who placed an order in 2024,” that is a WHERE — the year is a per-row property.
What you’ll learn
- Filtering groups with
HAVING - The difference between
WHERE(per row) andHAVING(per group) - Combining
WHEREandHAVINGin one query - Aggregates allowed inside
HAVING - Why pushing predicates into
WHEREis usually faster
Example
The execution order helps remember the difference: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. A row predicate written in WHERE makes its intent clear and can reduce the input to grouping; PostgreSQL can also push suitable HAVING predicates down. Push as much filtering as possible into WHERE.
HAVING lets you write aggregate predicates directly: HAVING avg(amount) > 100, HAVING count(DISTINCT product_id) > 5. These cannot appear in WHERE because the aggregates do not exist there.
The last query is technically valid but a smell: filtering on a non-aggregate in HAVING does the same as WHERE but later in the pipeline. Prefer WHERE for clarity; the planner can often push this grouped-column predicate down either way.
Run the example
To run (requires orders_example from the joins lesson):
Common pitfalls
- Writing
WHERE count(*) > 3— aggregates are not allowed inWHERE. UseHAVING. - Writing
HAVING amount > 100is valid only whenamountis grouped or functionally dependent on the grouping. UseWHERE amount > 100to filter individual input rows. The planner usually rewrites this, but readers will be confused. - Forgetting that
HAVINGruns afterGROUP BY, so any column referenced must be aGROUP BYcolumn or inside an aggregate. - Using
HAVINGinstead of aWHEREincount(*) FILTER (WHERE ...)-style logic.FILTERis per-aggregate and lives inSELECT;HAVINGis per-group and decides which groups are kept.
When you want “the top N groups by some aggregate,” combine GROUP BY + ORDER BY agg DESC + LIMIT N — no HAVING needed. HAVING is for predicates (“at least 3”), not rankings (“top 3”).
Change the threshold: HAVING sum(total) < 50 and see how the result set shifts. Then move the filter into WHERE: WHERE total > 20 GROUP BY customer_id; and observe that the count drops because fewer rows reach the grouping.
Downloads
SQL file: having.sql · All lesson files · Setup guide