Aggregations and GROUP BY
Collapse rows without collapsing their meaning
GROUP BY collapses rows into groups and aggregate functions summarize them. HAVING filters groups, and the grain of the group defines the meaning of every aggregate.
What you will learn
- Write correct GROUP BY + HAVING queries
- Explain why SELECT columns must be grouped or aggregated
- Use ROLLUP/CUBE for hierarchical totals
What is this?
GROUP BY splits rows into groups sharing the listed columns; aggregate functions (COUNT, SUM, AVG, MIN, MAX) produce one row per group. HAVING filters whole groups after aggregation. The grouping columns define the grain of the output — the single most important thing about an aggregate query.
Why do you need it?
- Every dashboard metric is a GROUP BY query; getting the grain wrong mislabels the number it reports.
- HAVING vs WHERE is a common confusion that yields seemingly-anomalous results.
- ROLLUP/CUBE add subtotal rows that reports and expense cubes need.
Mental model
Sorting coins into jars by denomination
GROUP BY
Each jar holds one denomination (a group); the count in each jar is the aggregate. A jar's label (the grouping column) IS the grain — if two jars share a label, your grouping was wrong.
How it works
- Grouping keys must appear in SELECT either verbatim or as aggregates.
- Aggregates (SUM/COUNT/AVG) compute per group; COUNT(*) counts rows, COUNT(col) counts non-NULL values.
- HAVING filters groups after aggregation; WHERE filtered rows before grouping.
- GROUP BY a, b: unique (a, b) pairs become groups — grain is the pair.
- ROLLUP (a, b) adds (a, total) and (grand-total) rows; CUBE adds all combinations.
WHERE vs HAVING in the query pipeline.
Examples
Grouped revenue with a group filter
SELECT region, SUM(amount) AS revenueFROM salesWHERE ts >= '2026-01-01'GROUP BY regionHAVING SUM(amount) > 100ORDER BY revenue DESC;WHERE trims pre-group rows; HAVING drops regions whose post-group total is small.
In production
- A daily KPI table computes revenue by channel/day with GROUP BY channel, day — the grain a dashboard trusts.
- A sales cube builds ROLLUP rows so BI can pivot to region and grand totals without extra queries.
- A quality gate counts DISTINCT keys after an upsert to prove no duplicates creep in.
Best practices
- State the grouping grain in the model's documentation before writing the query.
- Use HAVING for group-level filters; keep row filters in WHERE for performance.
- COUNT(DISTINCT key) to assert uniqueness; COUNT(*) to assert row volume.
- Prefer SUM on numeric typed columns; SUM over text or NULL-rich data misleads.
Common mistakes
- SELECTing a non-grouped, non-aggregated column — allowed in some dialects, silently wrong data in all.
- Filtering groups in WHERE instead of HAVING, so the aggregates are already wrong before they reach the report.
- Mixing COUNT(*) with COUNT(col): dropping NULLs undercounts.
Common issues
Dashboard total differs from source sumA grouping grain (e.g. by day-hour while source is by-minute) collapsed differently
Align the reporting grain with the fact's grain or document the intended rollup.
HAVING effectively didn't filterFilter applied to a NULL-laden COALESCE that passes despite the spirit
Filter on the raw column, then COALESCE only in SELECT.
Debugging tips
- When a total doesn't reconcile, re-read the grouping keys and add the missing grain.
- Run a COUNT(*) per group to sanity-check group sizes before trusting aggregates.
- If HAVING seems to ignore rules, confirm you aren't masking with COALESCE in the same expression.
Interview checkpoint
Hands-on lab
- Aggregate sales into a daily-by-channel KPI table
- Use HAVING to drop under-performing groups
- Add ROLLUP subtotal rows
Build a Daily KPI Table with Correct Grain
25 min · intermediate
Quick quiz
SELECT region, SUM(amount) FROM sales GROUP BY region — what is the row grain?
HAVING can reference an aggregate like SUM(amount); WHERE cannot.