skip to content

What extra rows does GROUP BY ROLLUP(region, product) add to a normal grouped result?

level: juniorimportance: should knowfreq 40%

answer

  1. More rows than plain GROUP BY
  2. Subtotals and a grand total
  3. Prefixes of the column list
  4. n columns give n+1 grouping sets
  5. Rolled-away columns come back NULL

basics

~10 s

It adds one subtotal row per region plus one grand-total row on top of the ordinary region-and-product groups. In those extra rows the columns rolled away come back as NULL placeholders.

solid answer

~40 s

`GROUP BY ROLLUP(region, product)` is shorthand for several grouping levels in one statement. It expands to the grouping sets `(region, product)`, `(region)` and `()` — so you get the normal detail rows, one subtotal row per region, and one grand-total row from the empty grouping set. In a subtotal row the columns that level did not group by are returned as NULL: the region subtotal has `product = NULL`, and the grand total has both `region` and `product` NULL. Those NULLs are placeholders meaning "not part of this row's grouping", not unknown data. With n columns ROLLUP produces n+1 grouping sets, and the whole thing is computed from one pass over the filtered rows rather than from three UNIONed queries. Result order is still undefined without an explicit `ORDER BY`.

code

sql · 12 lines
sql
-- sales holds 3 rows: ('EU','A',10), ('EU','B',20), ('US','A',30)
SELECT region, product, SUM(amount) AS total
FROM sales
GROUP BY ROLLUP(region, product);

-- result (order is not guaranteed without ORDER BY):
-- EU   | A    | 10
-- EU   | B    | 20
-- EU   | NULL | 30   <- subtotal for EU
-- US   | A    | 30
-- US   | NULL | 30   <- subtotal for US
-- NULL | NULL | 60   <- grand total

go deeper

for a junior

Recall that ROLLUP produces the normal groups plus subtotals plus one grand total, and that the rolled-away columns show NULL. Being able to count the rows for a small sample table is enough at this level.

for a middle

Explain the expansion to prefix grouping sets, why n columns give n+1 sets, why column order changes the middle level, and that each level is computed from the base rows rather than from the level beneath it.

for a senior

Show judgment about when a single ROLLUP beats three UNION ALL queries, how HAVING and ORDER BY interact with super-aggregate rows, and how result size grows if you roll up a high-cardinality column.

for a principal

Own the reporting contract: which levels the query is responsible for producing versus what the BI layer aggregates itself, and what portability costs you accept when a target engine offers only WITH ROLLUP or nothing at all.

## The problem ROLLUP solves A report usually wants more than one level of detail at once: sales by region **and** product, a line per region, and one line for the whole company. With plain `GROUP BY` that is three separate queries stitched together with `UNION ALL`, each reading the table again and each padding out the columns it does not group by. `ROLLUP` asks for all those levels in a single statement. ## What ROLLUP expands to `GROUP BY ROLLUP(a, b, c)` is shorthand for a list of **grouping sets** — the prefixes of the column list, longest to empty: ```sql GROUP BY GROUPING SETS ((a, b, c), (a, b), (a), ()) ``` So n columns give n+1 grouping sets. The empty grouping set `()` puts every row into a single group and produces the **grand total** — exactly what an aggregate query with no `GROUP BY` at all returns. The order of the columns matters: `ROLLUP(region, product)` subtotals by region, `ROLLUP(product, region)` subtotals by product. ## Reading the result Rows from the full grouping set are the ordinary detail rows. Every other set produces **super-aggregate** rows — rows summarising several detail rows. In a super-aggregate row, each column the grouping set left out is returned as NULL. Given a `sales(region, product, amount)` table holding `('EU','A',10)`, `('EU','B',20)`, `('US','A',30)`: ```sql SELECT region, product, SUM(amount) FROM sales GROUP BY ROLLUP(region, product); ``` returns six rows: `EU/A/10`, `EU/B/20`, `EU/NULL/30`, `US/A/30`, `US/NULL/30`, `NULL/NULL/60`. Three detail rows, two region subtotals, one grand total. ## Those NULLs are placeholders The NULL in `EU/NULL/30` does not mean the product is unknown; it means "this row is not grouped by product". If the base data also contains genuine NULL products, the two kinds of NULL appear in the same column and cannot be told apart by looking at the value — that is what the `GROUPING()` function exists for. ## Counting the rows The result size is the sum over grouping sets of the number of distinct value combinations in that set: detail groups, plus one row per distinct region, plus one. That growth is worth a thought before rolling up a high-cardinality column such as a customer id. ## Aggregates are recomputed, not summed Every grouping set is evaluated over the underlying rows, not over the rows of the level below. `SUM` and `COUNT(*)` happen to come out the same either way, but `AVG`, `COUNT(DISTINCT …)` and ratios do not — the grand-total `AVG` is the average of all base rows, not the average of the subtotal averages. ## Filtering and ordering `WHERE` still filters base rows before any grouping happens, so it applies to every level. `HAVING` runs after the grouping sets are formed and can filter super-aggregate rows like any other row. And the result has **no defined order** without `ORDER BY`; because placeholder NULLs sort wherever the engine puts NULLs, reporting queries usually sort with `GROUPING()` first so each subtotal lands under its own block. ## Use it for hierarchies ROLLUP fits columns that form a hierarchy — `(country, state, city)`, `(year, month, day)`, `(category, subcategory)` — because the prefixes are exactly the meaningful roll-up levels. Over independent dimensions the prefixes are arbitrary; `CUBE` or an explicit `GROUPING SETS` list is the better fit. You can also pin a column to every level by leaving it outside: `GROUP BY tenant_id, ROLLUP(region, product)` keeps `tenant_id` in all four sets. ## Portability `GROUP BY ROLLUP(...)` is the standard SQL spelling and is what PostgreSQL, Oracle, SQL Server and DB2 accept. MySQL 8 supports only the trailing form `GROUP BY region, product WITH ROLLUP` and has no `CUBE` or `GROUPING SETS`. SQLite supports none of them, so there the `UNION ALL` rewrite is still the answer. Check your engine's documentation before assuming a grouping-set feature is available.

  • Does ROLLUP(region, product) give the same result as ROLLUP(product, region)?
    No. ROLLUP takes prefixes of the list in order, so `ROLLUP(region, product)` yields `(region, product)`, `(region)`, `()` — subtotals per region. `ROLLUP(product, region)` yields `(product, region)`, `(product)`, `()` — subtotals per product. The detail rows and the grand total match; the middle level does not. Column order is a semantic choice, not a formatting one.
  • How do you keep one column present at every level of a ROLLUP?
    Leave it outside the ROLLUP list: `GROUP BY tenant_id, ROLLUP(region, product)`. The grouping sets become `(tenant_id, region, product)`, `(tenant_id, region)` and `(tenant_id)`, so every row — including the per-tenant total — still carries a real `tenant_id` and no cross-tenant grand total is produced.
  • Can you filter out the grand-total row that ROLLUP adds?
    Yes, with `HAVING`, which runs after the grouping sets are formed. `HAVING GROUPING(region) = 0` drops every row where the region was rolled away, which removes the grand total while keeping detail rows. Filtering on `region IS NOT NULL` is not equivalent: it would also drop a legitimate group whose region value is genuinely NULL.

saying these in an interview costs you the question

  • Thinks ROLLUP sorts or 'rolls up' rows into fewer rows
  • Says the NULLs mean missing data in the source table
  • Believes ROLLUP returns all column combinations like CUBE
  • Assumes the subtotal rows come back in report order automatically
  • Thinks the grand total is computed by summing the subtotal rows

context