Skip to content

/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:

bash
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.mjs

Output

[{"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

sql
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