Articles

What Is MVCC? Multi-Version Concurrency Control

MVCC lets readers and writers work on a database concurrently without blocking each other, by keeping multiple versions of each row instead of locking it.

The Lycoris Team The Lycoris Team · · 4 min read
Abstract illustration representing stacked database cylinders

MVCC, or multi-version concurrency control, is a technique databases use to let reads and writes happen concurrently without blocking each other. Instead of a reader waiting for a writer to finish (or vice versa), MVCC keeps multiple versions of each row and hands each transaction a consistent snapshot of the data as it existed at a specific point in time — so a long-running read never has to wait on, or be blocked by, a write happening at the same moment.

The problem MVCC solves

The naive way to keep concurrent transactions from corrupting each other is locking: a writer takes an exclusive lock on a row, and any reader or writer that wants that row has to wait until the lock is released. This works, but it creates contention — a slow analytical query holding a read lock can stall unrelated writes, and a long transaction can back up an entire table’s worth of traffic behind it.

MVCC sidesteps this by never making readers and writers block each other. A write doesn’t overwrite a row in place; it creates a new version of that row, tagged with the transaction that created it. A read doesn’t take a lock; it simply reads whichever version of each row was current at the moment its own transaction began. Readers see a consistent snapshot even while writers are actively creating new versions underneath them, and writers don’t have to wait for readers to finish before proceeding.

How versioning actually works

Most MVCC implementations tag each row version with transaction metadata — commonly a creation transaction ID and an expiration transaction ID (left open for the current live version). When a row is updated:

  1. The old version isn’t deleted immediately. It’s marked as expired by the updating transaction’s ID.
  2. A new version is inserted, marked as created by that same transaction ID.
  3. Other transactions determine which version is “theirs” to see by comparing their own snapshot — typically the set of transactions that had already committed when they started — against each row version’s creation and expiration IDs.

A transaction that started before an update sees the old version; one that started after sees the new one. Both can run at the same time without either blocking the other, because they’re each looking at a version consistent with their own snapshot rather than fighting over a single shared row.

Snapshot isolation and read consistency

The practical benefit of MVCC is a form of read consistency often called snapshot isolation: a transaction sees the database as it existed at a single point in time, for its entire duration, regardless of what other transactions commit in the meantime. This is closely related to database isolation levels — MVCC is the mechanism that makes levels like Repeatable Read and Snapshot Isolation efficient to implement without heavy locking, though it doesn’t automatically grant full serializability; some anomalies (like write skew) can still occur under snapshot isolation and require additional checks in the database engine to prevent.

MVCC is why a long-running report query doesn’t need to block writes to the tables it’s reading, and why writes don’t need to wait for that report to finish — a very different experience from lock-based systems, where a slow read can visibly stall unrelated write traffic.

The cost: old versions have to go somewhere

Keeping multiple versions of every row isn’t free. Old, no-longer-visible versions accumulate as “dead” rows until nothing could possibly need them anymore — no active transaction’s snapshot still references them. Databases that use MVCC need a garbage-collection process to reclaim that space; PostgreSQL’s is called VACUUM, and other MVCC databases have their own equivalents. Skipping or falling behind on this cleanup causes table and index bloat, which degrades performance over time even though the logical data hasn’t grown — a maintenance cost that’s specific to MVCC and doesn’t exist in pure lock-based systems.

MVCC vs traditional locking

MVCCLock-based concurrency
Readers block writersNoOften, depending on isolation level
Writers block readersNoOften, depending on isolation level
Storage overheadMultiple row versions, needs cleanupSingle version per row
Read consistencySnapshot as of transaction startDepends on lock duration and level
Common inPostgreSQL, MySQL/InnoDB, Oracle, SQLite (WAL mode)Older or simpler engines, some in-memory stores

Most mainstream relational databases use MVCC today precisely because read-heavy and write-heavy workloads coexisting on the same tables is the common case, not the exception, and lock contention between them scales badly. It’s a core reason engines like PostgreSQL can sustain high concurrent throughput without readers and writers constantly queuing behind each other.

MVCC and write-ahead logging

MVCC is often paired with write-ahead logging, though the two solve different problems: WAL guarantees durability and crash recovery by persisting changes to a log before applying them, while MVCC governs how concurrent transactions see and create row versions. A database can use one without the other, but combining them is common — WAL provides the durable record of every change, and MVCC governs how those changes become visible to concurrently running transactions.

The takeaway

MVCC lets database readers and writers proceed concurrently by keeping multiple versions of each row instead of relying on locks, giving every transaction a consistent snapshot of the data as it existed when that transaction began. It trades storage overhead and a need for periodic cleanup of old versions for dramatically reduced lock contention — a tradeoff that’s paid off well enough that it’s the default concurrency model in most mainstream relational databases today.

The Lycoris Team The Lycoris Team · · 5 min read

What Is a Database Trigger?

A database trigger is a procedure that runs automatically on an insert, update, or delete — enforcing rules the application layer can't guarantee.

#Databases #SQL #Backend
The Lycoris Team The Lycoris Team · · 5 min read

What Is Little's Law? Capacity Planning Explained

Little's Law relates the number of requests in a system, their arrival rate, and how long each one takes — a simple formula for sizing capacity.

#Computer Science #Performance #Backend