Why is adding SELECT DISTINCT to make duplicate rows disappear a performance smell?
answer
- not a free keyword
- something produced the duplicates
- runs after every join has fanned out
- forces a sort or hash of everything
- hides fan-out rather than fixing it
basics
~20 sSELECT DISTINCT deduplicates the entire result after the joins have already run, so the engine sorts or hashes every row produced just to throw most of them away. It hides the real cause, usually a fan-out join, instead of removing duplicates at the source.
solid answer
~50 s`DISTINCT` is applied at the end of the pipeline: the joins run, produce whatever rows they produce, and only then are duplicate rows collapsed by a sort or a hash. If a one-to-many join fanned 50,000 orders out to 5 million rows, the engine still built and deduplicated 5 million rows. When the dedup is implemented as a sort it is also blocking, so no row comes back until the input is exhausted. Worse, it treats the symptom: the duplicates came from somewhere, and the same fan-out that duplicated rows is still corrupting any `SUM` or `COUNT` in the query, which `DISTINCT` does nothing about. The fix is at the source: use `EXISTS` when the join only tests existence, pre-aggregate the child table in a derived table before joining, or add the join predicate you forgot.
code
sql · 13 lines-- Smell: DISTINCT repairs fan-out from a one-to-many join
SELECT DISTINCT o.id, o.customer_id, o.total
FROM orders o
JOIN order_items i ON i.order_id = o.id
WHERE i.sku = 'ABC-1';
-- Fix: the child table is only an existence test
SELECT o.id, o.customer_id, o.total
FROM orders o
WHERE EXISTS (SELECT 1
FROM order_items i
WHERE i.order_id = o.id
AND i.sku = 'ABC-1');go deeper
Recall that DISTINCT dedups the whole selected row and runs after the joins. If you reach for it, be ready to say which join produced the duplicate rows.
Explain the mechanics: the dedup sorts or hashes every row the join produced, a sort-based one is blocking, and the same fan-out silently corrupts any aggregate in the query.
In review, treat an unexplained DISTINCT as a defect report. Show the rewrite you would apply — EXISTS for existence-only joins, pre-aggregation for value-carrying ones — and how you verify the row counts before and after.
Own the standard: queries state their grain explicitly, and DISTINCT appears only when uniqueness is the requirement. Blanket DISTINCT in a codebase hides join bugs that surface later as wrong reported numbers.
## What the keyword actually asks for `SELECT DISTINCT` returns only unique rows of the **whole select list** — the full tuple, not one column. There is no way to guarantee that without comparing every produced row against the others, so the engine either sorts the result and collapses adjacent equal rows, or builds a hash table keyed on the entire row. Either way the work sits on top of everything the query already did, because duplicate elimination is logically the last step before `ORDER BY`. That ordering is the whole point. The joins run first and produce whatever they produce; only then does the dedup see the rows. If a join fans 50,000 orders out to 5 million rows, `DISTINCT` processes 5 million rows to hand you back 50,000. The row count you see in the result tells you nothing about the row count the engine had to build. When the implementation is a sort, it is also a blocking step: the first row cannot be returned until the last input row has been read, so a query that could have streamed now has the latency of its entire result set. ## Where the duplicates came from Duplicates in a join result are not random noise, they are arithmetic. Joining a parent to a one-to-many child multiplies each parent row by the number of matching children: ```sql -- one row per order_item, not per order SELECT o.id, o.customer_id, o.total FROM orders o JOIN order_items i ON i.order_id = o.id WHERE i.sku = 'ABC-1'; ``` If an order contains that SKU three times, its row appears three times. Other common sources: an incomplete join predicate (joining on `customer_id` when the key is `(customer_id, region)`), a join through a bridge table, or a versioned table where you forgot to pick the current row. ## Why the reflex is a smell Because it patches the end of the pipeline for a defect introduced at the beginning, and it silences only one of that defect's symptoms. Consider what happens when the same query grows an aggregate: ```sql SELECT DISTINCT o.id, o.total, SUM(i.qty) AS qty -- still wrong ``` The fan-out that duplicated `o.total` will also double-count it in any `SUM(o.total)` you add later, and `DISTINCT` cannot help there — it only collapses rows that are identical in every selected column. A query carrying an unexplained `DISTINCT` is therefore a query nobody has reasoned about, and the next person to extend it inherits a landmine. There is a second, quieter cost: `DISTINCT` makes the query's correctness depend on the select list. Add one more column with a differing value and the duplicates come back, because the rows are no longer identical. ## Removing the duplicates at the source Three rewrites cover almost every case. **The join only tests existence.** If the child table contributes no columns to the output, do not join to it: ```sql SELECT o.id, o.customer_id, o.total FROM orders o WHERE EXISTS (SELECT 1 FROM order_items i WHERE i.order_id = o.id AND i.sku = 'ABC-1'); ``` This returns each order once by construction — there is nothing to deduplicate, because no extra rows were ever produced. **You need a value from the many side.** Aggregate it first, then join one-to-one: ```sql SELECT o.id, o.total, x.qty FROM orders o JOIN (SELECT order_id, SUM(qty) AS qty FROM order_items GROUP BY order_id) x ON x.order_id = o.id; ``` **The join predicate is incomplete.** Add the missing key column or the missing filter that selects one row per parent. This is the case where `DISTINCT` is most dangerous, because it makes a genuinely wrong join look right. ## When DISTINCT is the right answer When uniqueness is the question, not a repair. "Which cities do we ship to" is a set-valued question and `SELECT DISTINCT city FROM addresses` states it directly; the dedup is the work you asked for, over a narrow projection. `DISTINCT` is also fine as a deliberate, commented decision when the alternative rewrite is genuinely more complex and the intermediate row count is small. ## Reviewing a query that already has one Delete the keyword and run the query. If the row count is unchanged, it was dead weight — remove it. If duplicates appear, count them: compare the result's row count with the count of distinct keys of the driving table, and look for the join that multiplies. Then decide which of the three rewrites applies. Keep `DISTINCT` only when you can say in one sentence why uniqueness is part of the requirement rather than part of the cleanup.
- You remove DISTINCT from a query and the row count is identical. What does that tell you?That the keyword was doing nothing: the joins already produced unique rows for the current data and select list. Delete it — it is pure cost and a misleading signal to the next reader. Be aware the result is data-dependent, so if uniqueness is a real requirement, enforce it with a key or the join, not with a keyword nobody can justify.
- Why does DISTINCT not rescue a SUM in the same query?Because duplicate elimination happens after aggregation in the logical order, and it compares whole output rows. By the time `DISTINCT` runs, the fanned-out child rows have already been added into `SUM`, so the number is inflated and identical in every duplicate row. The fix is to aggregate the many side in a derived table first, then join.
Adding DISTINCT to fix duplicate rows is like photocopying a document five times by mistake and then sorting the pile to keep one of each. The paper was still printed, and nobody asked why the copier ran five times.
saying these in an interview costs you the question
- Says DISTINCT is free because the optimizer removes it
- Adds DISTINCT without asking where duplicates came from
- Thinks DISTINCT is applied before or inside the join
- Claims DISTINCT deduplicates only the first selected column
- Assumes deduplicating is cheaper than fixing the join