A table is supposed to hold exactly one row per (customer, product) pair, but duplicates keep appearing. How would you decide whether duplicate elimination belongs in the schema, in the write path, or in every read - and what does each choice cost?
answer
- defect or legitimate event?
- unique constraint = race-proof + planner proof
- check-then-insert races
- nullable key column defeats uniqueness
- dedup at ingest boundary, materialize once
basics
~20 sPrefer the schema: a unique constraint makes the table a set on every write, is race-proof, and lets the planner skip duplicate removal. Write-path logic alone races; read-time deduplication taxes every query and hides the defect. Read-time dedup is right only when duplicates are legitimate events.
solid answer
~60 sFirst decide whether a duplicate is a defect or a fact. If (customer, product) is genuinely an identity, the table should be a set and the constraint belongs in the schema - it is enforced under concurrency, it documents intent, and it lets the optimizer prove uniqueness and drop dedup operators and fan-out risk from downstream queries. The costs are real but bounded: an index maintained on every write, and writers that must handle constraint violations, typically via an atomic upsert or merge, which needs the constraint anyway. Application-level 'check then insert' is not an alternative: two concurrent writers both see no row and both insert. It reduces error noise; it enforces nothing. Read-time deduplication is right only for append-only event or ingest tables where at-least-once delivery makes repeats expected. Then I keep the raw bag as the record of what arrived, dedupe on a business key when materializing the curated table, and make that dedup deterministic - pick a winner by an explicit rule rather than hoping the copies are identical.
go deeper
Know that a unique constraint is what actually prevents duplicate rows, and that removing duplicates in a query does not stop them being written.
Explain why check-then-insert races and why an atomic upsert built on the constraint is the standard write pattern.
Weigh index write cost, cleanup of existing duplicates, and the difference between a defect duplicate and an at-least-once delivery duplicate.
Own the boundary: raw bag at ingest, deduplicated set materialized once, constraints wherever a key is an identity, and the planner benefits that follow from declaring them.
## The decision starts with semantics Ask what a second identical row *means*. If it means the same fact was recorded twice, the table's intended shape is a set and duplicates are corruption. If it means the same event happened twice, or that an at-least-once pipeline delivered a message twice, the table is legitimately a bag and duplicates must be tolerated somewhere downstream. These two answers lead to opposite designs, and getting them backwards is the real failure mode - teams put a dedup in the read path of a table that should have had a constraint, and put a constraint on an ingest table that then rejects perfectly normal retries. ## Schema enforcement A declared unique constraint on (customer_id, product_id) is the strongest option. It holds under concurrency because enforcement happens inside the engine's index maintenance, so two simultaneous inserts cannot both win. Creating it requires cleaning existing duplicates, which surfaces the size of the problem immediately. And it is the only option that pays a dividend to readers: because the planner may rely on enforced constraints, it can prove that joining on that pair cannot fan out and can eliminate duplicate-removal steps. Costs to weigh honestly: an extra index maintained on every insert, update and delete of the key columns, plus its storage; write amplification on hot tables; and on very high-throughput ingest, index contention. Writers also need a strategy for violations - most commonly an atomic upsert or merge, which is not merely convenient but the correct concurrent formulation, and which itself relies on the constraint. A trap worth naming: if a key column is nullable, uniqueness usually does not bite for rows where it is absent, because such rows are typically treated as distinct from one another. A unique constraint over a nullable column is a partial guarantee at best; make the columns NOT NULL or model the optional case explicitly. ## Write-path enforcement Deduplicating in application or pipeline code - look up, then insert if absent - fails under concurrency. Two workers can interleave between the check and the insert and both succeed. Serializable isolation or explicit locking closes the window, but you are then re-implementing what a unique index does natively, more slowly and with more code. Write-path logic is still useful as a first line of defence, to give friendly errors and avoid pointless constraint violations, but it should sit on top of a constraint, never instead of one. There is one legitimate write-path-only variant: an idempotency or dedup key attached to incoming messages, checked against a store of recently-seen keys. That is still a uniqueness constraint - it has simply moved to a different table. ## Read-time deduplication For an append-only landing table fed by at-least-once delivery, duplicates are expected and the raw table should stay a bag - it is the audit record of what arrived. The set is produced when building the curated table: group by the business key and select a deterministic winner (latest event timestamp, then highest sequence number as a tiebreak). Two properties matter: it must be deterministic so reruns give identical results, and the business key must be right, because deduping on the whole row only catches byte-identical repeats and misses copies that differ in an ingest timestamp. The cost of read-time dedup is that every consumer pays a sort or hash forever, and consumers who forget it get wrong numbers. That is why the deduplicated result should be materialized once, not left as a convention each analyst is expected to remember. ## How I usually land it Declare the constraint wherever the pair is an identity, make the key columns NOT NULL, use an atomic upsert in writers, and reserve read-time deduplication for the ingest boundary where duplicates are part of the delivery contract. Then treat any duplicate-elimination step appearing in a report over a constrained table as a bug report about a join, not as normal practice.
- Why is 'select then insert if missing' insufficient even inside a transaction?Under snapshot or read-committed isolation, two concurrent transactions can both read no matching row and both insert, because neither sees the other's uncommitted row. Only a unique index, serializable isolation, or an explicit lock on a shared object makes the check-and-insert atomic. The index does it cheaply and without extra failure modes.
- What do you do about existing duplicates before adding the constraint?Quantify them first - total rows minus count of distinct key pairs tells you the scale. Then decide a merge rule rather than deleting arbitrarily, because the copies often differ in some columns and one of them is the truth. Clean in batches, add the constraint, and only then simplify the queries that were compensating for duplicates.
saying these in an interview costs you the question
- Treating an application-level existence check as equivalent to a unique constraint
- Adding a duplicate-removal step to every report instead of fixing the table
- Putting a unique constraint on an at-least-once ingest table and breaking retries
- Including a nullable column in the unique key and assuming duplicates are impossible
- Deduplicating on the whole row when copies differ only in an ingest timestamp