skip to content

What does SELECT DISTINCT country FROM users return when four rows have a NULL country?

level: middleimportance: should knowfreq 50%

answer

  1. Duplicate removal does not use the = operator
  2. Think "not distinct from", not "equals"
  3. Nulls group together rather than disappear
  4. One output row for all the nulls

basics

~20 s

All four NULL rows collapse into a single row whose country is NULL. Duplicate elimination treats two NULLs as the same value, so the result is one NULL row plus one row per distinct non-null country.

solid answer

~40 s

Duplicate elimination does not use the `=` comparison. Two rows are duplicates when, for each column, either both values are non-null and equal **or** both are null. That is exactly the "not distinct from" rule, which is why the keyword is `DISTINCT`. So four NULL countries collapse to one output row containing NULL, and it is kept — `DISTINCT` never filters NULLs out, it only merges them. This surprises people who know that `NULL = NULL` evaluates to UNKNOWN and therefore expect either four NULL rows or none. The same rule applies across several columns: `('A', NULL)` appearing twice is one output row, while `('A', NULL)` and `('A','B')` are two. If you want the NULL row gone, say so with `WHERE country IS NOT NULL`; don't expect `DISTINCT` to do it.

go deeper

for a junior

Remember the outcome: a column full of NULLs contributes exactly one NULL row, and that row is kept. Practise predicting the row count of a small DISTINCT over data containing NULLs.

for a middle

Explain the mechanism, not just the outcome: duplicate elimination uses the not-distinct comparison, where two NULLs match, while WHERE predicates use three-valued logic where they do not.

for a senior

Show that you think about grain: deduplicating on nullable columns changes how many rows you get depending on how much data is missing, and a stray NULL row can break a downstream export or join.

for a principal

Own the data-quality framing. Decide as a policy whether missing values are a distinct category or must be filtered at the edge, so every report does not re-litigate what a NULL row in a dimension list means.

## The comparison rule behind duplicate elimination SQL uses three-valued logic in predicates: `NULL = NULL` evaluates to UNKNOWN, and a `WHERE` clause keeps only rows whose predicate is TRUE. If duplicate elimination were defined in terms of `=`, no two NULLs could ever be recognised as equal, and every NULL row would survive. That is not how it is defined. For duplicate removal the standard uses a different notion: two values are *not distinct* when they are both null, or both non-null and equal. Two rows are duplicates when they are not distinct in every column. `DISTINCT` therefore treats NULL as a value that equals itself, purely for the purpose of grouping duplicates together. The predicate `IS NOT DISTINCT FROM` exists to make the same comparison available inside a `WHERE` clause. ## Worked example ```sql -- users.country: 'DE', 'DE', 'FR', 'JP', NULL, NULL, NULL, NULL SELECT DISTINCT country FROM users; -- 'DE' -- 'FR' -- 'JP' -- NULL <- one row, not four and not zero ``` Four rows out of eight inputs. The three distinct non-null values each contribute a row, and the four NULLs contribute exactly one. The key point is that the NULL row is **present**: `DISTINCT` is a deduplication operator, never a filter. ## Why this trips people up Developers correctly learn early that `WHERE country = NULL` matches nothing and that you must write `WHERE country IS NULL`. They then generalise "NULL is never equal to anything" to every part of the language. But the language applies the not-distinct rule in several places where values must be grouped rather than tested: duplicate elimination for `DISTINCT`, grouping keys (all NULL keys form one group), and duplicate removal in set operations such as `UNION`. Predicates use three-valued logic; grouping-style operations use the not-distinct rule. Keeping those two contexts separate is what the question is really testing. ## Multiple columns The rule is per column, and all columns must match: ```sql -- t: ('A', NULL), ('A', NULL), ('A', 'B'), (NULL, NULL) SELECT DISTINCT c1, c2 FROM t; -- ('A', NULL) -- ('A', 'B') -- (NULL, NULL) ``` Three rows. The repeated `('A', NULL)` collapses because both columns are not distinct; `('A','B')` differs in the second column; `(NULL, NULL)` differs in the first. Note that a row of all NULLs is a perfectly ordinary duplicate-elimination candidate — it merges with other all-NULL rows and with nothing else. ## A contrast worth knowing Deduplication in the select list keeps a NULL row, whereas aggregate functions generally ignore NULL inputs, so a count of distinct values and a count of the rows returned by `SELECT DISTINCT` can legitimately differ by one. Recognising that these are two different operators with two different NULL policies avoids a whole family of off-by-one report bugs. ## Practical consequences The behaviour is usually what you want when you are enumerating the values a column actually takes: seeing a NULL row tells you the column has missing data, which is real information. It becomes a trap in two situations. First, when the deduplicated columns act as a business key. If `(email, phone)` is meant to identify a person and phone is frequently NULL, `SELECT DISTINCT email, phone` will merge all the phone-less rows for an email into one — which may or may not be the intent — while any row that does carry a phone splits off separately. Deduplicating on nullable columns quietly changes the grain of the result depending on how much data is missing. Second, when the NULL row flows downstream into a UI dropdown, a join key, or a CSV export where a blank category is meaningless. The fix is explicit, not incidental: ```sql SELECT DISTINCT country FROM users WHERE country IS NOT NULL; -- or, to fold missing data into a label: SELECT DISTINCT COALESCE(country, 'UNKNOWN') AS country FROM users; ``` The second form deduplicates on the coalesced expression, so genuine `'UNKNOWN'` values in the data would merge with the NULLs — a small reminder that the select list, not the stored column, is what gets deduplicated. ## Pitfalls Expecting NULL rows to be dropped; expecting each NULL to survive as its own row; assuming `DISTINCT` and a `=`-based predicate follow the same comparison rules; and deduplicating on nullable business keys without deciding what a missing value means.

  • How do you get the distinct values without the NULL row?
    Filter explicitly with `WHERE country IS NOT NULL`, or map missing data to a label with `SELECT DISTINCT COALESCE(country, 'UNKNOWN')`. Note that the second form deduplicates on the coalesced expression, so a real `'UNKNOWN'` stored in the column would merge with the NULLs. There is no option on `DISTINCT` itself that skips NULLs.
  • A table has ('A', NULL) twice and ('A','B') once. What does SELECT DISTINCT c1, c2 return?
    Two rows: `('A', NULL)` and `('A','B')`. The repeated pair collapses because both columns are not distinct — matching values in the first, both null in the second. The third row differs in the second column, so it survives on its own.
  • Which other parts of a query use this same NULLs-are-equal rule?
    Grouping keys collapse all NULL keys into a single group, and duplicate removal in `UNION`, `INTERSECT` and `EXCEPT` treats two NULLs as matching. The pattern is that operations which *group or match rows* use the not-distinct rule, while predicates evaluated per row use three-valued logic.

saying these in an interview costs you the question

  • Says DISTINCT drops NULL rows from the result
  • Expects each NULL row to survive separately
  • Claims NULL = NULL applies to duplicate elimination
  • Confuses the NULL policy of DISTINCT with an aggregate's
  • Deduplicates on nullable business keys without deciding intent

context