skip to content

What happens when a MERGE source contains two rows matching the same target row?

level: middleimportance: must knowfreq 50%

answer

  1. each target row may be acted on once
  2. the engine refuses to pick a winner
  3. statement fails, nothing is applied
  4. cardinality violation; ORA-30926 in Oracle
  5. fix by deduplicating the USING query

basics

~20 s

It is a cardinality violation: the standard forbids acting on the same target row twice in one MERGE, so the statement fails with an error instead of silently letting the last source row win. Collapse the source to one row per key first.

solid answer

~50 s

MERGE guarantees each target row is touched at most once. If two source rows both satisfy the ON condition against the same target row, the statement raises a cardinality-violation error rather than applying both updates in some arbitrary order — Oracle's ORA-30926 ("unable to get a stable set of rows in the source tables") is the best-known message, and other engines report the same thing in their own words. The fix is in the `USING` query: deduplicate or aggregate so it yields one row per match key, choosing a deterministic winner (latest timestamp, highest sequence number). Note the asymmetry — one source row matching *several* target rows is legal and updates each of them once. Duplicate keys can also bite through the not-matched arm, where two new rows with the same key both get inserted.

code

sql · 9 lines
sql
-- FAILS when incoming_feed has two rows for the same account_id
-- that already exists in account: one target row would be updated twice
MERGE INTO account t
USING incoming_feed s
   ON (t.account_id = s.account_id)
WHEN MATCHED THEN
  UPDATE SET balance = s.balance
WHEN NOT MATCHED THEN
  INSERT (account_id, balance) VALUES (s.account_id, s.balance);

go deeper

for a junior

Remember that a MERGE fails if the source contains two rows for the same key that exists in the target — it does not quietly keep the last one. The fix is to clean the source before merging.

for a middle

Explain the cardinality rule: each target row may be acted on at most once, so a doubly matched target row aborts the statement. Know that the reverse case, one source row matching many target rows, is legal.

for a senior

Treat the error as a data-quality signal from upstream and design the source query to resolve it deterministically — a per-key ordering on an event timestamp — rather than papering over it. Cover the not-matched twin, where duplicates become duplicate inserts.

for a principal

Decide the policy for conflicting records at the pipeline level: silent last-write-wins, explicit reconciliation, or rejecting the batch. Whatever the rule, it should be visible and measurable, not an accident of how one MERGE happens to be written.

## The rule A MERGE statement may act on any given target row **at most once**. If the ON condition makes two or more source rows match the same target row, the statement is in error — the standard calls this a cardinality violation — and the engine aborts it rather than choosing a winner. This is deliberate: "whichever source row the engine happened to process last wins" would make the result depend on physical row order, which SQL refuses to expose. ## What it looks like in practice A daily feed arrives with two rows for account 42 — an intraday correction followed by the end-of-day figure: ```sql MERGE INTO account t USING incoming_feed s ON (t.account_id = s.account_id) WHEN MATCHED THEN UPDATE SET balance = s.balance WHEN NOT MATCHED THEN INSERT (account_id, balance) VALUES (s.account_id, s.balance); ``` If account 42 already exists in `account`, both feed rows are matched against the same target row and the statement fails. Oracle reports `ORA-30926: unable to get a stable set of rows in the source tables`; other engines phrase it as the MERGE attempting to update or delete the same row more than once. The important part is the shape of the failure: it is loud, it is at statement level, and nothing is applied. ## The asymmetry people miss The rule protects the **target** row, not the source row. One source row that matches five target rows is perfectly legal — each of those five target rows is affected exactly once, so all five are updated with the same values. That is often exactly what you want when merging a lookup value onto many detail rows, and it is why the error message talking about "the source" confuses people the first time. ## The duplicate also bites the insert arm Suppose account 42 does *not* exist yet. Now neither feed row matches anything, so both are classified not matched and both flow into `WHEN NOT MATCHED THEN INSERT`. Classification happens against the target as it was before the statement, so the second row does not "see" the row the first one inserted. You end up with two rows for the same key — or, if the target has a unique constraint on `account_id`, a unique-constraint violation instead of the tidy cardinality error. Either way, duplicate source keys are the defect. ## The fix: shape the source The repair always lives in the `USING` query. Reduce the source to exactly one row per match key, using a rule you can defend: ```sql MERGE INTO account t USING ( SELECT account_id, MAX(balance) AS balance FROM incoming_feed GROUP BY account_id ) s ON (t.account_id = s.account_id) WHEN MATCHED THEN UPDATE SET balance = s.balance WHEN NOT MATCHED THEN INSERT (account_id, balance) VALUES (s.account_id, s.balance); ``` Aggregation is the simplest form, but it is only correct when the aggregate *is* the business rule. For "last write wins" feeds the honest rule is a per-key ordering on an event timestamp or sequence column, keeping one row per key; for genuinely conflicting rows the right answer may be to reject the batch rather than pick silently. The other half of the fix is the target side: the ON columns should be unique in the target — usually its primary key or a unique key. If they are not, ask whether MERGE is expressing what you actually mean, because "update every target row that loosely matches" is rarely the intent of an upsert. ## Why it is worth knowing This error is the single most common MERGE failure in ETL work, and it usually surfaces in production rather than in testing, because test fixtures rarely contain the duplicate that a real upstream system eventually emits. Treat it as a data-quality signal: the statement is telling you the batch contains two versions of the same fact and nobody decided which one is true. Building the deduplication into the source query — and, ideally, monitoring how often it discards rows — is the durable answer, not retrying the merge.

  • Is it also an error for one source row to match many target rows?
    No. That direction is legal: each of those target rows is acted on exactly once, so all of them are updated from the same source row. The rule protects the target row from a second action, not the source row from being reused.
  • What if the duplicated key does not exist in the target yet?
    Then no cardinality error fires. Both source rows are classified not matched — classification uses the target as it was before the statement — so both flow into the insert arm. You get two rows for one key, or a unique-constraint violation if the target enforces uniqueness.
  • How do you choose which duplicate to keep?
    Make the rule explicit in the source query: order by an event timestamp or sequence number and keep one row per key, or aggregate when the aggregate genuinely is the business rule. If duplicates represent conflicting facts, rejecting the batch is often more honest than silently choosing.

saying these in an interview costs you the question

  • Says the last matching source row simply wins
  • Thinks the engine applies both updates in sequence
  • Blames the target and adds a DISTINCT to the target side
  • Assumes a unique index on the target prevents the error
  • Retries the merge instead of deduplicating the source

context