Aggregation
Grouping rows with GROUP BY
In the previous chapter, you used aggregate functions to summarize rows.
For example:
12SELECT COUNT(*) AS product_countFROM products;This returns one count for the entire products table:
123 product_count--------------- 5000But suppose we don't want one count for all products.
We want to know:
How many products are in each category?
For this, we use GROUP BY.
Seeing the categories
Run:
123456SELECT id, categoryFROM productsWHERE id <= 10ORDER BY id;You should see:
123456789101112 id | category----+---------- 1 | home 2 | outdoor 3 | office 4 | garden 5 | audio 6 | home 7 | outdoor 8 | office 9 | garden 10 | audioThe categories repeat across different rows:
12345homeoutdoorofficegardenaudioInstead of treating all products as one set, we can divide them into groups based on their category.
GROUP BY
Run:
123456SELECT category, COUNT(*) AS product_countFROM productsGROUP BY categoryORDER BY category;You should see:
1234567 category | product_count----------+--------------- audio | 1000 garden | 1000 home | 1000 office | 1000 outdoor | 1000PostgreSQL divided the rows according to category.
Then it ran:
1COUNT(*)separately for each group.
Conceptually:
12345678910products │ ▼GROUP BY category │ ├── audio ─────▶ COUNT(*) = 1000 ├── garden ────▶ COUNT(*) = 1000 ├── home ──────▶ COUNT(*) = 1000 ├── office ────▶ COUNT(*) = 1000 └── outdoor ───▶ COUNT(*) = 1000Individual rows fall into buckets, and each bucket produces one result row:
1234567891011product rows buckets resultProduct 1 home ────┐Product 6 home ────┤──▶ home ────────▶ home | 1000Product 11 home ────┘Product 3 office ───┐Product 8 office ───┤──▶ office ────────▶ office | 1000Product 13 office ───┘...GROUP BY produces one result per group
Without grouping:
12SELECT COUNT(*)FROM products;we get one summary:
15000With:
1GROUP BY categorywe get one summary for every distinct category.
A useful mental model is:
12345aggregate without GROUP BY→ one group containing all selected rowsaggregate with GROUP BY→ one group for each distinct grouping valueGrouping orders by status
The same idea works with our orders table.
Run:
123456SELECT status, COUNT(*) AS order_countFROM ordersGROUP BY statusORDER BY order_count DESC;You should see:
1234567 status | order_count------------+------------- delivered | 70000 shipped | 15000 processing | 8000 cancelled | 5000 pending | 2000Instead of counting all 100000 orders together, PostgreSQL calculates a separate count for each status.
GROUP BY works with other aggregates
We are not limited to COUNT().
For example:
12345678910SELECT status, COUNT(*) AS order_count, SUM(total_amount) AS total_amount, ROUND(AVG(total_amount), 2) AS average_amount, MIN(total_amount) AS minimum_amount, MAX(total_amount) AS maximum_amountFROM ordersGROUP BY statusORDER BY status;You should see:
1234567 status | order_count | total_amount | average_amount | minimum_amount | maximum_amount------------+-------------+--------------+----------------+----------------+---------------- cancelled | 5000 | 2062750.00 | 412.55 | 10.93 | 899.97 delivered | 70000 | 28836260.00 | 411.95 | 10.00 | 899.69 pending | 2000 | 825170.00 | 412.59 | 10.98 | 899.99 processing | 8000 | 3299880.00 | 412.49 | 10.85 | 899.92 shipped | 15000 | 6185550.00 | 412.37 | 10.70 | 899.84Each aggregate is calculated independently for every status group.
Conceptually:
1234567891011121314151617delivered orders │ ├── COUNT ├── SUM ├── AVG ├── MIN └── MAXshipped orders │ ├── COUNT ├── SUM ├── AVG ├── MIN └── MAX...Selected columns and GROUP BY
Consider:
12345SELECT status, COUNT(*)FROM ordersGROUP BY status;status can appear in the SELECT list because it is the grouping column.
Within each group, every row has the same status.
For example:
123456processing groupstatus = processingstatus = processingstatus = processing...so PostgreSQL knows which value to return for the group.
Now consider:
123456SELECT status, total_amount, COUNT(*)FROM ordersGROUP BY status;PostgreSQL rejects this:
1ERROR: column "orders.total_amount" must appear in the GROUP BY clause or be used in an aggregate functionFor a status such as delivered, there are many different total_amount values.
PostgreSQL cannot choose one arbitrary value to represent the whole group.
The general rule is:
A selected value in a grouped query normally needs to either be part of the grouping or be calculated by an aggregate function.
For example:
12345SELECT status, MAX(total_amount)FROM ordersGROUP BY status;is valid because MAX() reduces all the different amounts in each group to one value.
A PostgreSQL functional-dependency exception
There is an important PostgreSQL exception to the general rule.
If the grouped columns include a table's primary key, PostgreSQL knows that other columns from that same table are determined by that primary key.
For example:
12345678SELECT id, full_name, emailFROM customersGROUP BY idORDER BY idLIMIT 3;is allowed because:
1customers.idis the primary key.
You should see:
12345 id | full_name | email----+------------+----------------------- 1 | Customer 1 | Customer1@Example.com 2 | Customer 2 | Customer2@Example.com 3 | Customer 3 | Customer3@Example.comA particular id identifies exactly one customer, so full_name and email are determined by it.
You do not need this exception for most aggregation queries, but it explains why PostgreSQL may accept some queries that appear to select columns not explicitly listed in GROUP BY.
Grouping by multiple columns
A group can be defined by more than one column.
For example, suppose we want an order count for each status in each year.
Run:
1234567891011SELECT EXTRACT(YEAR FROM placed_at)::int AS placed_year, status, COUNT(*) AS order_countFROM ordersGROUP BY placed_year, statusORDER BY placed_year, status;You should see:
12345678910111213141516171819202122 placed_year | status | order_count-------------+------------+------------- 2021 | cancelled | 1240 2021 | delivered | 17674 2021 | pending | 496 2021 | processing | 1984 2021 | shipped | 3790 2022 | cancelled | 1245 2022 | delivered | 17636 2022 | pending | 498 2022 | processing | 2062 2022 | shipped | 3735 2023 | cancelled | 1253 2023 | delivered | 17335 2023 | pending | 490 2023 | processing | 2002 2023 | shipped | 3740 2024 | cancelled | 1262 2024 | delivered | 17355 2024 | pending | 516 2024 | processing | 1952 2024 | shipped | 3735PostgreSQL created groups based on each distinct combination:
12345672021 + delivered2021 + shipped2021 + processing2022 + delivered2022 + shipped...A row belongs to the group defined by both values.
Four years and five statuses produce twenty groups, so the query returns twenty rows.
WHERE happens before grouping
Suppose we want to count only orders that are not delivered.
Run:
1234567SELECT status, COUNT(*) AS order_countFROM ordersWHERE status <> 'delivered'GROUP BY statusORDER BY order_count DESC;You should see:
123456 status | order_count------------+------------- shipped | 15000 processing | 8000 cancelled | 5000 pending | 2000The logical flow is:
12345678910111213orders │ ▼WHERE status <> 'delivered' │ ▼remaining rows │ ▼GROUP BY status │ ▼COUNT each groupWHERE decides which individual rows are available to be grouped.
Then GROUP BY divides the remaining rows.
Then aggregate functions calculate values for those groups.
Notice there is no delivered row at all in the result. The group was never formed, because no delivered row survived WHERE.
GROUP BY does not sort the result
Consider:
12345SELECT category, COUNT(*)FROM productsGROUP BY category;Even though PostgreSQL groups rows by category, that does not mean the final rows are guaranteed to appear alphabetically.
Grouping and sorting are different operations.
If you need a particular output order, write it explicitly:
123456SELECT category, COUNT(*) AS product_countFROM productsGROUP BY categoryORDER BY category;A useful distinction is:
12345GROUP BY→ decide which rows belong to the same groupORDER BY→ decide how result rows are displayedGROUP BY and NULL
If a grouping column contains NULL, PostgreSQL groups those NULL values together for grouping purposes.
Imagine:
123456category--------audioNULLofficeNULLThen:
1GROUP BY categoryproduces one group for the rows whose category is NULL.
So grouping does not create a separate group for every individual NULL row.
Filtering groups
Suppose we run:
12345SELECT status, COUNT(*) AS order_countFROM ordersGROUP BY status;and now want only statuses with more than 10000 orders.
We cannot use:
1WHERE COUNT(*) > 10000because WHERE runs before the groups and their counts exist.
To filter based on an aggregate result, PostgreSQL provides another clause:
1HAVINGThe important point is:
`GROUP BY` divides rows into groups so aggregate functions can calculate a separate result for each group.