Lesson 3 of 8 · 48 min
Aggregations & metric definitions
GROUP BY discipline, FILTER/CASE aggregates, distinct metrics, weighted ratios, ROLLUP awareness, and date spines.
Lesson 3 · Aggregations
GROUP BY, HAVING, and metrics that survive review
Aggregation is where product truth dies
GROUP BY rules and functional dependency
1-- Invalid / nondeterministic pattern2SELECT country, name, COUNT(*) -- name not determined by country3FROM users4GROUP BY country;56-- Valid: metric at country grain7SELECT country, COUNT(*) AS n_users8FROM users9GROUP BY country;1011-- Valid: attributes of the group key (user_id → name)12SELECT u.user_id, u.name, COUNT(o.order_id) AS n_orders13FROM users u14LEFT JOIN orders o ON o.user_id = u.user_id15GROUP BY u.user_id, u.name;Filtered aggregates and pivot-style metrics
1SELECT2 user_id,3 COUNT(*) AS orders_all,4 COUNT(*) FILTER (WHERE status = 'paid') AS orders_paid,5 COUNT(*) FILTER (WHERE status = 'refund') AS orders_refunded,6 SUM(amount) FILTER (WHERE status = 'paid') AS gmv_paid,7 AVG(amount) FILTER (WHERE status = 'paid') AS aov_paid8FROM orders9GROUP BY user_id;1011-- Portable CASE form12SUM(CASE WHEN status = 'paid' THEN amount END) AS gmv_paid,13COUNT(CASE WHEN status = 'paid' THEN 1 END) AS orders_paidKey idea
Ratios, averages of ratios, and weighted mistakes
1-- Wrong: unweighted mean of daily rates2SELECT AVG(daily_cvr) FROM (3 SELECT day, SUM(converts)::float / SUM(visits) AS daily_cvr4 FROM funnel5 GROUP BY day6) d;78-- Right: overall rate (visits-weighted)9SELECT SUM(converts)::float / NULLIF(SUM(visits), 0) AS cvr10FROM funnel;1112-- Per-segment rates then compare - still compute each as ratio of sumsCommon mistake
AVG(price) after a join to a dimension is always the right 'average selling price'.
DISTINCT counts and approximate cardinality
1-- DAU / WAU style2SELECT event_date, COUNT(DISTINCT user_id) AS dau3FROM events4WHERE event_name = 'app_open'5GROUP BY event_date;67-- Multi-condition uniques8COUNT(DISTINCT CASE WHEN event_name = 'purchase' THEN user_id END) AS purchasersHAVING, QUALIFY cousins, and post-aggregate filters
1-- Users with ≥3 paid orders and paid GMV ≥ 1002SELECT user_id,3 COUNT(*) AS paid_orders,4 SUM(amount) AS paid_gmv5FROM orders6WHERE status = 'paid'7GROUP BY user_id8HAVING COUNT(*) >= 39 AND SUM(amount) >= 100;GROUPING SETS, ROLLUP, CUBE (interview awareness)
1SELECT country, device, SUM(revenue) AS rev2FROM sales3GROUP BY ROLLUP (country, device);4-- rows for (country, device), (country, total), (grand total)56SELECT7 country,8 device,9 GROUPING(country) AS g_country, -- 1 means rolled up10 SUM(revenue) AS rev11FROM sales12GROUP BY ROLLUP (country, device);Time buckets and reporting calendars
1WITH days AS (2 SELECT generate_series(3 DATE '2026-01-01', DATE '2026-01-31', INTERVAL '1 day'4 )::date AS d5),6daily AS (7 SELECT created_at::date AS d, SUM(amount) AS gmv8 FROM orders9 WHERE status = 'paid'10 GROUP BY 111)12SELECT days.d, COALESCE(daily.gmv, 0) AS gmv13FROM days14LEFT JOIN daily ON daily.d = days.d15ORDER BY days.d;Common mistake
If the charting tool 'fills zeros', SQL can skip the date spine.
- 01"GROUP BY rule?" → non-aggregated select cols must be group keys (or FD on them); when unsure, group them.
- 02"HAVING vs WHERE?" → WHERE row filters pre-agg; HAVING group filters post-agg.
- 03"Average of ratios?" → usually wrong; use sum(num)/sum(den) for overall rates.
- 04"COUNT DISTINCT nulls?" → nulls ignored in COUNT(DISTINCT col).
- 05"FILTER clause?" → COUNT(*) FILTER (WHERE cond) for partial aggregates in one pass.
- 06"Why date spine?" → zero-activity days must appear for time series correctness.
- 07"Weighted ASP?" → define weight: rows vs units vs revenue; AVG alone is row-weighted.
- 08"ROLLUP use?" → subtotals in one query; use GROUPING() to tell 'all' from NULL dimension.
- 09"Metric definition first?" → yes - paid vs all, tax in/out, refund policy - then code.
- 10"Double count in agg?" → still usually a join fan-out before the GROUP BY.
Checkpoint
Overall conversion across 30 days with very different daily traffic - best CVR?
Checkpoint
COUNT(CASE WHEN status='paid' THEN 0 END) - what does it count?
Checkpoint
You SELECT user_id, country, COUNT(*) FROM orders JOIN users GROUP BY user_id - engine complains or is nondeterministic about country. Why?
Checkpoint
Daily revenue chart shows gaps on quiet days. Interviewer wants zeros. Fix?
Checkpoint
Metric: 'paying users' = users with at least one paid order in-period. Best expression?
Can you define a metric, pick grain, write GROUP BY/HAVING/FILTER, and avoid average-of-averages mistakes live?
Takeaways
- Restate metric definition and grain before aggregating.
- GROUP BY must cover non-aggregated select expressions.
- Prefer FILTER/CASE aggregates; avoid COUNT(CASE THEN 0).
- Overall rates = sum/sum, not avg of rates.
- Date spines for zero-filled time series.
Next: window functions - ranking, running totals, and framing traps that separate seniors.
Sources
Free to read · better with Enzo
Learn it with Enzo
Save your progress, answer the checkpoints, and let Enzo quiz you on what you just read.