Aggregation and grouping
Aggregates and GROUP BY
The idea in 30 seconds
Aggregate functions such as count, sum and avg turn many rows into one value. GROUP BY splits the rows into groups first and computes the aggregates once per group. Two rules follow: every column you select must be grouped or aggregated, and aggregates other than count(*) skip NULLs.
Helps to know first: Joins and relational querying
On this page
Many rows into one value
An aggregate function reads many rows and returns one value:
SELECT min(price), max(price), round(avg(price), 2) AS avg_price, sum(price) FROM products;
min | max | avg_price | sum
--------+----------+-----------+----------
499.00 | 18999.00 | 6481.09 | 71292.00
count, and what it skips
count comes in three forms, and they answer different questions:
SELECT count(*) AS customers, count(city) AS with_city, count(DISTINCT city) AS distinct_cities
FROM customers;
customers | with_city | distinct_cities
-----------+-----------+-----------------
8 | 6 | 5
count(*)counts rows: 8 customers.count(city)counts rows wherecityis not NULL: Elena and Hugo are skipped, so 6.count(DISTINCT city)counts different non-NULL values: Pune appears twice, so 5.
Every other aggregate (sum, avg, min, max) also ignores NULLs. avg(city_population) would be the average of the known values, not of every row.
GROUP BY: one result per group
GROUP BY splits the rows into groups that share a value, and computes the aggregates once per group:
SELECT category, count(*) AS products, min(price), max(price)
FROM products
GROUP BY category
ORDER BY products DESC, category;
category | products | min | max
-------------+----------+----------+----------
Accessories | 5 | 499.00 | 4500.00
Furniture | 2 | 2199.00 | 14500.00
Audio | 1 | 12999.00 | 12999.00
…
Grouping fits into the evaluation order between WHERE and SELECT:
| Step | Clause |
|---|---|
| 1 | FROM and joins |
| 2 | WHERE: filter rows |
| 3 | GROUP BY: form groups, compute aggregates |
| 4 | HAVING: filter groups (next page) |
| 5 | SELECT: compute output columns |
| 6 | ORDER BY, then LIMIT |
Aggregates work across joins too. The three largest orders by value:
SELECT o.id AS order_id, sum(oi.quantity * oi.unit_price) AS order_total
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id
ORDER BY order_total DESC
LIMIT 3;
order_id | order_total
----------+-------------
102 | 37998.00
109 | 23499.00
105 | 18898.00
The grouping rule
PredictA column that is neither grouped nor aggregated
What does this return?
SELECT category, name, count(*) FROM products GROUP BY category;An error:
ERROR: column "products.name" must appear in the GROUP BY clause or be used in an aggregate functionAfter grouping, each output row stands for a whole group. Accessories is one row standing for five products, so “the” name of the group does not exist. A selected column must either define the group (GROUP BY category) or be summarized (count(name), string_agg(name, ', ')).
The sum of nothing
SELECT sum(price) FROM products WHERE price > 100000; -- NULL, not 0
SELECT count(*) FROM products WHERE price > 100000; -- 0
Except for count, aggregates return NULL when there are no rows. A report showing “revenue: NULL” or code that crashes on a missing number usually comes from this. Use coalesce(sum(price), 0) when you want zero.
Check yourself
What is the difference between
count(*)andcount(city)?Show answer
count(*)counts all rows.count(city)counts only the rows wherecityis not NULL.Why is
SELECT category, name, count(*) FROM products GROUP BY categoryan error?Show answer
Each output row represents a whole category, which has many names. A selected column must be in
GROUP BYor inside an aggregate.What does
sum(amount)return when no rows match, and how do you get 0 instead?Show answer
NULL. Wrap it:
coalesce(sum(amount), 0).
Finished reading?
Saved in this browser only.
Sources
Where each claim on this page comes from.
- 1.PostgreSQL documentation: 9.21 Aggregate FunctionsPrimary· §9.21 Aggregate Functions
count(*) counts input rows; count(expression) counts rows where the value is not null; except for count, aggregates return null when no rows are selected, so sum of no rows is null; coalesce can substitute zero.
- 2.PostgreSQL documentation: 7.2 Table ExpressionsPrimary· §7.2.3 The GROUP BY and HAVING Clauses
When a table is grouped, columns not listed in GROUP BY can be referenced only inside aggregate expressions.
- 3.PostgreSQL documentation: SELECTPrimary· Description
GROUP BY and aggregates are computed after WHERE and before the SELECT output list.
Go further
What to read next, and why it is worth the time.
- 1.PostgreSQL documentation: 9.21 Aggregate FunctionsPrimary· §9.21, Table 9.62
Every built-in aggregate. Skim it once: string_agg, bool_and and percentile_cont solve problems people often write loops for.
Applies to PostgreSQL 18. Draft: not yet fact-checked end to end.