Aggregation & GROUP BY
Collapsing many rows into one summary row per group — and the one rule that decides what's allowed in SELECT.
Beginner
GROUP BY, every column in SELECT must be either (a) an aggregate function, or (b) a column listed in GROUP BY. Anything else is ambiguous — which row's value would it even show, out of a whole group? PostgreSQL enforces this at query-parse time; some engines (older MySQL) used to silently pick an arbitrary row instead, which is worse.SELECT category, COUNT(*), AVG(price)
FROM products
GROUP BY category;
-- fine: category is grouped, COUNT/AVG are aggregates
SELECT category, name, COUNT(*)
FROM products
GROUP BY category;
-- error: name is neither aggregated nor grouped
WHERE filters rows before grouping happens; HAVING filters groups after aggregation. This is why you can't write WHERE COUNT(*) > 5 — at the point WHERE runs, no aggregate has been computed yet.SELECT category, COUNT(*) AS n
FROM products
WHERE price > 0 -- row-level filter, applied first
GROUP BY category
HAVING COUNT(*) > 5; -- group-level filter, applied after grouping
| Function | Notes |
|---|---|
COUNT(*) | counts rows, including ones with NULL columns |
COUNT(col) | counts non-NULL values of col only |
COUNT(DISTINCT col) | counts distinct non-NULL values |
SUM, AVG, MIN, MAX | ignore NULLs automatically |
ARRAY_AGG(col) | collects values into an array |
STRING_AGG(col, sep) | concatenates values with a separator |
UNION works but is verbose; PostgreSQL can do it in one pass.SELECT region, product, SUM(amount)
FROM sales
GROUP BY ROLLUP (region, product);
-- produces: (region, product) subtotals, then (region, NULL) subtotals per region, then a (NULL, NULL) grand total
ROLLUP produces a hierarchical set of subtotals; CUBE produces every combination (not just hierarchical ones); GROUPING SETS lets you specify exactly which combinations you want. Worth knowing these exist — reach for them before hand-rolling a UNION ALL of near-identical queries.