Why does COUNT(DISTINCT o.order_id) repair a fanned-out count but SUM(DISTINCT o.amount) not repair the total?
answer
- DISTINCT removes values, not rows
- only a unique column makes those the same
- two orders can share one amount
- the key deduplicates safely; money does not
- it rescues counts, never totals
basics
~20 sCOUNT(DISTINCT pk) works because a primary key identifies a row uniquely, so collapsing repeats restores the true row count. SUM(DISTINCT amount) deduplicates values, not rows: two different orders of 100 collapse into a single 100.
solid answer
~50 s`DISTINCT` inside an aggregate removes duplicate **values**, and that only matches removing duplicate **rows** when the column uniquely identifies a row. A primary key does, so `COUNT(DISTINCT o.order_id)` counts each order once no matter how many child rows the join produced — a correct repair. A money column does not: if two separate orders both cost 100, `SUM(DISTINCT o.amount)` sees one value 100 and returns 100 instead of 200, so it is wrong even on data with no fan-out at all. That makes `COUNT(DISTINCT pk)` a *partial* guard: it rescues counts of the parent entity and nothing else, and it hides the fact that fan-out is still corrupting every `SUM` and `AVG` in the same SELECT list. When measures are involved, remove the multiplication instead — pre-aggregate the child into a one-row-per-parent summary and join that.
code
sql · 7 lines-- two orders, both amount = 100, each with 2 items
SELECT COUNT(*) AS joined_rows, -- 4
COUNT(DISTINCT o.order_id) AS orders, -- 2 (correct)
SUM(o.amount) AS inflated_sum, -- 400
SUM(DISTINCT o.amount) AS wrong_sum -- 100 (true total is 200)
FROM orders o
JOIN order_items i ON i.order_id = o.order_id;go deeper
Remember the one-line rule: DISTINCT inside an aggregate removes duplicate values. Counting a primary key that way is safe; summing a money column that way is not, because different rows can hold the same amount.
Give the counterexample from memory — two separate orders of 100 becoming 100 — and explain that value deduplication only equals row deduplication for a uniquely-valued column.
Show that you treat COUNT(DISTINCT pk) as a partial guard: flag any SELECT list that pairs a repaired count with an unrepaired SUM, and prefer EXISTS or pre-aggregation so no DISTINCT is needed at all.
Decide the standard: DISTINCT scattered through a SELECT list is a grain defect, not a fix. Push teams to publish measures from queries whose grain is declared, so counts and sums in one row are guaranteed consistent.
## What DISTINCT inside an aggregate actually does `COUNT(DISTINCT x)`, `SUM(DISTINCT x)` and `AVG(DISTINCT x)` are standard SQL: the aggregate first collapses the multiset of `x` values to its distinct values, then aggregates those. The crucial word is *values*. It is a deduplication of the column's contents, not of the rows those contents came from. Join fan-out duplicates **rows**. So `DISTINCT` inside an aggregate repairs fan-out exactly when "one distinct value" and "one original row" mean the same thing — that is, when the column is unique across the parent table. ## Why the primary key case works ```sql SELECT COUNT(DISTINCT o.order_id) AS orders FROM orders o JOIN order_items i ON i.order_id = o.order_id; ``` `order_id` is the primary key of `orders`: unique and not nullable. The join produced one row per item, so `order_id` appears once per item, but the set of distinct `order_id` values is exactly the set of orders that had at least one item. Deduplicating values and deduplicating rows coincide, so the count is right. Two caveats travel with this. `COUNT(DISTINCT col)` ignores NULLs, so counting a nullable column counts only the rows where it is present. And the guard is only as strong as the uniqueness: `COUNT(DISTINCT o.customer_name)` counts distinct *names*, merging two different customers who share one — a bug that has nothing to do with the join and everything to do with picking a non-key column. ## Why the sum case fails ```sql -- two orders, both amount = 100, each with 2 items SELECT SUM(o.amount) AS inflated, -- 400 SUM(DISTINCT o.amount) AS wrong -- 100 FROM orders o JOIN order_items i ON i.order_id = o.order_id; ``` The true total is 200. The plain `SUM` gives 400 because each order's 100 is added twice. `SUM(DISTINCT o.amount)` gives 100, because the distinct set of amounts is `{100}`. The repair overshoots: it removes the fan-out duplicates *and* the legitimate repetition of equal amounts across different orders. Worse, it is wrong on this data even with no join at all — `SELECT SUM(DISTINCT amount) FROM orders` also returns 100. A construct that gives the wrong answer on a single table is not a fan-out fix; it is a different bug that happens to move the number in the right direction sometimes. The same reasoning condemns `AVG(DISTINCT x)`: it averages the distinct values, which is a meaningful question only if you actually wanted "average of the distinct price points". ## What DISTINCT can and cannot be used for Use `COUNT(DISTINCT parent_pk)` when: - you need how many parent entities appear in a fanned-out result, and - the column is the parent's key, and - no additive measure over parent columns shares the same SELECT list. That last condition is the one people miss. In a query like `SELECT COUNT(DISTINCT o.order_id), SUM(o.amount) FROM orders o JOIN order_items i ...`, the count is right and the sum is wrong, and the correct count makes the wrong sum look trustworthy. Mixing a repaired measure with an unrepaired one in the same row is how fan-out reaches dashboards. ## The structural alternative When measures are in play, remove the multiplication rather than compensating for it: ```sql SELECT COUNT(*) AS orders, SUM(o.amount) AS revenue FROM orders o WHERE EXISTS (SELECT 1 FROM order_items i WHERE i.order_id = o.order_id); ``` Here no join multiplies anything — the existence test filters without duplicating — so both aggregates are correct with no `DISTINCT` anywhere. If you also need a measure *from* the child, pre-aggregate it into a one-row-per-order derived table and join that. Both rewrites make `DISTINCT` unnecessary, which is the real goal: `DISTINCT` sprinkled through a SELECT list is a symptom that the query's grain is wrong. ## Interview framing State the distinction in one line — "DISTINCT deduplicates values; fan-out duplicates rows; they coincide only for a key column" — give the two-orders-of-100 counterexample, and finish by saying you would rather remove the fan-out than paper over it. Being explicit that `COUNT(DISTINCT pk)` is a *partial* guard, correct for counts and useless for sums, is the answer interviewers are listening for.
- Is SUM(DISTINCT o.amount) ever the right thing to write?Only when "sum the distinct values" is genuinely the question — summing the distinct price points in a catalogue, for instance. It is never a fan-out repair, because it also collapses legitimately equal values from different rows, and it returns the same wrong number even on a single table with no join.
- What if you count DISTINCT on a non-key column such as customer_name?You get the number of distinct names, not the number of customers. Two different customers with the same name merge into one, and rows where the name is NULL are skipped entirely, since COUNT(DISTINCT col) ignores NULLs. Only a column with an enforced uniqueness guarantee makes the count equal a row count.
- How do you get a correct order count and correct revenue in the same query over a fanned-out join?Do not fan out. Filter with `EXISTS` if you only need the child as a condition, or pre-aggregate the child into a one-row-per-order derived table and join that. Then `COUNT(*)` and `SUM(o.amount)` are both correct with no DISTINCT, and the two numbers are consistent with each other.
saying these in an interview costs you the question
- Treats SUM(DISTINCT x) as the fan-out fix for totals
- Assumes DISTINCT deduplicates rows, not values
- Uses COUNT(DISTINCT name) as if names were unique
- Mixes a DISTINCT-repaired count with a raw SUM in one SELECT
- Forgets COUNT(DISTINCT col) skips NULL values