We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
GROUPING SETS, ROLLUP, and CUBE
Standard GROUP BY computes one level of aggregation. But business reports often need subtotals (by region), cross-tabulations (by region AND product), and grand totals -- all in a single result set. PostgreSQL's GROUPING SETS, ROLLUP, and CUBE do exactly that.
The Problem: Multiple Aggregation Levels
Suppose you want a sales report showing:
- Totals by region and payment method (detail rows).
- Totals by region alone (subtotals).
- A grand total across everything.
Without GROUPING SETS, you would need three separate queries combined with UNION ALL:
-- The hard way
SELECT region, payment_method, SUM(amount) FROM sales GROUP BY region, payment_method
UNION ALL
SELECT region, NULL, SUM(amount) FROM sales GROUP BY region
UNION ALL
SELECT NULL, NULL, SUM(amount) FROM sales;
This scans the table three times. There is a better way.
GROUPING SETS
GROUPING SETS lets you specify exactly which groupings you want in a single query:
SELECT
region,
payment_method,
SUM(amount) AS total_amount,
COUNT(*) AS num_sales
FROM sales
GROUP BY GROUPING SETS (
(region, payment_method), -- detail: region + payment method
(region), -- subtotal: region only
() -- grand total
)
ORDER BY region NULLS LAST, payment_method NULLS LAST;
Sample output:
| region | payment_method | total_amount | num_sales |
|---|---|---|---|
| East | bank_transfer | 1250.00 | 5 |
| East | cash | 890.00 | 4 |
| East | credit_card | 1640.00 | 6 |
| East | NULL | 3780.00 | 15 |
| North | bank_transfer | 980.00 | 3 |
| North | cash | 1120.00 | 5 |
| North | credit_card | 1560.00 | 7 |
| North | NULL | 3660.00 | 15 |
| ... | ... | ... | ... |
| NULL | NULL | 14500.00 | 50 |
Rows where payment_method is NULL are subtotals for that region. The row where both columns are NULL is the grand total.
Custom Grouping Combinations
You can specify any combination of groupings:
-- Only subtotals by region and by payment_method, plus grand total
-- (no detail rows)
SELECT
region,
payment_method,
SUM(amount) AS total_amount
FROM sales
GROUP BY GROUPING SETS (
(region),
(payment_method),
()
)
ORDER BY region NULLS LAST, payment_method NULLS LAST;
ROLLUP -- Hierarchical Subtotals
ROLLUP is a shorthand for the most common pattern: hierarchical subtotals from left to right, ending with a grand total.
ROLLUP(a, b, c)
-- is equivalent to:
GROUPING SETS (
(a, b, c),
(a, b),
(a),
()
)
Example: Sales Hierarchy
SELECT
region,
payment_method,
SUM(amount) AS total_amount,
COUNT(*) AS num_sales
FROM sales
GROUP BY ROLLUP(region, payment_method)
ORDER BY region NULLS LAST, payment_method NULLS LAST;
This produces:
- Detail rows (region + payment_method)
- Subtotals per region
- Grand total
Think of ROLLUP like a report with expanding summaries: the leftmost column is the broadest category, and each additional column adds detail.
Three-Level ROLLUP
-- If we had year data, this is how a 3-level rollup would look
SELECT
EXTRACT(YEAR FROM sale_date)::INT AS sale_year,
region,
payment_method,
SUM(amount) AS total_amount
FROM sales
GROUP BY ROLLUP(
EXTRACT(YEAR FROM sale_date)::INT,
region,
payment_method
)
ORDER BY sale_year NULLS LAST, region NULLS LAST, payment_method NULLS LAST;
This gives you subtotals at every level: year+region+method, year+region, year, and grand total.
CUBE -- All Possible Combinations
CUBE generates every possible combination of the listed columns:
CUBE(a, b)
-- is equivalent to:
GROUPING SETS (
(a, b),
(a),
(b),
()
)
Example
SELECT
region,
payment_method,
SUM(amount) AS total_amount,
COUNT(*) AS num_sales
FROM sales
GROUP BY CUBE(region, payment_method)
ORDER BY region NULLS LAST, payment_method NULLS LAST;
This includes everything ROLLUP gives you plus subtotals by payment_method alone (which ROLLUP omits). It is a complete cross-tabulation.
ROLLUP vs. CUBE Comparison
With two columns (region, payment_method):
| Grouping | ROLLUP | CUBE |
|---|---|---|
| (region, payment_method) | Yes | Yes |
| (region) | Yes | Yes |
| (payment_method) | No | Yes |
| () | Yes | Yes |
With three columns, the difference grows:
ROLLUP(a, b, c)produces 4 grouping sets.CUBE(a, b, c)produces 8 grouping sets (2^3).
Use ROLLUP when your columns have a natural hierarchy (year > quarter > month). Use CUBE when you want every possible subtotal.
The GROUPING() Function
The tricky part of GROUPING SETS is that subtotal rows show NULL in columns they aggregate over. But what if a column legitimately contains NULL values? The GROUPING() function resolves this ambiguity:
GROUPING(column) returns:
0 = the column is part of the current grouping (real value, even if NULL)
1 = the column is aggregated over (subtotal row)
Using GROUPING() to Label Rows
SELECT
CASE WHEN GROUPING(region) = 1 THEN 'ALL REGIONS'
ELSE region
END AS region,
CASE WHEN GROUPING(payment_method) = 1 THEN 'ALL METHODS'
ELSE payment_method
END AS payment_method,
SUM(amount) AS total_amount,
COUNT(*) AS num_sales
FROM sales
GROUP BY ROLLUP(region, payment_method)
ORDER BY GROUPING(region), region, GROUPING(payment_method), payment_method;
Now instead of NULL in subtotal rows, you see clear labels:
| region | payment_method | total_amount | num_sales |
|---|---|---|---|
| East | bank_transfer | 1250.00 | 5 |
| East | cash | 890.00 | 4 |
| East | credit_card | 1640.00 | 6 |
| East | ALL METHODS | 3780.00 | 15 |
| North | bank_transfer | 980.00 | 3 |
| ... | ... | ... | ... |
| ALL REGIONS | ALL METHODS | 14500.00 | 50 |
Adding a Row Type Column
SELECT
region,
payment_method,
SUM(amount) AS total_amount,
CASE
WHEN GROUPING(region) = 1 AND GROUPING(payment_method) = 1 THEN 'grand_total'
WHEN GROUPING(payment_method) = 1 THEN 'region_subtotal'
ELSE 'detail'
END AS row_type
FROM sales
GROUP BY ROLLUP(region, payment_method)
ORDER BY region NULLS LAST, payment_method NULLS LAST;
Partial ROLLUP and CUBE
You can mix regular GROUP BY columns with ROLLUP or CUBE:
-- Always group by region, but ROLLUP on payment_method only
SELECT
region,
payment_method,
SUM(amount) AS total_amount
FROM sales
GROUP BY region, ROLLUP(payment_method)
ORDER BY region, payment_method NULLS LAST;
This produces detail rows and subtotals per region, but no grand total (because region is always grouped).
Multiple ROLLUP/CUBE in One Query
SELECT
region,
payment_method,
SUM(amount) AS total_amount
FROM sales
GROUP BY GROUPING SETS (
(region, payment_method),
(region),
(payment_method),
()
)
ORDER BY
GROUPING(region),
region NULLS LAST,
GROUPING(payment_method),
payment_method NULLS LAST;
Practical Report: Monthly Sales Summary
Here is a real-world report combining ROLLUP with other techniques:
SELECT
CASE WHEN GROUPING(region) = 1 THEN '** TOTAL **'
ELSE region
END AS region,
COUNT(*) AS num_sales,
SUM(amount) AS revenue,
ROUND(AVG(amount), 2) AS avg_sale,
MIN(amount) AS min_sale,
MAX(amount) AS max_sale,
COUNT(DISTINCT customer_id) AS unique_customers,
string_agg(DISTINCT payment_method, ', ' ORDER BY payment_method)
FILTER (WHERE GROUPING(region) = 0) AS payment_methods
FROM sales
GROUP BY ROLLUP(region)
ORDER BY GROUPING(region), revenue DESC;
Performance Considerations
- GROUPING SETS, ROLLUP, and CUBE scan the table once (or a small number of times with hash aggregation), which is far more efficient than multiple UNION ALL queries.
- The planner may use HashAggregate or GroupAggregate depending on the data volume.
- Adding more columns to CUBE grows the result exponentially (2^n groupings). Be cautious with
CUBE(a, b, c, d, e)-- that is 32 grouping sets. - ROLLUP is generally more practical than CUBE because real data usually has a natural hierarchy.
Try It Yourself
Run these queries against the sales table:
-
Use
ROLLUP(region)withSUM(amount)andCOUNT(*)to produce subtotals by region and a grand total. -
Use
CUBE(region, payment_method)to see every combination of subtotals. How many rows are in the result? -
Add
GROUPING()calls to label subtotal rows with descriptive text like "All Regions" and "All Methods" instead of NULL. -
Write a report using
GROUPING SETSthat shows totals by region, totals by payment_method, and a grand total -- but not the detail-level region+payment_method combinations. -
Try a partial rollup:
GROUP BY payment_method, ROLLUP(region). How does the output differ fromROLLUP(payment_method, region)?
Summary
| Clause | Groupings Generated | Use Case |
|---|---|---|
GROUP BY a, b |
(a, b) only | Standard single-level grouping |
GROUPING SETS ((a,b),(a),()) |
Exactly the listed sets | Custom subtotal combinations |
ROLLUP(a, b) |
(a,b), (a), () | Hierarchical subtotals (reports) |
CUBE(a, b) |
(a,b), (a), (b), () | All possible subtotals |
GROUPING(col) |
N/A | Returns 0/1 to identify subtotal rows |
| Pattern | Tip |
|---|---|
| Label subtotals | Use CASE WHEN GROUPING(col) = 1 THEN 'Total' ELSE col END |
| Order subtotals last | Use ORDER BY GROUPING(col), col NULLS LAST |
| Partial rollup | GROUP BY fixed_col, ROLLUP(rollup_cols) |
| Avoid CUBE explosion | Limit CUBE to 3-4 columns maximum |