2026-06-14
Under serializability, readers and writers fight for locks. Under snapshot isolation (SI), every transaction sees a consistent snapshot of the database as of the moment it started — no matter how many writers commit while it runs. Readers never block writers; writers never block readers. This is the default in PostgreSQL, Oracle, SQL Server (when enabled), and most modern OLTP databases.
The trick is multi-version concurrency control (MVCC): every row update writes a new version tagged with the transaction ID that created it. When transaction T starts, it gets a snapshot ID. Every read filters out versions created after that snapshot. Old versions stick around until no active transaction needs them, then get garbage-collected (Postgres calls this VACUUM).
Real-world example. A reporting query scans 50 million orders to compute daily revenue. It runs for 90 seconds. During those 90 seconds, 12,000 new orders are inserted and 400 existing orders are refunded. Under serializability, your report blocks every writer — or gets killed by lock timeouts. Under SI, the report sees exactly the database state at second zero. The 12,000 new inserts and 400 refunds are invisible to it, but they commit normally. Everyone wins — almost.
The catch: write skew. SI is not serializable. Two transactions can each read overlapping data, make decisions based on what they read, and write to disjoint rows — leaving the database in a state no serial order could produce. Classic example: hospital on-call scheduling. Rule: "at least one doctor must be on call." Alice and Bob both check (two on call: themselves). Both transactions see two doctors on call, both decide it's safe to remove themselves. Both commit. Now zero doctors are on call.
Rule of thumb. If your invariant depends on a row you're not writing, SI won't protect it. Fix options: (1) use SELECT ... FOR UPDATE to lock the rows you read, (2) use a database that offers Serializable Snapshot Isolation (Postgres's SERIALIZABLE mode detects dangerous read-write cycles and aborts one transaction), or (3) enforce the invariant with a unique constraint or check constraint where possible.
Cost. MVCC trades disk and CPU for concurrency. Each row update creates a dead tuple; under heavy write load, vacuum lag causes table bloat. Watch pg_stat_user_tables.n_dead_tup — when it exceeds ~20% of live tuples on a hot table, autovacuum is falling behind and queries will slow.
SELECT FOR UPDATE or true serializable mode when invariants span rows you only read.
