TL;DR — Snapshot isolation (often built on MVCC) gives each reader a consistent view of the data without acquiring shared locks. Because reads operate on a point‑in‑time version chain, they never block writers, and writers never block readers, dramatically reducing lock contention in high‑concurrency workloads.

Databases that rely on row‑level locking can quickly become bottlenecks when many clients issue overlapping read queries. Modern engines solve this problem by taking snapshots of the data state at transaction start, allowing reads to proceed without waiting for locks. This article explains the theory behind snapshots, how they are implemented in popular RDBMSs, and why they effectively prevent locking during concurrent reads.

Understanding Database Locking

What Locks Do

Locks are a concurrency control primitive that enforce serializability—the guarantee that the outcome of concurrent transactions is equivalent to some sequential order. In a traditional two‑phase locking (2PL) system, a transaction:

  1. Acquires shared (S) locks for reads.
  2. Acquires exclusive (X) locks for writes.
  3. Holds all locks until the transaction commits or aborts.

While this approach is simple and safe, it forces readers to wait for writers that hold X‑locks, and writers to wait for readers holding S‑locks. The result is lock contention, especially under heavy read‑write mixes.

When Locks Cause Contention

Consider an e‑commerce site where thousands of users browse product listings while a background job updates inventory levels. If each SELECT statement acquires an S‑lock on the rows it reads, the inventory update must wait for every ongoing browse transaction to finish. The latency spikes dramatically, and the shop appears sluggish.

Real‑world systems often see:

  • Lock queues that grow linearly with concurrent sessions.
  • Deadlocks when two transactions each hold locks the other needs.
  • Throughput collapse under high read‑write concurrency.

These symptoms drive the need for lock‑free read paths, which snapshots provide.

Snapshot Isolation Fundamentals

MVCC Basics

Multi‑Version Concurrency Control (MVCC) is the engine behind most snapshot implementations. Instead of a single mutable copy of a row, the database stores multiple versions, each tagged with:

  • xmin – the transaction ID that created the version.
  • xmax – the transaction ID that deleted or superseded the version (or NULL if still visible).

When a transaction starts, it receives a snapshot—a list of active transaction IDs at that moment. Visibility rules then determine which version of each row the transaction can see:

  • A version is visible if xmin ≤ snapshot_id and (xmax is NULL or xmax > snapshot_id).

Because visibility is computed from metadata, a reader never needs to lock the row; it simply reads the appropriate version.

How Snapshots Are Created

At the beginning of a transaction (or at the first query in autocommit mode), the engine records:

-- PostgreSQL example
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT txid_current_snapshot();

The returned snapshot contains:

  • The current transaction ID.
  • A list of in‑flight transaction IDs that started earlier and have not yet committed.

All subsequent reads use this snapshot to decide which row versions are visible, regardless of later writes. Writers, on the other hand, create new row versions with a fresh xmin, leaving existing versions untouched for readers.

Why Snapshots Prevent Locking During Reads

Read Consistency Without Locks

Because each row version is immutable after creation, a reader can safely scan the table without acquiring S‑locks. The engine merely filters out invisible versions according to the snapshot. Even if a writer concurrently inserts a new version, the reader’s snapshot excludes it, guaranteeing repeatable read semantics.

This behavior eliminates the classic lock‑wait scenario:

Reader (Snapshot)Writer (New Version)
Sees version V1Inserts V2 (xmin = T2)
Continues using V1No impact on reader

The reader never blocks on the writer, and the writer never blocks on the reader.

Version Chains and Garbage Collection

Over time, old versions become obsolete once no active snapshot can see them. Databases run vacuum or garbage collection processes to reclaim space:

# PostgreSQL vacuum example
VACUUM (VERBOSE, ANALYZE);

Since readers never hold locks on rows, the cleanup can proceed concurrently, further reducing contention.

Practical Implementations Across Engines

PostgreSQL

PostgreSQL pioneered MVCC with snapshot isolation as its default for READ COMMITTED and REPEATABLE READ. A typical read‑only transaction looks like:

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT id, price FROM products WHERE category = 'books';
-- No explicit LOCK statements needed
COMMIT;

Under the hood, PostgreSQL:

  • Stores each tuple version in the heap with xmin/xmax.
  • Uses CLOG (commit log) to track transaction commit status.
  • Runs autovacuum to prune dead tuples.

The official docs explain the mechanism in detail: PostgreSQL Concurrency Control.

MySQL InnoDB

InnoDB implements MVCC for READ COMMITTED and REPEATABLE READ (the default). Row versions are stored in the undo log. A read‑only transaction:

START TRANSACTION READ ONLY;
SELECT * FROM orders WHERE status = 'pending';
COMMIT;

InnoDB’s consistent read uses the snapshot taken at the start of the statement (for READ COMMITTED) or the transaction (for REPEATABLE READ). The undo log entries are reclaimed by the purge thread after no active snapshot references them. See the MySQL reference: InnoDB Consistent Read.

SQL Server

SQL Server offers snapshot isolation as an optional database setting (ALTER DATABASE SET ALLOW_SNAPSHOT_ISOLATION ON). When enabled, a transaction can request it:

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRAN;
SELECT * FROM Inventory WHERE Quantity > 0;
COMMIT TRAN;

SQL Server stores row versions in tempdb. Readers access the version that was current when the transaction began, while writers insert new versions. The version store is cleaned up automatically once no active snapshot references it. Microsoft’s docs provide a deep dive: Snapshot Isolation in SQL Server.

Common Pitfalls and Misconceptions

Phantom Reads vs. True Snapshots

A phantom read occurs when a transaction sees a different set of rows in two identical queries because another transaction inserted or deleted rows that match the predicate. Snapshot isolation prevents phantoms only at the REPEATABLE READ level when the engine uses predicate locking (as in PostgreSQL’s SERIALIZABLE mode). In plain snapshot isolation, new rows can appear in later queries, which is acceptable for many workloads but not for strict serializability.

Write Skew

Snapshot isolation can allow write skew, a phenomenon where two concurrent transactions read overlapping data, make non‑conflicting writes, and together violate a business rule. For example, two doctors each check that the other is on call before taking a night shift, leading to no doctor on call. Detecting and preventing write skew requires higher isolation (SERIALIZABLE) or explicit application‑level checks.

Performance Considerations

Space Overhead

Maintaining multiple versions consumes additional storage:

  • Undo logs (InnoDB) or version stores (SQL Server) can grow proportionally to write traffic.
  • VACUUM (PostgreSQL) may need frequent runs to keep table bloat low.

Administrators should monitor version store size and tune parameters like max_wal_size (PostgreSQL) or innodb_max_undo_log_size (MySQL).

Impact on Long‑Running Transactions

Long‑running read‑only transactions hold onto old snapshots, preventing the cleanup of obsolete versions. This can cause:

  • Transaction ID wraparound in PostgreSQL, requiring periodic VACUUM FREEZE.
  • Purge lag in InnoDB, leading to increased disk usage.
  • Tempdb pressure in SQL Server.

Best practice: keep snapshot transactions short, or use statement‑level snapshots (READ COMMITTED) when possible.

Key Takeaways

  • Snapshot isolation provides lock‑free reads by giving each transaction a point‑in‑time view of the data.
  • MVCC stores immutable row versions, allowing readers to filter visible rows without acquiring shared locks.
  • Major RDBMSs (PostgreSQL, MySQL InnoDB, SQL Server) implement snapshots using undo logs, version stores, or tuple metadata.
  • Garbage collection (VACUUM, purge, tempdb cleanup) reclaims old versions once no active snapshot needs them.
  • Pitfalls such as phantom reads and write skew require higher isolation levels or careful application logic.
  • Performance trade‑offs include extra storage and the need to manage long‑running transactions to avoid version bloat.

Further Reading