Upsert Quick Reference
Upsert Quick Reference
Available since: v3.9.5 (2026-04-10) — native planner / executor implementation; no handler-level INSERT-catch-UPDATE workaround. Verified against: v3.19.1.
UVP
Two SQL dialects, one engine: INSERT … ON CONFLICT (col) DO UPDATE SET col = EXCLUDED.col (PostgreSQL) and INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col) (MySQL) both run in the same database, on the same table, against the same rows. The MySQL translator rewrites ON DUPLICATE KEY UPDATE into the canonical PG form internally — there’s a single OnConflictAction::DoUpdate path with full EXCLUDED.col reference resolution. Migrate from MySQL to Postgres-flavour incrementally without rewriting upserts.
10 regression tests cover the full matrix: DO NOTHING, DO UPDATE, EXCLUDED references, multi-column conflicts, partial update, no-conflict insert. Implementation in src/sql/planner.rs::convert_on_conflict.
PostgreSQL Form — ON CONFLICT
DO NOTHING
Insert if not present; silently skip on conflict:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT UNIQUE NOT NULL, name TEXT);
INSERT INTO users (email, name) VALUES ('alice@x.com', 'Alice')ON CONFLICT (email) DO NOTHING;If email = 'alice@x.com' already exists, the row is unchanged. Affected rows = 0.
DO UPDATE SET … = EXCLUDED.…
The “real” upsert. EXCLUDED refers to the proposed row that conflicted:
INSERT INTO users (email, name) VALUES ('alice@x.com', 'Alice Updated')ON CONFLICT (email) DO UPDATESET name = EXCLUDED.name;Behaviour:
- If no row with
email = 'alice@x.com'→ INSERT. - If a row exists → UPDATE with
name = 'Alice Updated'.
Combining with column expressions
EXCLUDED can mix with the existing column value:
CREATE TABLE counters ( key TEXT PRIMARY KEY, count INTEGER DEFAULT 0, updated_at TIMESTAMP DEFAULT now());
INSERT INTO counters (key, count) VALUES ('signups', 1)ON CONFLICT (key) DO UPDATESET count = counters.count + EXCLUDED.count, updated_at = now();counters.count is the existing value, EXCLUDED.count is the proposed value.
Composite unique constraints
CREATE TABLE tags ( user_id INT, tag TEXT, weight REAL DEFAULT 1.0, UNIQUE (user_id, tag));
INSERT INTO tags (user_id, tag, weight) VALUES (42, 'rust', 0.5)ON CONFLICT (user_id, tag) DO UPDATESET weight = tags.weight + EXCLUDED.weight;Bulk upsert
INSERT INTO users (email, name) VALUES ('alice@x.com', 'Alice'), ('bob@x.com', 'Bob'), ('carol@x.com', 'Carol')ON CONFLICT (email) DO UPDATESET name = EXCLUDED.name;Each row is evaluated independently for conflict.
DO UPDATE with RETURNING
INSERT INTO users (email, name) VALUES ('alice@x.com', 'Alice v2')ON CONFLICT (email) DO UPDATESET name = EXCLUDED.nameRETURNING id, email, name;Returns the row regardless of whether it was inserted or updated.
MySQL Form — ON DUPLICATE KEY UPDATE
The same operation in MySQL syntax — works in the same database against the same tables:
INSERT INTO users (email, name) VALUES ('alice@x.com', 'Alice')ON DUPLICATE KEY UPDATE name = VALUES(name);VALUES(col) in MySQL is the equivalent of EXCLUDED.col in PG. The MySQL translator rewrites this to the canonical PG form (src/sql/planner.rs, convert_on_conflict).
Counter increment, MySQL style
INSERT INTO counters (key, count) VALUES ('signups', 1)ON DUPLICATE KEY UPDATE count = count + VALUES(count);Composite unique key
CREATE TABLE tags ( user_id INT, tag TEXT, weight REAL DEFAULT 1.0, UNIQUE KEY user_tag (user_id, tag));
INSERT INTO tags (user_id, tag, weight) VALUES (42, 'rust', 0.5)ON DUPLICATE KEY UPDATE weight = weight + VALUES(weight);The UNIQUE KEY syntax (MySQL) is parsed and creates the same unique index that PG UNIQUE produces.
Mixing the Dialects
Both forms target the same unique constraints. You can issue PG-form upserts and MySQL-form upserts in the same session, against the same table, in any order:
-- Connection 1 (PG-flavour client)INSERT INTO users (email, name) VALUES ('alice@x.com', 'Alice')ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;
-- Connection 2 (MySQL-flavour client)INSERT INTO users (email, name) VALUES ('bob@x.com', 'Bob')ON DUPLICATE KEY UPDATE name = VALUES(name);Both rows end up in the same table with the same constraint behaviour.
What Counts as a Conflict?
The conflict target is any unique constraint — primary key or UNIQUE index — that the proposed row violates. v3.9.4 fixed the conflict-detection path so:
PRIMARY KEYviolations trigger ON CONFLICT (was missed pre-3.9.4 — only checkedunique_indexes).- Composite
UNIQUE (a, b)triggers when bothaandbmatch. - Columns must be quoted-or-not consistently with the table definition.
For PG ON CONFLICT (col), the explicit conflict target must match a unique constraint. For MySQL ON DUPLICATE KEY UPDATE, any unique-key violation matches.
Verification
-- Idempotent insertINSERT INTO users (email, name) VALUES ('test@x.com', 'Test')ON CONFLICT (email) DO NOTHING;INSERT INTO users (email, name) VALUES ('test@x.com', 'Test 2')ON CONFLICT (email) DO NOTHING;SELECT name FROM users WHERE email = 'test@x.com';-- 'Test' (not 'Test 2')
-- Upsert produces the new valueINSERT INTO users (email, name) VALUES ('test@x.com', 'Test 3')ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;SELECT name FROM users WHERE email = 'test@x.com';-- 'Test 3'Common Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
ON CONFLICT (col) without UNIQUE on col | Parse error | Add UNIQUE constraint or PRIMARY KEY on the column |
EXCLUDED.col references base column | column "col" does not exist | Qualify with EXCLUDED.col for proposed, tablename.col for existing |
MySQL INSERT IGNORE not used here | Errors not silenced | Use ON CONFLICT DO NOTHING or ON DUPLICATE KEY UPDATE col = col |
| Conflict on PRIMARY KEY missed (pre-3.9.4) | Duplicate insert silently succeeded | Upgrade to v3.9.5+ — PK is now a real conflict target |
| Upsert in transaction sees stale rows | Read isolation surprise | Use FOR UPDATE (PG) or rely on default snapshot — both work |
| Multi-row upsert with conflicting batch | Only first conflict caught | One conflict per row — second matching key in same INSERT does proceed correctly |
Where Next
- WINDOW_FUNCTIONS_QUICKREF — common partner of upserts in event-stream pipelines.
- BAAS_REST_API_TUTORIAL —
Prefer: resolution=merge-duplicatesheaders map to upsert behaviour. - EXPLAIN_AND_OPTIMIZATION_TUTORIAL — confirm your conflict target hits an index.
CHANGELOG references: v3.9.5 (native ON CONFLICT planner/executor + EXCLUDED resolution + 10 upsert tests), v3.9.4 (PK + composite UNIQUE in conflict detection), v3.9.0 (LAST_INSERT_ID for upsert returning), v3.19.1 (current verified release).