MERGE
The MERGE statement applies a set of changes to a table in one atomic statement —
updating rows that already exist, inserting those that do not, and deleting those a
rule says should go.
Warning: MERGE is experimental and only works against catalog-backed tables. It is not suitable for production use.
Syntax
MERGE INTO <target> AS <alias>
USING <source> AS <alias>
ON <condition>
[ WHEN MATCHED [ AND <predicate> ] THEN UPDATE SET <column> = <expression> [, ...] ]
[ WHEN MATCHED [ AND <predicate> ] THEN DELETE ]
[ WHEN NOT MATCHED [ AND <predicate> ] THEN
INSERT ( <column> [, ...] ) VALUES ( <expression> [, ...] ) ];Parameters
<target>— the table being changed. Must be a catalog-backed table, and must be aliased.<source>— the table supplying the changes. Must be a table, and must be aliased.<condition>— how a source row is paired with a target row. Usually an equality on a key; multi-column conditions are supported.<predicate>— an extra condition on an arm. The arm only fires when it holds.WHEN MATCHED— applies to target rows the source paired with.WHEN NOT MATCHED— applies to source rows that paired with nothing.
At least one WHEN clause is required. Arms are evaluated in the order written: the
first arm whose condition holds is the one that fires.
What Each Row Does
Every row of the join resolves to exactly one action:
| Outcome | When | Effect |
|---|---|---|
| Nothing | The row paired with nothing and no NOT MATCHED arm fired, or it paired but no arm's condition held |
The target row is left exactly as it was |
| Insert | A NOT MATCHED arm fired |
A new row is added |
| Update | A WHEN MATCHED … UPDATE arm fired |
The old row is replaced |
| Delete | A WHEN MATCHED … DELETE arm fired |
The row is removed |
Target rows the source never mentions are not touched, not rewritten, and cost nothing.
Examples
Upsert
The common case — bring a table up to date from a table of changes:
MERGE INTO catalogue.security.vulnerabilities AS v
USING catalogue.security.vulnerabilities_delta AS d
ON v.cve = d.cve
WHEN MATCHED THEN UPDATE SET details = d.details
WHEN NOT MATCHED THEN INSERT (cve, details) VALUES (d.cve, d.details);Only Update What Actually Changed
A feed that republishes everything will re-write every row unless you say otherwise.
Guarding the MATCHED arm means an unchanged row is left alone entirely:
MERGE INTO catalogue.security.vulnerabilities AS v
USING catalogue.security.vulnerabilities_delta AS d
ON v.cve = d.cve
WHEN MATCHED AND v.details IS DISTINCT FROM d.details
THEN UPDATE SET details = d.details,
revision = v.revision + 1,
last_updated = CURRENT_TIMESTAMP
WHEN NOT MATCHED
THEN INSERT (cve, details, revision, last_updated)
VALUES (d.cve, d.details, 1, CURRENT_TIMESTAMP);Note v.revision + 1. An UPDATE arm can read the row it is replacing, which is what
MERGE offers that a delete followed by an insert cannot. Columns the SET list does
not mention keep their existing values.
Without the IS DISTINCT FROM guard, revision would count how many times the feed ran
rather than how many times the CVE changed.
Several Arms
Arms are tried in order, so put the more specific one first:
MERGE INTO catalogue.inventory.stock AS s
USING catalogue.inventory.movements AS m
ON s.sku = m.sku
WHEN MATCHED AND m.quantity = 0 THEN DELETE
WHEN MATCHED THEN UPDATE SET quantity = m.quantity
WHEN NOT MATCHED THEN INSERT (sku, quantity) VALUES (m.sku, m.quantity);Delete Only
Omit the other arms to remove every target row the source names:
MERGE INTO catalogue.sessions.active AS a
USING catalogue.sessions.expired AS e
ON a.session_id = e.session_id
WHEN MATCHED THEN DELETE;Insert Only
MERGE INTO catalogue.reference.countries AS c
USING catalogue.reference.countries_new AS n
ON c.iso_code = n.iso_code
WHEN NOT MATCHED THEN INSERT (iso_code, name) VALUES (n.iso_code, n.name);Inspecting the Plan
EXPLAIN shows what a MERGE will do without running it:
EXPLAIN
MERGE INTO catalogue.security.vulnerabilities AS v
USING catalogue.security.vulnerabilities_delta AS d
ON v.cve = d.cve
WHEN MATCHED THEN UPDATE SET details = d.details
WHEN NOT MATCHED THEN INSERT (cve, details) VALUES (d.cve, d.details);EXPLAIN ANALYZE is rejected for MERGE, because ANALYZE measures a statement by
running it — which would make inspecting a plan change your table.
Duplicate Source Rows
If a target row is paired with more than one source row, the statement would have to act on that row twice. That is rejected:
MERGE INTO cardinality violation: a row of <table> is matched by more than one
source row, so the statement would act on it twice. De-duplicate the source on
the ON key.
Nothing is written when this happens. De-duplicate the source first — for example with
GROUP BY or a window function picking one row per key — and re-run.
Concurrency
A MERGE is built against the version of the table it read. If another writer commits to
that table first, the statement is refused rather than published, because publishing
would drop their work:
Another writer committed to <table> while this statement was preparing its own
commit, so it was refused. Nothing was written. Re-run the statement - it will be
rebuilt against the current state of the relation.
Nothing is written, and re-running rebuilds against whatever is now current. It is not retried for you: whether the work is still valid depends on what the other writer did.
Limitations
MERGE is experimental. The following are rejected when the query is planned, not silently ignored:
- Both relations must be aliased.
MERGE INTO t AS x USING s AS y— an un-aliased relation is refused. - The source must be a table.
USING ( SELECT ... )is not supported; write the query's result to a table first. WHEN NOT MATCHED BY SOURCEis not supported. It acts on target rows the source never mentioned, which this implementation does not read.- An
INSERTarm must name its columns.INSERT (a, b) VALUES (...), never a bareVALUES, so the values cannot silently bind to different columns if the target's schema changes.INSERT ROWandINSERT *are not supported. - An
UPDATEarm cannot carry its ownWHERE. Put the condition on the arm:WHEN MATCHED AND <predicate> THEN UPDATE .... - One
VALUESrow perINSERTarm, and each column assigned at most once perUPDATEarm. - Catalog-backed tables only. A read-only or non-catalog connector is refused.
- The target's schema is not changed.
MERGEwrites rows; it does not add, drop or retype columns.
There is also a memory ceiling. A merge holds the address of every row it acts on until it commits, because the commit is atomic. An extremely large merge is refused rather than partially applied:
MERGE INTO ran out of address budget tracking which rows of <table> it has acted on.
Notes
- The whole statement is one commit. Readers see either all of its changes or none of them — never a half-applied merge.
- Replacing a row does not rewrite the file it lives in. The old row is marked as removed and the replacement written alongside, so the cost of a merge scales with how much changed, not with the size of the table. Removed rows are cleaned up by OPTIMIZE TABLE.
- A merge in which no arm fires for any row is a success that does nothing — it writes no new version of the table.
- Re-running the same merge against the same source is safe: the second run finds nothing left to change.
- A materialized view is not a table: this statement is rejected against one. Its
contents come from its defining
SELECT— see REFRESH MATERIALIZED VIEW.