Postgres by Example: HAVING
June 22, 2026 · View on GitHub
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
-- HAVING with one aggregate condition
SELECT customer_id, count(*) AS order_count, sum(total) AS total_spent
FROM orders_example
GROUP BY customer_id
HAVING sum(total) > 30
ORDER BY customer_id;
-- HAVING with multiple conditions
SELECT customer_id, count(*) AS order_count, sum(total) AS total_spent
FROM orders_example
GROUP BY customer_id
HAVING count(*) >= 1 AND sum(total) > 30;
-- WHERE then HAVING: filter rows first, group, then filter groups
SELECT customer_id, count(*) AS order_count
FROM orders_example
WHERE total > 20 -- per-row filter
GROUP BY customer_id
HAVING count(*) >= 1; -- per-group filter
-- HAVING without aggregates is legal but pointless — same as WHERE
SELECT customer_id
FROM orders_example
GROUP BY customer_id
HAVING customer_id > 50; -- could just be WHERE
The execution order helps remember the difference: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Anything you can express in WHERE is cheaper than HAVING because it reduces the number of rows that have to be grouped. 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. Move it to WHERE so the planner can use indexes.
To run (requires orders_example from the joins lesson):
$ psql -f source/having.sql postgres
customer_id | order_count | total_spent
-------------+-------------+-------------
1 | 2 | 80.00
(1 row)
...
Common pitfalls:
- Writing
WHERE count(*) > 3— aggregates are not allowed inWHERE. UseHAVING. - Writing
HAVING amount > 100whenamountis a row-level column — it works but should beWHERE amount > 100. 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.
Tip: 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").
Try it: 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.
Source: having.sql
Next: Scalar and IN Subqueries
Home: Postgres by Example