skip to content

What does COUNT(DISTINCT status) return for four rows with statuses A, A, B and NULL?

level: middleimportance: should knowfreq 66%

answer

  1. two filters happen, not one
  2. unknowns are dropped before de-duplication
  3. repeats collapse to a single value
  4. row-level DISTINCT disagrees about NULL
  5. the answer is smaller than three

basics

~10 s

Two. COUNT(DISTINCT expr) counts different non-NULL values, so A and B count once each and the NULL is ignored — it never forms a value of its own.

solid answer

~40 s

`COUNT(DISTINCT status)` returns **2**. The aggregate first discards NULL inputs, then counts how many different values remain: `A` (however many times it repeats) and `B`. For comparison, on the same four rows `COUNT(*)` returns 4 and `COUNT(status)` returns 3. The asymmetry worth remembering is against row-level `DISTINCT`: `SELECT DISTINCT status FROM t` returns **three** rows — `A`, `B` and `NULL` — because `DISTINCT` in the select list keeps NULL as a group, whereas the aggregate drops it before counting. If your metric needs the unknowns as a bucket, make that explicit, for example `COUNT(DISTINCT COALESCE(status, 'UNKNOWN'))`, and be aware that this then merges genuine `'UNKNOWN'` values with the NULLs.

code

sql · 8 lines
sql
-- tickets holds statuses: 'A', 'A', 'B', NULL
SELECT COUNT(*)               AS rows_total,      -- 4
       COUNT(status)          AS with_a_value,   -- 3
       COUNT(DISTINCT status) AS distinct_values -- 2
FROM tickets;

-- but SELECT DISTINCT keeps the unknown as its own row
SELECT DISTINCT status FROM tickets; -- 3 rows: 'A', 'B', NULL

go deeper

for a junior

Know the one-line rule — different non-NULL values — and be able to give all three numbers (COUNT(*), COUNT(col), COUNT(DISTINCT col)) for a tiny worked example.

for a middle

Explain the two-step evaluation and, unprompted, contrast it with row-level SELECT DISTINCT, which keeps NULL as a group. That contrast is what the question is really testing.

for a senior

Show the reporting judgement: decide explicitly whether NULL means 'no category' or 'a category called unknown', and describe how you would surface unknowns without folding them into a real value by accident.

for a principal

Own the definition, not just the SQL: metrics like 'active users' must state their NULL policy and their grouping scope, otherwise two teams compute two defensible numbers and neither can reconcile them.

## The rule `COUNT(DISTINCT <expression>)` performs two steps: throw away rows where the expression is NULL, then count the number of **different** remaining values. On the values `A, A, B, NULL` that leaves `{A, B}`, so the result is 2. Seen next to its siblings on the same four rows: ```sql SELECT COUNT(*) AS rows_total, -- 4 COUNT(status) AS with_a_value, -- 3 COUNT(DISTINCT status) AS distinct_values -- 2 FROM tickets; ``` Three numbers, three different questions: how many rows, how many rows carry a value, how many values exist. ## The asymmetry with row-level DISTINCT This is the part interviewers probe. `SELECT DISTINCT status FROM tickets` returns three rows: `A`, `B` and `NULL`. Row-level `DISTINCT` treats NULLs as duplicates of each other and collapses them into a single row — it keeps them. The aggregate does not: it removes NULL inputs before counting anything. So `COUNT(DISTINCT status)` is **not** the same as "the number of rows returned by `SELECT DISTINCT status`" whenever NULLs are present; it is one less. Anyone who has replaced a `SELECT DISTINCT x` list with a count in a dashboard has felt this discrepancy: two panes that should agree differ by exactly one. ## Empty and all-NULL groups If a group contains no rows, or every row's expression is NULL, `COUNT(DISTINCT ...)` returns **0**, not NULL. That makes it safe in comparisons and in denominators guarded by a zero check, unlike `SUM`, which is NULL over empty input. ## Where it is the right tool The classic uses are "how many different customers ordered this month" (`COUNT(DISTINCT customer_id)`) and "how many distinct products appear in this basket". The NULL-dropping is usually what you want there: a row whose `customer_id` is unknown should not be counted as a customer. The danger is the opposite case — a status, category or country column where NULL is a meaningful bucket ("not classified yet") that stakeholders expect to see in a count of categories. ## Making unknowns countable If NULL must count as its own category, replace it with a real value before the aggregate: ```sql SELECT COUNT(DISTINCT COALESCE(status, 'UNCLASSIFIED')) FROM tickets; ``` The caveat is collision: if `'UNCLASSIFIED'` also occurs as real data, the two merge and you undercount by one. Choose a sentinel that cannot appear, or report the unknowns separately as a second measure so nothing is folded together. ## A note on the interaction with DISTINCT elsewhere `COUNT(DISTINCT x)` de-duplicates within each group independently. In a grouped query, the same value appearing in two different groups is counted once per group — the distinctness is scoped to the group, not to the whole result. This trips people who expect the sum of per-group distinct counts to equal the overall distinct count; it does not, unless the value sets are disjoint. ## Answering it well Give the number, then the rule in one sentence ("different non-NULL values"), then volunteer the contrast with row-level `DISTINCT`, which shows you know that SQL's two de-duplication mechanisms disagree about NULL. Finish with the practical consequence: decide deliberately whether unknown means "no category" or "a category called unknown".

  • How many rows does SELECT DISTINCT status return on the same data, and why does it differ?
    Three: `A`, `B` and `NULL`. Row-level DISTINCT treats all NULLs as duplicates of each other and keeps one, while the aggregate discards NULL inputs before counting. The two mechanisms therefore differ by exactly one whenever the column contains NULLs.
  • What does COUNT(DISTINCT status) return for a group where every status is NULL?
    0. COUNT always returns an integer for a group it evaluates, so an all-NULL or empty input yields zero rather than NULL. That is why COUNT is safe to compare against 0, unlike SUM, which is NULL over empty input.

saying these in an interview costs you the question

  • Says NULL counts as one distinct value
  • Assumes COUNT(DISTINCT x) equals the row count of SELECT DISTINCT x
  • Thinks COUNT(DISTINCT x) can return NULL
  • Confuses it with counting rows that have a value
  • Believes per-group distinct counts sum to the overall distinct count

context