{"post":{"slug":"upsert-patterns-explained-2026","title":"Upsert Patterns: Insert or Update Without a Race","excerpt":"Checking whether a row exists and then inserting it is a race condition with good intentions. The database has a single-statement answer, with sharp edges.","category":"code","date":"August 17, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">T</span>he code checks whether a user with this email exists. It does not, so the code inserts one. This is correct, obvious, and wrong under any concurrency at all.\n\nTwo requests arrive milliseconds apart. Both check, both find nothing, both insert. One succeeds and one hits a unique constraint violation — or, if there is no constraint, you now have two users with the same email and a data problem that will surface much later.\n\nThe window between the check and the insert is the bug, and no amount of care in application code closes it.\n\n## What changed in 2026\n\n- **Upsert support converged.** Mainstream databases settled on comparable semantics, even where the syntax differs, which made the pattern portable in practice.\n- **Idempotency requirements spread.** Retry-heavy distributed systems made \"apply this write exactly once\" a routine need rather than a special case.\n- **Bulk upsert deadlocks became better understood.** The ordering fix moved from folklore to documented practice.\n- **Sequence gap surprises persisted.** The behaviour where conflicts consume identifiers continued to catch teams out, particularly in tests.\n\n## What upsert does\n\nA single statement that inserts a row, and if it conflicts with an existing one on a unique constraint, updates instead. Atomic, with no window between checking and acting.\n\nThe essential requirement: **there must be a unique constraint or unique index to conflict on.** The database detects the conflict through that constraint. Without one, there is nothing to detect and no conflict to handle — this is the most common reason an upsert does not behave as expected.\n\nThree broad behaviours are available:\n\n**Update on conflict.** The common case — insert, or overwrite the existing row's columns with the new values.\n\n**Do nothing on conflict.** Insert if absent, silently skip if present. Useful for idempotent inserts where you do not want to overwrite anything.\n\n**Conditional update.** Update only when a condition holds — for instance, only if the incoming version is newer than the stored one. This is how you avoid an out-of-order retry overwriting fresher data with stale data.\n\nThat last one matters more than it looks in any system with retries, where a delayed message can arrive after a newer one and quietly undo it.\n\n## The sharp edges\n\n**Sequence gaps.** In many engines, an insert that conflicts has already consumed a value from the sequence backing the primary key. The row is not inserted; the identifier is gone. Your IDs develop gaps proportional to your conflict rate.\n\nThis is harmless if IDs are opaque and alarming if anyone treats them as a count or expects contiguity. Tests asserting on specific ID values are the usual casualty.\n\n**Bulk upsert deadlocks.** Two concurrent batches upserting overlapping keys in different orders will deadlock — batch A locks key 1 then waits for key 2 while batch B holds key 2 and waits for key 1.\n\nThe fix is simple and easy to forget: **sort each batch by the conflict key before sending it.** Every batch then acquires locks in the same order, and the cycle cannot form. This is one of those one-line changes that eliminates an entire class of intermittent production failure.\n\n**Triggers fire differently.** An upsert that updates fires update triggers, not insert triggers. Logic that assumed every new record passes through an insert trigger will silently miss rows.\n\n**Knowing which happened.** By default you often cannot tell whether a given row was inserted or updated. Most engines can return that information if you ask — and if your logic depends on it, ask explicitly rather than inferring from an affected-row count, which varies by engine.\n\n## Common mistakes\n\n- **Select-then-insert.** The race this pattern exists to eliminate.\n- **No unique constraint.** Nothing to conflict on, so nothing works.\n- **Unsorted bulk batches.** Intermittent deadlocks under concurrency.\n- **Unconditional overwrite in a retry-heavy system.** A late retry clobbers newer data; guard with a version or timestamp condition.\n- **Expecting contiguous IDs.** Conflicts consume sequence values.\n- **Assuming insert triggers always fire.** Updates take the update path.\n- **Upserting a huge batch in one statement.** A long transaction holding many locks; chunk it.\n\n## FAQ\n\n**Is upsert the same as idempotency?**\nIt is a tool for achieving it, not the same thing. An upsert keyed on a stable identifier makes repeating a write safe, which is exactly what retry logic needs — see [idempotency explained](/blog/idempotency-explained-2026). Idempotency is the property; upsert is one way to get it.\n\n**What about `MERGE`?**\nStandard SQL's more general statement, supported by a number of engines, capable of insert, update, and delete in one operation. More expressive, more verbose, and historically more prone to subtle concurrency issues in some implementations. For simple insert-or-update, the dedicated upsert syntax is usually clearer.\n\n**Does it work with partial unique indexes?**\nGenerally yes, and it is a useful combination — upserting against uniqueness that only applies to live rows, for instance. Check your engine's syntax for specifying which index to conflict on. See [partial indexes](/blog/partial-indexes-explained-2026).\n\n**Can it deadlock with a single row?**\nNot with itself. Deadlocks come from multiple rows locked in inconsistent orders, which is why sorting fixes it.\n\n**What about soft-deleted rows?**\nThey still occupy the unique value unless your constraint is partial, so an upsert will update a deleted row rather than inserting a new one. Frequently surprising — see [soft delete patterns](/blog/soft-delete-patterns-2026).\n\n## Where to go next\n\nFor the retry safety upsert supports, read [idempotency explained](/blog/idempotency-explained-2026). For the concurrency anomalies underneath, [database isolation levels](/blog/database-isolation-levels-2026), and for constraints that apply to a subset of rows, [partial indexes](/blog/partial-indexes-explained-2026).","hue":318,"metaTitle":"Upsert Patterns: Insert or Update Without a Race","metaDescription":"Why select-then-insert races under concurrency, how upsert handles conflicts atomically, and the sequence gaps and deadlocks that surprise people.","keywords":["upsert","insert on conflict","merge statement sql","race condition insert","unique constraint conflict","bulk upsert deadlock","idempotent writes","database concurrency 2026"],"tldr":["**Select-then-insert is a race** — two requests both see nothing, both insert, one gets a constraint violation.","**Upsert needs a unique constraint to conflict on** — without one, there is nothing for the database to detect.","**A failed upsert can still consume an ID** — sequences advance on conflict in many engines, leaving gaps.","**Bulk upserts can deadlock** — sort your batch by key so concurrent batches lock in the same order.","**Skip:** upsert when you need to know whether you inserted or updated. Ask for it explicitly; do not infer it."],"readTime":"4 min read","wordCount":925},"related":[{"slug":"race-conditions-explained-2026","title":"Race conditions explained in 2026: when timing breaks code","excerpt":"A race condition is a bug whose outcome depends on the timing of concurrent operations. Here is the 2026 guide to how they happen, why they are so hard to catch, and the primitives that prevent them.","category":"code","date":"June 30, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":320},{"slug":"what-is-concurrency-2026","title":"What is concurrency in 2026? A plain-language guide with examples","excerpt":"Concurrency is structuring a program to handle many tasks at once. This guide explains how it differs from parallelism, why it matters for fast software, and the common pitfalls like race conditions.","category":"code","date":"June 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":140},{"slug":"exclusion-constraints-explained-2026","title":"Exclusion Constraints: Preventing Overlaps in the Database","excerpt":"Two bookings for the same room at overlapping times is a rule no unique constraint can express. An exclusion constraint enforces it, atomically, without application-level locking.","category":"code","date":"January 12, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":302}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"ufs-4-1-storage-explained-2026","title":"UFS 4.1 Explained: Why Phone Storage Speed Matters","excerpt":"Two phones with identical processors can feel very different, and storage is often why. Here is what UFS 4.1 changes and when you would actually notice.","category":"tech","date":"August 17, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":244},"older":{"slug":"vision-language-models-explained-2026","title":"Vision-Language Models Explained: How They See","excerpt":"Vision-language models turn images into tokens a language model can read. Here is what that architecture means for accuracy, resolution limits, and cost per image.","category":"ai","date":"August 17, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":222}}