SQL theory · page 4 of 10

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, MIN and MAX ignore nulls; the sum of no rows is null, not 0: wrap it in COALESCE(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. Try it in the playground

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;
In the playground, WITH puts these few rows in place of the real tables, so PostgreSQL answers on exactly the rows shown here. Try it in the playground
Playback speed

Groups

The final result as a table
Every group, its aggregates and what HAVING does with it
cause COUNT(*) SUM(reserve) HAVING COUNT(*) > 1
burglary21200kept
fire15000removed
storm35100kept
water damage1700removed

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.

\text{loss ratio} = \frac{\sum \text{incurred}}{\sum \text{earned premium}} \neq \frac{1}{n} \sum \frac{\text{incurred}_i}{\text{premium}_i}

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. Try it in the playground
Filter rows with 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.
Practise it on Day 4

Check your understanding

Answer every question, then check.

1. What is the difference between COUNT(*) and COUNT(broker_id)?

2. Which filters groups after aggregation?

3. How is a portfolio's loss ratio computed?

An unhandled error has occurred. Reload 🗙

Reconnecting…

Your connection to Data Analytics SQL Lab dropped, probably a Wi-Fi blip. Hold on while it reconnects. Your work is kept.

The connection could not be restored. The page will reload in a few seconds.