Two concurrent transactions can each read a valid database state, update different rows, and both commit without colliding on a written row. The final state can still violate a constraint that spans those rows. No lost update is required; each transaction can preserve every value written by the other and still produce an invalid result.
This anomaly is commonly called write skew. Its defining feature is that the conflict lives in the relationship among values rather than in two writes aimed at the same row. Isolation mechanisms that detect direct write-write conflicts therefore do not automatically protect every application invariant.
The invariant spans more state than either write
Consider two records representing whether two operators are available for an on-call role. The application requires at least one operator to remain available.
The initial state is valid:
operator_a = available
operator_b = availableTransaction T1 reads both records, sees that operator B remains available, and marks operator A unavailable. At the same time, T2 reads the same initial state, sees that operator A remains available, and marks operator B unavailable.
Their writes target different records:
T1: operator_a = unavailable
T2: operator_b = unavailableIf the isolation model permits both transactions to commit from their respective snapshots, the resulting state has no available operator. Each transaction made a decision from a state that satisfied the invariant, yet the combined result does not.
The anomaly differs from a lost update. In a lost update, competing writes target the same logical value and one update can overwrite another. Write skew can preserve both writes exactly. The invalid state emerges because each write depended on a predicate over data that the other transaction changed indirectly.
Row conflict detection sees only part of the dependency
A database can detect that two transactions attempt incompatible writes to the same row. Write skew can avoid that intersection entirely. T1 writes A; T2 writes B. At the row level, those mutations are disjoint.
The relevant dependency appears in the reads. T1’s decision to change A depends on the observed state of B. T2’s decision to change B depends on the observed state of A. Together, those dependencies form a cycle even though the write sets do not overlap.
This makes the transaction’s logical read set important. A statement such as count available operators >= 2 is not merely fetching data for display. Its result authorizes a later mutation. If another transaction changes rows that can alter that predicate before commit, the authorization basis can become stale.
Snapshot-based execution makes this especially visible. A transaction may continue to read from a stable snapshot while concurrent commits advance the current database state. That stable view prevents some inconsistent-read effects, but stability of a snapshot does not by itself prove that concurrent decisions made from equivalent snapshots can be combined safely.
A predicate can be the contested resource
The protected fact in the example is not either operator row in isolation. It is the predicate that at least one qualifying row exists after the transaction commits.
This distinction matters for constraints expressed as counts, ranges, absence checks, quotas, or combinations of records. A transaction can read a set defined by a predicate and later modify one member while another transaction modifies a different member. If both decisions rely on the original membership or count, disjoint row writes can still conflict at the invariant level.
A uniqueness constraint illustrates a related boundary from another direction. When the database has a declared unique constraint, the storage engine can reject two committed rows carrying the same constrained key even if application code first performs an absence check. The invariant is represented inside the database as an enforceable constraint.
Many cross-row rules do not map to a simple built-in constraint. Their protection then depends on transaction isolation, explicit locking, schema design, or another serialization mechanism capable of covering the logical state used for the decision.
Locking only the rows being changed can miss the dependency
A transaction that locks A before updating A prevents another transaction from making an incompatible change to A through the same locking discipline. It does not automatically protect B, even when B supplied the evidence that made the change to A permissible.
For the on-call invariant, locking only each transaction’s target row leaves the two transactions free to proceed independently. A locking design has to cover the state whose relationship is being protected. That can mean locking all relevant rows, locking a parent or aggregate row that represents the invariant, or relying on database facilities that track conflicts over predicates or ranges.
The appropriate mechanism depends on the database and access pattern. Broad locks can serialize more work than the invariant strictly requires. Narrow locks can leave gaps when the decision depends on rows outside the locked set. The important boundary is semantic: lock coverage has to correspond to the state that authorizes the mutation, not merely the row receiving the mutation.
Serializable execution changes the admissible histories
Serializable isolation aims to make committed transactions equivalent to some serial execution, even when their physical execution overlaps. For a write-skew pair that cannot both appear in any valid serial order with the same observations, a serializable implementation must prevent both from committing as if each ran against the original state.
The mechanism varies by database. Some systems use locking schemes that protect ranges or predicates. Others detect dependency patterns and abort a transaction when allowing every participant to commit would admit a non-serializable history. The application therefore cannot infer the exact mechanism from the isolation level name alone; concrete database documentation defines the implementation and any operational conditions attached to it.
An abort is not evidence that the database failed to provide isolation. Under conflict-detecting serializable schemes, aborting one participant can be the mechanism that preserves the serializable result. Application code using such a mode must treat serialization failures according to the database’s documented transaction semantics.
Serializable isolation also does not make every business rule automatic. The transaction still has to read the state on which its decision depends and perform the relevant mutation inside the protected transaction. Data never consulted by the transaction cannot contribute a dependency merely because an unstated application rule exists.
Schema shape can expose or hide the conflict
The physical representation of an invariant influences which conflicts become visible to the database. If a cross-row condition is represented by a single aggregate record that every relevant transaction must update, concurrent operations may acquire a direct write conflict on that record. The same logical condition spread only across independent rows can leave no common write target.
That does not make denormalizing an invariant into one row universally preferable. A shared record can become a serialization point, and maintaining derived state introduces its own consistency obligations. It does show that schema design and concurrency control are connected: representation determines which logical relationships correspond to concrete database objects and constraints.
Declarative constraints are valuable when the database can express the rule directly. Explicit locks can make an application-defined serialization point visible. Serializable isolation can cover broader dependency structures without requiring every invariant to collapse into one row. Each approach places the conflict boundary in a different part of the system.
Conflict boundaries have to match decision boundaries
Write skew exposes a gap between physical mutation and logical dependency. Two transactions may have disjoint write sets while competing over the same decision predicate. Looking only for rows written by both transactions misses that relationship.
The durable question is therefore not whether concurrent transactions touch the same row. It is whether the database’s concurrency mechanism represents every piece of state that made each mutation admissible. When that representation covers only direct writes, cross-row invariants can sit outside the conflict boundary. When predicates, locks, constraints, or serializable dependency tracking bring the decision state inside that boundary, the database has enough information to reject an incompatible concurrent history.