skip to content

MERGE and Upsert

The ANSI answer to 'insert it, unless it exists — then update it': MERGE with its WHEN MATCHED / WHEN NOT MATCHED arms. Interviewers ask it for ETL and sync scenarios, and expect you to know vendor upserts exist but diverge from the standard.

part ofSQLoverview, primer and where to startread it →
on this pageshow

questions

5

How does MERGE INTO ... USING ... ON perform an upsert in a single statement?

level: juniorimportance: must knowfreq 60%

answer

  1. one statement, two possible outcomes per row
  2. the target is the only table written
  3. ON decides matched versus not matched
  4. WHEN MATCHED and WHEN NOT MATCHED arms
  5. UPDATE for existing keys, INSERT for new ones

basics

~20 s

MERGE joins a target table to a source rowset through an ON condition, then applies one action per source row: WHEN MATCHED THEN UPDATE for keys that already exist in the target, WHEN NOT MATCHED THEN INSERT for the rest.

solid answer

~40 s

`MERGE INTO target t USING source s ON (t.key = s.key)` classifies every source row against the target using the ON predicate. Rows that find a matching target row are *matched* and drive the `WHEN MATCHED THEN UPDATE SET ...` arm; rows that find none are *not matched* and drive `WHEN NOT MATCHED THEN INSERT (...) VALUES (...)`. Only the target is modified — the source can be a table, a view, a derived table, or a `VALUES` list given an alias. Each source row triggers at most one action, in a single statement, so you do not write an UPDATE and then a conditional INSERT. The standard also allows `WHEN MATCHED THEN DELETE`, and arms may carry an extra `AND` condition. MERGE was standardized in SQL:2003, but engine support is not universal.

code

sql · 8 lines
sql
MERGE INTO customer_dim t
USING staging_customer s
   ON (t.customer_id = s.customer_id)
WHEN MATCHED THEN
  UPDATE SET name = s.name, city = s.city
WHEN NOT MATCHED THEN
  INSERT (customer_id, name, city)
  VALUES (s.customer_id, s.name, s.city);

go deeper

for a junior

Be able to write a basic upsert from memory: MERGE INTO target, USING source, ON the key, then the matched-update and not-matched-insert arms. Know that only the target table is modified.

for a middle

Explain the classification step — every source row is matched or not matched by the ON predicate — and know the source can be any query, that arms accept extra AND conditions, and that DELETE is a legal matched action.

for a senior

Show judgment about the source query: dedupe it, keep filters out of ON, and know that standard MERGE cannot act on target rows the source omits. Be candid that MySQL and SQLite have no MERGE, so portable code may need a different shape.

for a principal

Own the call of whether load logic lives in one MERGE or in explicit staged steps: MERGE is compact but engine-dependent and hard to audit row-by-row, while separate INSERT and UPDATE passes are portable and easier to instrument.

## The problem MERGE solves Loading data into a table that already holds rows almost always means the same thing: *insert the row if it is new, otherwise update the row that is already there*. Written with ordinary DML that becomes two statements — an `UPDATE` for the keys that exist plus an `INSERT ... SELECT` for the keys that do not — and application code often degenerates further into a per-row "try update, check the row count, insert if zero" loop. `MERGE`, standardized in SQL:2003, expresses the whole thing as one set-based statement over an entire source rowset. ## Anatomy of the statement ```sql MERGE INTO customer_dim t USING staging_customer s ON (t.customer_id = s.customer_id) WHEN MATCHED THEN UPDATE SET name = s.name, city = s.city WHEN NOT MATCHED THEN INSERT (customer_id, name, city) VALUES (s.customer_id, s.name, s.city); ``` Four parts: - **`MERGE INTO <target> [alias]`** — the only table the statement modifies. - **`USING <source> [alias]`** — a table, a view, a derived table (a parenthesised `SELECT` with an alias), or a `VALUES` list. It is read-only. - **`ON (<condition>)`** — the *match* condition. It is a join predicate, not a filter. - **One or more `WHEN ...` arms** — the actions. ## How rows are classified Conceptually the engine evaluates the ON condition between the source and the target. For each source row: if at least one target row satisfies ON, that source row is **matched**; otherwise it is **not matched**. Matched source rows drive `UPDATE` or `DELETE` against the matched target row; unmatched ones drive `INSERT` of a brand-new target row. Exactly one arm fires per row, and the classification is made against the target as it stood when the statement began — a row this same statement inserts is not then visible as a match for a later source row. ## The arms - `WHEN MATCHED THEN UPDATE SET col = expr, ...` — the assignment targets are target columns, written unqualified; the right-hand expressions may reference both the target and the source alias. There is no `FROM`, no `WHERE`: the arm applies to the matched target row. - `WHEN MATCHED THEN DELETE` — removes the matched target row. This is what lets a single MERGE apply a delta feed that contains deletions. - `WHEN NOT MATCHED THEN INSERT (cols) VALUES (exprs)` — the value expressions may reference source columns, constants and functions only, because there is no matched target row to read from. Any arm may carry an extra condition, as in `WHEN MATCHED AND s.op = 'D' THEN DELETE`, and arms of the same kind may be repeated; they are tested in written order and the first one whose condition holds wins. ## What MERGE does not give you MERGE has no `WHERE` clause of its own — the only way to narrow the source is inside the `USING` query, and narrowing the *target* through the ON condition is a classic trap, because a target row excluded by ON is reported as *not matched* and gets inserted a second time. Standard MERGE also has no arm for target rows the source knows nothing about; the "not matched by source" direction is an engine extension where it exists at all (SQL Server spells it `WHEN NOT MATCHED BY SOURCE`). And a target row may be acted on only once: if two source rows match the same target row, the statement is in error rather than picking a winner, so the source usually has to be collapsed to one row per key first. ## Portability MERGE is genuinely part of the SQL standard and is implemented by Oracle, SQL Server, Db2 and PostgreSQL (added in PostgreSQL 15). MySQL and SQLite have no MERGE statement at all; they offer INSERT-side upsert clauses instead, which are dialect extensions with different matching rules. So "standard" here means "in the standard", not "available everywhere" — check the target engine before writing MERGE into portable code.

  • Can the USING clause be a query rather than a table?
    Yes. `USING` accepts any table, view, parenthesised `SELECT` with an alias, or a `VALUES` list with an alias and column names. That is how real loads work: the source is usually a staging select that already filters, joins and shapes the rows the merge should apply.
  • Which columns may the WHEN NOT MATCHED THEN INSERT arm reference?
    Only source columns, constants, and expressions over them. There is no matched target row for that arm, so referencing the target alias in the VALUES list is meaningless and the engine rejects it. The matched arms, by contrast, can read both sides.
  • Does MERGE require a unique index on the ON columns?
    The standard does not require one — matching is by whatever the ON predicate says. But without uniqueness on the target's match key, a single source row can match several target rows and update all of them, and duplicate source keys make the statement fail outright. In practice you merge on a key that is unique in both.

MERGE is a shipping clerk with one inbound pallet and a shelf: for each item on the pallet, if the shelf already has that SKU he restocks that slot, otherwise he creates a new slot. He never touches the pallet, only the shelf.

saying these in an interview costs you the question

  • Thinks MERGE runs an UPDATE and then a separate INSERT statement
  • Says the ON clause filters which target rows survive
  • Believes WHEN NOT MATCHED can update an existing target row
  • Assumes every engine has MERGE because it is in the standard
  • Tries to add a WHERE clause to the MERGE statement itself

context

open as a page

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

level: middleimportance: must knowfreq 50%

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.

open as a page

How does a MERGE evaluate multiple WHEN MATCHED arms carrying AND conditions?

level: middleimportance: should knowfreq 30%

basics

~20 s

Arms are tested in the order written, and the first one whose condition holds fires — at most one action per row. That ordering lets a single MERGE delete tombstoned rows, update changed ones, and leave unchanged rows alone.

open as a page

How does ANSI MERGE differ from a vendor upsert that keys on a unique-constraint conflict?

level: middleimportance: should knowfreq 40%

basics

~20 s

MERGE decides matched-versus-new with an arbitrary ON predicate against a source rowset and can update, insert or delete. Conflict-style upserts hang off an INSERT, trigger only when a unique or primary key collides, and cannot delete.

open as a page

Why does putting a filter predicate in a MERGE ON clause cause unwanted INSERTs?

level: seniorimportance: should knowfreq 33%

basics

~20 s

ON only classifies source rows as matched or not matched. An existing target row excluded by an extra predicate in ON is reported as not matched, so the insert arm adds a second row for the same key — a duplicate or a unique-constraint failure.

open as a page