Why does a Delta Lake MERGE INTO fail when the source contains duplicate keys?
answer
- one target row, one matched action
- two candidate updates, no defined winner
- the problem is in the batch, not the statement
- last write wins, picked explicitly
- unmatched duplicates behave differently
basics
~20 sA matched UPDATE or DELETE must have a single, deterministic outcome per target row. If two source rows match the same target row, Delta cannot decide which wins and aborts the merge. The fix is to deduplicate the source to one row per key.
solid answer
~50 s`MERGE INTO` applies at most one matched action per target row. When the `ON` condition matches a target row against **two or more source rows** and a `WHEN MATCHED THEN UPDATE` (or `DELETE`) applies, the result would depend on which source row happened to be processed last — so Delta refuses and raises an error about multiple source rows matching the same target row. This is almost always a CDC feed carrying several changes for one key in the same batch. The fix is in the source, not the merge: collapse it to one row per key before merging, typically with `row_number() OVER (PARTITION BY id ORDER BY event_ts DESC) = 1`, or an aggregate that picks the latest state. Note the asymmetry: duplicates that match **nothing** in the target are simply inserted, so an un-deduplicated source silently creates duplicate rows on first load and only errors once those keys exist.
code
sql · 21 lines-- Fails: two source rows match one target row
MERGE INTO customers t
USING cdc_batch s
ON t.customer_id = s.customer_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;
-- Works: collapse the batch to the latest row per key first
MERGE INTO customers t
USING (
SELECT * FROM (
SELECT s.*, ROW_NUMBER() OVER (
PARTITION BY customer_id ORDER BY event_ts DESC, op_seq DESC) rn
FROM cdc_batch s
) WHERE rn = 1
) s
ON t.customer_id = s.customer_id
AND t.signup_month >= '2026-01'
WHEN MATCHED AND s.op = 'D' THEN DELETE
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED AND s.op <> 'D' THEN INSERT *;go deeper
Know that MERGE INTO combines insert, update and delete in one statement, and that the source must not contain two rows for the same key when those keys already exist in the target.
Explain why multiple matched source rows make the outcome nondeterministic, that the whole statement aborts rather than half-applying, and how a row_number deduplication with an explicit ordering column fixes it.
Handle the production shape: CDC batches with inserts, updates and deletes for one key, deletes that must survive deduplication, and ON-clause predicates that keep the merge from rewriting the entire table.
Set the contract upstream — ordering guarantees, sequence numbers and key uniqueness expectations for change feeds — so every consumer is not reinventing deduplication, and decide when snapshot-and-replace beats incremental merge entirely.
## What MERGE INTO is doing ```sql MERGE INTO customers t USING updates s ON t.customer_id = s.customer_id WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *; ``` Delta evaluates the `ON` condition as a join between the target table and the source relation, decides per target row whether a matched clause applies, and then rewrites the data files containing affected rows. The whole thing lands as one atomic commit. The critical property is that each target row can receive **at most one** matched action. That is not an implementation shortcut; it is what makes the statement deterministic. ## Why duplicates in the source break it If the source has two rows with `customer_id = 42` and the target has one, the join produces two candidate updates for the same target row. Their `SET` expressions may differ, and there is no ordering guarantee in a distributed join, so the outcome would depend on scheduling. Delta detects the situation and fails the statement with an error stating that multiple source rows matched — and, importantly, **fails the entire merge**: nothing is committed, so the target is not left half-applied. The practical trigger is nearly always a change feed. A CDC batch covering five minutes routinely contains an insert and two updates for the same primary key. Feeding it straight into `MERGE` works while those keys are new and starts failing the moment they exist in the target — which is why this shows up in production rather than in the first test run. ## The fix: deduplicate the source Collapse the source to one row per key, choosing the winner by an explicit ordering column — a sequence number, LSN or event timestamp: ```sql WITH latest AS ( SELECT * FROM ( SELECT s.*, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY event_ts DESC, op_seq DESC ) AS rn FROM updates s ) WHERE rn = 1 ) MERGE INTO customers t USING latest s ON t.customer_id = s.customer_id WHEN MATCHED AND s.op = 'D' THEN DELETE WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED AND s.op <> 'D' THEN INSERT *; ``` Two details worth stating in an interview. First, the ordering column must be a genuine sequencer; ordering by a wall-clock ingestion time with ties reintroduces the nondeterminism you were trying to remove, so include a tiebreaker. Second, a delete must be handled through the same last-write-wins pick — if the final event for a key is a delete, that is the row that must survive deduplication, otherwise you resurrect deleted records. ## The asymmetry with inserts Rows matching nothing in the target are inserted, one per source row, with no duplicate check at all. So the same un-deduplicated batch produces two behaviours over the lifetime of a table: duplicates on the first load, hard failures afterwards. If your table has no enforced primary key — and Delta does not enforce one — that first-load duplication is silent. When someone reports "our dimension table has duplicate keys but the merge has never failed," this is usually the reason. ## Target-side matching The converse — several **target** rows matching one source row — is fine and intended: it updates all of them. That is how you apply a correction across a fact table. Multiplicity is only a problem on the source side of a matched action. ## Cost, and why merge is a maintenance topic A merge rewrites every data file that contains an affected row, even if only one row in a gigabyte file changed. Two consequences follow. First, narrow the search: add predicates on partition or clustering columns to the `ON` condition (`AND t.event_date >= current_date() - 3`) so file pruning applies and the merge does not scan and rewrite the entire table. Second, expect small-file churn from frequent merges, which is what makes scheduled `OPTIMIZE` a companion to a merge-based pipeline. On runtimes where deletion vectors are enabled for the table, an update no longer necessarily rewrites the whole file: affected rows can be marked in a deletion vector and the new values appended, which shortens the write substantially at the cost of a merge-on-read step for readers. Even then, deduplicating the source is still mandatory — deletion vectors change how the write is applied, not the determinism rule. ## Related edges `WHEN NOT MATCHED BY SOURCE THEN DELETE` extends merge to full-table synchronization: rows in the target with no counterpart in the source are removed. It is powerful and dangerous — with a partial source batch it deletes almost everything, so it belongs with a `WHEN NOT MATCHED BY SOURCE AND t.partition_key IN (...)` guard scoping it to the range the batch actually covers.
- What happens if two source rows share a key that does not exist in the target?Both are inserted. The multiple-match rule only governs matched UPDATE and DELETE actions, so an un-deduplicated batch silently duplicates rows on first load and only starts failing once those keys exist in the target. Since Delta does not enforce primary keys, nothing catches it — which is why deduplication belongs in the pipeline rather than being left to the merge to police.
- Is it a problem when several target rows match one source row?No — that is supported and often intended: every matched target row receives the action, which is how you apply one correction across many fact rows. Determinism is only threatened when one target row has multiple candidate source rows, because then the outcome would depend on join scheduling.
- How do you stop a MERGE from rewriting the whole table?Add predicates on partition or clustering columns to the ON condition so the planner can prune files — for example restricting to the date range the batch covers. Without them the merge must consider every file that could contain a matching key, and it rewrites every file holding an affected row. Pair that with periodic OPTIMIZE, since frequent merges leave small files behind.
- Why is WHEN NOT MATCHED BY SOURCE THEN DELETE risky in an incremental pipeline?It deletes every target row without a counterpart in the source, which is correct for a full snapshot and catastrophic for a partial batch — an incremental feed would wipe everything it did not happen to contain. Guard it with a condition scoping the delete to the key range or partitions the batch genuinely covers.
saying these in an interview costs you the question
- Blames Delta rather than duplicates in the source batch
- Says the merge partially applies before failing
- Deduplicates by an arbitrary row with no ordering column
- Assumes Delta enforces a primary key on the target
- Believes duplicate unmatched rows are also rejected