Filtering Groups with HAVING
WHERE filters rows before grouping; HAVING filters groups after aggregating. Why analysts need both, and how to keep them straight.
You can summarize per group. Now you want to keep only the interesting
groups — "regions with revenue over $1000", "customers with more than 10
orders", "products averaging below 3 stars." That is HAVING: a
filter that runs after aggregation, on the group summaries themselves.
The hardest part is keeping it distinct from WHERE, so that is what this
page nails down.
Two filters, two moments
WHERE and HAVING both filter — but at different stages of the query.
WHEREruns before grouping. It filters individual rows, so it can only see raw columns, never group totals.HAVINGruns after grouping. It filters whole groups, so it can see aggregates likeSUM(amount).
That single diagram is the whole mental model. WHERE is upstream of the
groups; HAVING is downstream. A condition that mentions an aggregate
must go in HAVING, because the aggregate does not exist yet when
WHERE runs.
HAVING in action
"Which regions brought in more than $150 of revenue?" The condition is
about SUM(amount) — a group total — so it belongs in HAVING.
North (60) and any other small region are dropped after their totals
are computed. You cannot express this with WHERE, because at WHERE
time there is no per-region total to compare against.
Using WHERE and HAVING together
They are not rivals — analysts routinely use both in one query, each doing
its proper job: WHERE narrows the rows that enter the groups; HAVING
selects which resulting groups survive.
Read it as a pipeline: "Of the paid orders, group by region, and show
only regions whose paid revenue exceeds $150." The refunded rows never
enter the groups at all — and that changes the totals HAVING then
judges.
The classic mistake
Putting an aggregate in WHERE is the error everyone makes once:
-- ❌ Wrong: SUM() does not exist yet when WHERE runs
SELECT region, SUM(amount)
FROM orders
WHERE SUM(amount) > 150
GROUP BY region;DuckDB rejects this because WHERE filters rows before any grouping or
summing happens. Move the aggregate condition to HAVING and it works.
The reverse mistake — putting a plain row condition in HAVING — usually
runs but is wasteful: filter rows in WHERE so fewer rows ever reach the
grouping step.
A quick decision rule
Ask: does my condition mention an aggregate?
- No (it is about a single row's column) →
WHERE. - Yes (it is about a group total/average/count) →
HAVING.
Check your understanding
What is the key difference between WHERE and HAVING?
WHERE is for numbers and HAVING is for text.
WHERE filters individual rows before grouping; HAVING filters groups after aggregates are computed.
HAVING runs before WHERE.
They are interchangeable synonyms.
Why does WHERE SUM(amount) > 150 fail?
SUM is not a valid function.
WHERE runs before grouping, so per-group sums do not exist yet — aggregate conditions belong in HAVING.
WHERE cannot compare numbers with >.
You must always use SUM in WHERE.
A query keeps only paid orders, groups by region, and keeps regions with revenue over $1000. Where does each condition go?
Both conditions go in HAVING.
Both conditions go in WHERE.
status = 'paid' goes in WHERE; SUM(amount) > 1000 goes in HAVING.
The query is impossible in SQL.
You have now mastered summarization: aggregates, grouping, multi-level subtotals, and group filtering. Next comes a different superpower — window functions, which summarize without collapsing the rows.
GROUP BY ALL and Grouping Sets
DuckDB conveniences that make aggregation faster to write and more powerful — GROUP BY ALL, plus ROLLUP, CUBE, and GROUPING SETS for subtotals and multi-level summaries.
Why Window Functions Exist
The problem GROUP BY cannot solve — keeping every row while also seeing a summary. Window functions explained from the ground up, with the OVER clause demystified.