/commands/postgresql/insert-on-conflict-upsert
PostgreSQL upsert: INSERT … ON CONFLICT DO UPDATE and MERGE
verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated 2026-10-20
- tool
- PostgreSQL
- versions
- PostgreSQL 9.5+ (ON CONFLICT), PostgreSQL 15+ (MERGE), PostgreSQL 17+ (MERGE RETURNING)
- os
- any
INSERT ... ON CONFLICT (key) DO UPDATE inserts a row, or updates the existing row when the insert would violate a unique constraint on key. EXCLUDED refers to the row you tried to insert. ON CONFLICT DO NOTHING silently skips duplicates. Both are atomic, so they are safe under concurrent inserts.
Insert, update, skip
PostgreSQL 18 in-process with PGlite:
npm init -y >/dev/null
npm install --silent --no-audit --no-fund @electric-sql/pglite@0.5.10 >/dev/null 2>&1
cat > q.mjs <<'EOF'
import { PGlite } from "@electric-sql/pglite";
const db = new PGlite();
const show = async (sql) => console.log(JSON.stringify((await db.query(sql)).rows));
await db.query(`create table users (id int primary key, email text, logins int default 0)`);
const upsert = `insert into users (id, email, logins) values (1, $1, 1)
on conflict (id) do update set email = excluded.email, logins = users.logins + 1
returning *`;
console.log(JSON.stringify((await db.query(upsert, ["a@example.com"])).rows));
console.log(JSON.stringify((await db.query(upsert, ["b@example.com"])).rows));
await show(`insert into users (id, email) values (1, 'c@example.com') on conflict do nothing returning *`);
await show(`merge into users u
using (values (1, 'm@example.com'), (2, 'n@example.com')) as s(id, email) on u.id = s.id
when matched then update set email = s.email
when not matched then insert (id, email) values (s.id, s.email)
returning merge_action(), u.id, u.email`);
EOF
node q.mjsOutput
[{"id":1,"email":"a@example.com","logins":1}]
[{"id":1,"email":"b@example.com","logins":2}]
[]
[{"merge_action":"UPDATE","id":1,"email":"m@example.com"},{"merge_action":"INSERT","id":2,"email":"n@example.com"}]The first call inserts, the second updates the same row (logins 2), DO NOTHING returns no row, and MERGE reports the action taken for each source row.
Syntax
INSERT INTO table (cols) VALUES (...)
ON CONFLICT (unique_col [, ...]) -- or ON CONFLICT ON CONSTRAINT name
DO UPDATE SET col = EXCLUDED.col
[WHERE table.updated_at < EXCLUDED.updated_at] -- update only when newer
RETURNING *;ON CONFLICT or MERGE?
- ON CONFLICT needs a unique index or constraint on the conflict columns and is the safest choice for concurrent upserts.
- MERGE (PostgreSQL 15+) can also delete, and works without a unique constraint, but concurrent MERGEs inserting the same key can still raise
23505.
Sources
Related pages
errors · postgresqlPostgreSQL: duplicate key value violates unique constraintPostgreSQL raises duplicate key value violates unique constraint (SQLSTATE 23505) when a key repeats. Read the DETAIL line, then upsert or reset the sequence.changes · postgresqlPostgreSQL 17 to 18: uuidv7, virtual generated columns, OLD and NEW in RETURNINGPostgreSQL 18 adds uuidv7(), makes generated columns virtual by default, lets RETURNING read OLD and NEW values, adds async I/O and deprecates MD5 passwords.codes · postgresqlPostgreSQL SQLSTATE error codes: 23505, 23503, 42P01, 40001 and moreThe PostgreSQL SQLSTATE codes apps handle most: 23505 unique violation, 23503 foreign key, 23502 not null, 42P01 missing table, 40001 serialization failure.