skip to content

How does GROUP BY treat rows whose grouping key is NULL, and how many groups do they form?

level: middleimportance: must knowfreq 60%

answer

  1. grouping does not use the = operator
  2. two missing values count as the same
  3. one group, not one group per row
  4. the key column shows NULL in the output
  5. same rule that DISTINCT uses

basics

~20 s

GROUP BY treats two NULLs as not distinct, so all rows with a NULL key collapse into one group whose key prints as NULL — even though NULL = NULL evaluates to UNKNOWN in a WHERE predicate.

solid answer

~50 s

Grouping does not use the `=` operator. The standard defines group membership by whether key values are *distinct*, and two NULLs are explicitly **not distinct** from each other. So every row with a NULL grouping key lands in one single group, and that group's key value appears as NULL in the output. That is deliberately different from `WHERE region = NULL`, which is UNKNOWN and matches nothing. The same not-distinct rule is why `SELECT DISTINCT` folds NULLs together. With a multi-column key the rule applies per position: `(NULL, 'paid')` groups with other `(NULL, 'paid')` rows but not with `('EU', 'paid')` or `(NULL, 'refunded')`. In reports the NULL group is easy to misread as "no data" — it actually means "rows whose key was missing", so label it explicitly, for example `COALESCE(region, 'unknown')`, and remember that grouping by that expression merges genuine NULLs with any literal `'unknown'` values already stored.

code

sql · 5 lines
sql
-- customers: 5 rows with region NULL, 3 rows with region 'EU'
SELECT region, COUNT(*) AS n
FROM customers
GROUP BY region;
-- 2 rows: (NULL, 5) and ('EU', 3) -- all NULLs share one group

go deeper

for a junior

Recall the fact and the count: all rows with a NULL grouping key form one group, shown with NULL in the key column, and those rows are not dropped. Predict two groups, not six, for five NULL rows plus one value.

for a middle

Explain the mechanism — grouping uses a not-distinct comparison in which two NULLs count as the same value, unlike a WHERE predicate's three-valued comparison — and apply it position by position to a multi-column key.

for a senior

Treat an unexpected NULL group as a data-quality signal, not a display nuisance. Show that you trace it to a missing NOT NULL constraint, an unset upstream key or an unintended outer join, and that filtering it away breaks reconciliation against raw counts.

for a principal

Decide the policy: whether missing dimension values are represented as NULL, as an explicit unknown member of the dimension, or rejected at write time. Each choice moves the cost between the schema, the load pipeline and every report that reads the data.

## Grouping does not use = The rule that surprises people is that `GROUP BY` and `WHERE` compare values by different rules. A `WHERE` predicate uses comparison operators and three-valued logic: `region = NULL` is UNKNOWN, and a row whose predicate is UNKNOWN is not kept. Grouping instead asks whether two key values are **distinct**, and the standard states that two NULLs are *not distinct* from one another, while a NULL and any non-NULL value *are* distinct. Consequences: - All rows with a NULL key belong to exactly one group. - That group is a real group like any other: it appears in the output with NULL in the key column and an aggregate computed over its rows. - The NULL group is never silently dropped. Only a `WHERE region IS NOT NULL` (or an inner join that eliminates those rows) removes it. ## Worked example ```sql -- customers(id, region): 5 rows with region NULL, 3 with 'EU' SELECT region, COUNT(*) AS n FROM customers GROUP BY region; -- 2 rows: (NULL, 5) and ('EU', 3) ``` Two rows, not six. If NULLs were compared with `=`, no two NULL rows would ever match and you would get one group per NULL row — that is the intuition candidates often bring, and it is wrong. ## Multi-column keys The comparison is per position across the key tuple. Two rows share a group when *every* key position is not distinct: | region | status | group | |--------|--------|-------| | NULL | paid | A | | NULL | paid | A | | NULL | refunded | B | | EU | paid | C | Three groups. NULLs match NULLs position by position, and a NULL never matches a non-NULL. ## Reading a NULL group in a report A NULL row in a grouped report is ambiguous to a human reader for three separate reasons, and it is worth knowing which one you are looking at: 1. **Missing source data** — the key column was genuinely NULL for those rows. This is the case discussed here. 2. **Outer-join extension** — the rows came from the unmatched side of an outer join, so the key was NULL-extended rather than stored as NULL. 3. **Super-aggregate placeholders** — if the query uses `ROLLUP` or `CUBE`, the subtotal rows carry NULL in the columns they are aggregating over. Telling those apart from data NULLs is exactly what the `GROUPING()` function is for. In a plain `GROUP BY` only case 1 and case 2 are possible, and both mean "these rows had no key value at grouping time". ## Labelling the group The usual production fix is to give the group a readable label: ```sql SELECT COALESCE(region, 'unknown') AS region_label, COUNT(*) AS n FROM customers GROUP BY COALESCE(region, 'unknown'); ``` Two cautions come with that idiom. First, you are now grouping by an expression, so the same expression must appear in `GROUP BY` — grouping by the raw column while displaying the coalesced value would still key on the raw NULL and merely relabel the output, which is usually what you want, but you must be deliberate about it. Second, the label collides: if `'unknown'` already exists as a stored value, the coalesce merges genuine unknown-region rows with genuinely-NULL rows into one indistinguishable group. Pick a sentinel that cannot occur in the data, or keep the NULL and format it in the presentation layer. ## When the NULL group is a bug signal A NULL group appearing where you did not expect one is a useful alarm. It usually means one of: - a column that the domain assumes is mandatory is not declared `NOT NULL`, and rows have slipped through; - an upstream load left a foreign key unset; - a join you thought was inner is actually outer, so unmatched rows arrived NULL-extended. In each case the interesting work is upstream of the query. Deleting the NULL group with `WHERE region IS NOT NULL` hides the count and makes the report's total disagree with the raw row count — a classic source of "the numbers don't add up" complaints. ## The one-line summary For grouping (and for duplicate elimination, which uses the same rule) NULLs are treated as equal to each other; for predicates they are not. Knowing which of the two rules a clause uses is the whole answer.

  • Why does GROUP BY fold NULLs together when WHERE region = NULL matches nothing?
    They apply different rules. A predicate uses comparison and three-valued logic, so NULL = NULL is UNKNOWN and the row is not kept. Grouping instead asks whether two key values are distinct, and the standard defines two NULLs as not distinct. Duplicate elimination uses the grouping rule, which is why DISTINCT also folds NULLs.
  • What is the risk in writing GROUP BY COALESCE(region, 'unknown') to label the NULL group?
    The label can collide with real data. If any row already stores the literal 'unknown', it merges with the genuinely-NULL rows into one group you can no longer separate. Choose a sentinel value that cannot occur, or keep the NULL in the query and format it in the presentation layer instead.
  • A grouped report shows an unexpected NULL row. What do you investigate?
    Whether the key column should have been NOT NULL, whether an upstream load left it unset, or whether a join you assumed was inner is actually outer so unmatched rows arrived NULL-extended. Filtering the NULL group away is not a fix — the totals then stop matching the raw row count.

Sorting mail into pigeonholes by city: envelopes with no city written on them all go into a single 'no address' pigeonhole rather than each getting its own — grouping treats every missing label as the same missing label.

saying these in an interview costs you the question

  • Says each NULL key forms its own group because NULL never equals NULL
  • Claims GROUP BY silently discards rows with NULL keys
  • Assumes WHERE and GROUP BY compare values by the same rule
  • Thinks a NULL group always means an outer-join artefact
  • Adds WHERE key IS NOT NULL to hide the group and breaks totals

context