Use a single INSERT ... ON CONFLICT DO UPDATE so the DB performs the insert-or-update atomically (no separate SELECT). Example: maintain inventory by SKU, incrementing qty and setting last_seen to the latest timestamp.sql
-- Ensure unique index on sku for conflict target
CREATE TABLE inventory (
sku text PRIMARY KEY, -- unique index on sku
qty bigint NOT NULL DEFAULT 0,
last_seen timestamptz
);
-- Upsert: add incoming_qty, update qty atomically, and update last_seen only if newer
INSERT INTO inventory (sku, qty, last_seen)
VALUES ('ABC-123', 5, now())
ON CONFLICT (sku) DO UPDATE
SET
qty = inventory.qty + EXCLUDED.qty, -- atomic increment
last_seen = GREATEST(inventory.last_seen, EXCLUDED.last_seen) -- avoid overwriting newer timestamp
RETURNING *;
Why this works- Conflict target: you need a unique constraint or index on the column(s) you use in ON CONFLICT — here PRIMARY KEY or UNIQUE(sku).- The DB executes the INSERT and, if a unique violation occurs on sku, applies the DO UPDATE in the same statement and lock scope, avoiding classic check-then-insert races.Concurrency implications & race conditions- ON CONFLICT DO UPDATE is atomic at statement level: it acquires the necessary row-level lock to avoid two sessions inserting the same sku from both succeeding inserts. This prevents the "lost insert" race you get when doing separate SELECT then INSERT.- However, concurrent ON CONFLICT updates can still contend on the same row (serialization, row locks). If two transactions concurrently run the above and both try to add qty, they will serialize their updates — one will wait, then apply, so the qty increments correctly if you use inventory.qty + EXCLUDED.qty as above.- If you have more complex logic or care about ordering of last_seen, use expressions like GREATEST(...) to avoid stale overwrites.Further robustness under high parallelism- Use a single upsert statement rather than SELECT-then-INSERT.- Keep ON CONFLICT update logic idempotent and deterministic (no non-deterministic side-effects).- For workloads with heavy contention on the same key: - Consider batching updates by key to reduce contention. - Use application-side sharding of hot keys. - Use advisory locks for very strict serialization (pg_advisory_lock / pg_try_advisory_lock) around the key when necessary: - Acquire a consistent hash-based advisory lock, perform upsert, release — but be careful: locks add latency and reduce concurrency. - Alternatively, use higher isolation (SERIALIZABLE) and implement retry logic for serialization failures (catch SQLSTATE '40001').Performance tips- Ensure the conflict target column(s) have a supporting unique index to avoid full-table locks.- Reduce work in DO UPDATE; only update columns when needed via a WHERE clause in DO UPDATE (Postgres supports WHERE in DO UPDATE), e.g.:sql
ON CONFLICT (sku) DO UPDATE
SET qty = inventory.qty + EXCLUDED.qty,
last_seen = EXCLUDED.last_seen
WHERE EXCLUDED.last_seen > inventory.last_seen;
This prevents unnecessary WAL churn and triggers.Summary- Create UNIQUE/PK on sku, use single INSERT ... ON CONFLICT (sku) DO UPDATE with atomic expressions, use GREATEST/WHERE to avoid stomping newer data, and for heavy contention add batching, sharding, advisory locks, or retry-on-serialization strategies.