Aggregation and grouping
Aggregates, nulls, WHERE against HAVING, ratios and subqueries.
Aggregate functions summarise many rows into one value: COUNT, SUM, AVG, MIN, MAX, and more such as STRING_AGG and BOOL_AND. With GROUP BY, they summarise each group instead of the whole table.
The rule of grouping
After GROUP BY, each output row is a group, so every column in SELECT must either be in the GROUP BY or be inside an aggregate. SELECT cause, reserve FROM claims GROUP BY cause fails: which of the claim's many reserves would it show?
Nulls in aggregates
COUNT(*)counts rows;COUNT(column)counts the rows where the column is not null.SUM,AVG,MINandMAXignore nulls; the sum of no rows is null, not 0: wrap it inCOALESCE(SUM(x), 0).
SELECT COUNT(*) AS policies, COUNT(broker_id) AS through_a_broker
FROM policies;
COUNT(broker_id) skips the 10,033 direct policies.
where or having
WHERE filters rows before grouping; HAVING filters groups after. 'Claims in 2025' is a WHERE; 'causes with more than 100 claims' is a HAVING, because the count exists only after grouping.
Watch GROUP BY and HAVING, row by row
SELECT cause, COUNT(*) AS claims, SUM(reserve) AS reserves
FROM claims
GROUP BY cause
HAVING COUNT(*) > 1
ORDER BY cause;
Groups
The final result as a table
| cause | COUNT(*) | SUM(reserve) | HAVING COUNT(*) > 1 |
|---|---|---|---|
| burglary | 2 | 1200 | kept |
| fire | 1 | 5000 | removed |
| storm | 3 | 5100 | kept |
| water damage | 1 | 700 | removed |
The result, sorted by ORDER BY: burglary, storm.
Ratios: sum first, then divide
A portfolio's loss ratio is total claims divided by total premium, not the average of each policy's ratio: the average of ratios gives a small policy with one large claim the same weight as a large one.
Subqueries
A subquery is a query inside another. It can return a single value (in WHERE reserve > (SELECT AVG(reserve) ...)), a list (after IN), or a table (in FROM). A correlated subquery refers to the outer row, and conceptually runs once per row.
SELECT cause, COUNT(*) AS claims
FROM claims
GROUP BY cause
HAVING COUNT(*) > 100
ORDER BY claims DESC;
HAVING keeps only the causes with more than 100 claims.
WHERE whenever the condition does not need an aggregate: fewer rows to group is less work, and HAVING is left for conditions on the groups.Check your understanding
Answer every question, then check.