In a ROLLUP result, how do you tell a subtotal row's NULL from a NULL in the data?
answer
- The value alone cannot tell you
- There is a function for this
- Returns 1 or 0 per grouped row
- 1 means rolled away, 0 means grouped
- GROUPING() also works in HAVING and ORDER BY
basics
~10 sUse the GROUPING() function: GROUPING(region) returns 1 when that row's grouping set left region out, and 0 when the row is genuinely grouped by region — including when the grouped value itself is NULL.
solid answer
~50 sYou cannot tell them apart by looking at the value: a super-aggregate row and a real group whose key is NULL both show NULL in that column. The standard answer is `GROUPING(expr)`, which returns **1** when the expression was rolled away for that row and **0** when the row really is grouped by it. So `CASE WHEN GROUPING(region) = 1 THEN 'All regions' ELSE COALESCE(region, '(unknown)') END` labels totals correctly and still distinguishes a genuine NULL region. `GROUPING()` is usable in `SELECT`, `HAVING` and `ORDER BY`, so it also drives filtering (`HAVING GROUPING(region) = 0`) and sorting (`ORDER BY GROUPING(region), region`). The common bug is `COALESCE(region, 'Total')`, which mislabels the real NULL group as a total. PostgreSQL's multi-argument `GROUPING(a, b)` and Oracle's and SQL Server's `GROUPING_ID(a, b)` return a bit mask covering several columns at once.
code
sql · 11 lines-- WRONG: 'All regions' is printed twice when some rows have region IS NULL
SELECT COALESCE(region, 'All regions') AS region_label, SUM(amount) AS total
FROM sales
GROUP BY ROLLUP(region);
-- RIGHT: test GROUPING() first, then handle the data NULL separately
SELECT CASE WHEN GROUPING(region) = 1 THEN 'All regions'
ELSE COALESCE(region, '(no region)') END AS region_label,
SUM(amount) AS total
FROM sales
GROUP BY ROLLUP(region);go deeper
Remember that both a real NULL key and a subtotal placeholder show up as NULL, and that GROUPING(col) returning 1 marks the placeholder. Recognising the COALESCE mislabelling bug is the expected takeaway.
Explain the 1/0 contract precisely, write the CASE label expression, and say why filtering on IS NOT NULL is not equivalent to filtering on GROUPING() = 0.
Demonstrate using GROUPING() in HAVING and ORDER BY to control which levels reach the report and where subtotals sort, and discuss returning a level indicator so the client never re-derives it from values.
Frame it as a contract between the query and the reporting layer: totals must be self-describing in the result set, and NOT NULL grouping keys or explicit sentinels remove a whole class of reporting defects at the schema level.
## Two different NULLs in one column A grouping-set query can put two kinds of NULL in the same output column: - a **data NULL** — the group really exists, and its grouping key is NULL (rows whose `region` was never filled in collapse into one group whose key is NULL); - a **placeholder NULL** — the row is a super-aggregate produced by a grouping set that excluded that column, so there is no single value to show. They are indistinguishable by value. Any presentation logic that reads the column alone (`COALESCE`, `IFNULL`, a client-side `if (region == null)`) will confuse them, and the symptom is a report with two rows labelled "Total" whose numbers do not match. ## GROUPING() The SQL standard provides `GROUPING(expression)` for exactly this. Its argument must be one of the expressions in the `GROUP BY` clause. It returns: - **1** if that expression is *not* part of the grouping set that produced this row (the value shown is a placeholder); - **0** if it *is* part of the grouping set (the value shown is a real grouping key, NULL or not). ```sql SELECT region, GROUPING(region) AS is_total, SUM(amount) AS total FROM sales GROUP BY ROLLUP(region); ``` For a table with regions `EU`, `US` and some rows with no region at all, this returns `EU/0`, `US/0`, `NULL/0` (the real no-region group) and `NULL/1` (the grand total). The `is_total` column is the only thing separating the last two. ## Labelling The correct label expression tests `GROUPING()` first and only then handles a data NULL: ```sql SELECT CASE WHEN GROUPING(region) = 1 THEN 'All regions' ELSE COALESCE(region, '(no region)') END AS region_label, SUM(amount) AS total FROM sales GROUP BY ROLLUP(region); ``` Writing `COALESCE(region, 'All regions')` instead is the classic bug: it prints "All regions" twice, once for the genuine NULL-region group and once for the grand total. ## Filtering and sorting with it `GROUPING()` is not restricted to the select list. Because it is evaluated per grouped row, it is legal in `HAVING` and in `ORDER BY`: - `HAVING GROUPING(region) = 0` keeps only rows that are grouped by region — a precise way to drop the grand total that, unlike `region IS NOT NULL`, does not also drop the legitimate NULL-region group. - `ORDER BY GROUPING(region), region, GROUPING(product), product` sorts each subtotal after its own detail block, independently of how the engine orders NULLs. That beats relying on `NULLS LAST`, which would also move a data NULL. ## Several columns at once When a query rolls up three or four columns, testing each one separately gets noisy. Engines offer a bit-mask form: PostgreSQL accepts multi-argument `GROUPING(region, product)`, and Oracle and SQL Server offer `GROUPING_ID(region, product)`. Both return an integer whose bits correspond to the arguments, the leftmost argument being the most significant bit: `0` = both columns grouped (a detail row), `1` = only the last argument rolled away, `3` = both rolled away (the grand total). That single integer is convenient as a "level" column for the reporting client to switch on. Which spelling you get is engine-specific, so check the documentation rather than assuming. ## Why the engine cannot just do it for you SQL has no distinct "total marker" value; the standard chose to reuse NULL for the placeholder and to add a function that exposes what the value alone cannot. That design means the *information* is always available, but only if the query asks for it. If your reporting layer needs to know which level a row belongs to, the query must return `GROUPING()` (or the bit mask) as a column — it cannot be reconstructed afterwards from the values. ## Practical rules Select a `GROUPING()` column for every column you roll up when the base data can contain NULLs in those columns; add a `NOT NULL` constraint on grouping keys where the domain allows, which removes the ambiguity at the source; and never let a client infer "this is a total row" from a NULL value alone.
- Why is HAVING region IS NOT NULL not a safe way to drop ROLLUP total rows?Because it also drops any legitimate group whose region value is genuinely NULL, silently removing real data from the report. `HAVING GROUPING(region) = 0` tests the row's grouping set instead of its value, so it removes only the super-aggregate rows and keeps the NULL-region group intact.
- What does the multi-argument form of GROUPING return?A bit mask. PostgreSQL's `GROUPING(region, product)` and Oracle's and SQL Server's `GROUPING_ID(region, product)` return an integer whose bits correspond to the arguments, leftmost most significant: 0 when both are grouped, 1 when only `product` was rolled away, 3 for the grand total. It is a compact level indicator for the client to switch on.
- Where can GROUPING() legally appear in a query?Anywhere an aggregate may appear in a grouped query: the select list, `HAVING`, and `ORDER BY`. Its argument must be an expression from the `GROUP BY` clause — `GROUPING(some_ungrouped_column)` is an error. It is meaningless in `WHERE`, which runs before grouping happens.
- How would you avoid the ambiguity entirely?Make the grouping columns `NOT NULL` where the domain allows it, or map missing values to an explicit sentinel such as 'UNKNOWN' during load. Then any NULL in a grouping column of the result is unambiguously a super-aggregate placeholder. That is a schema decision, though; `GROUPING()` remains the correct answer for data you do not control.
saying these in an interview costs you the question
- Says COALESCE(region, 'Total') is the standard solution
- Claims a subtotal NULL is recognisable by its position in the result
- Thinks GROUPING() returns the number of rows in the group
- Uses region IS NOT NULL to filter out super-aggregate rows
- Believes engines emit a special total marker instead of NULL