← Back to list

A Deep Dive into MVCC in PostgreSQL: How Concurrency Really Works

Concurrency is one of the hardest problems in database systems — not because it’s complex in theory, but because it becomes unpredictable…

Navidbarsalari · 2026-04-17 17:02 · 0 claps · 3.6 min read
#postgresql #mvcc #postgres-architecture #architecture #database
Open on Medium ↗
Wiki topics: 🏛️ · Architecture

A Deep Dive into MVCC in PostgreSQL: How Concurrency Really Works

A Deep Dive into MVCC in PostgreSQL

A Deep Dive into MVCC in PostgreSQL

Concurrency is one of the hardest problems in database systems — not because it’s complex in theory, but because it becomes unpredictable under real-world load.

As systems scale, multiple users attempt to read and write the same data simultaneously. Without a proper concurrency model, this leads to inconsistency, blocking, and performance collapse.

PostgreSQL addresses this challenge using Multi-Version Concurrency Control (MVCC) — a design that prioritizes consistency and performance without relying on heavy locking.

This article explores MVCC from the ground up: why it exists, how it works internally, and what trade-offs it introduces in production systems.

The Problem with Traditional Locking

Before MVCC, most relational databases relied on locking-based concurrency control, particularly Two-Phase Locking (2PL).

In this model:

  • A transaction reading data acquires a shared lock
  • A transaction modifying data requires an exclusive lock

This creates a fundamental limitation:

READ → blocks WRITE
WRITE → blocks READ and WRITE

While this guarantees consistency, it introduces:

  • Lock contention under high concurrency
  • Increased latency due to waiting
  • Reduced throughput

In high-traffic systems, this becomes a bottleneck.

The MVCC Principle

PostgreSQL takes a different approach:

Reads don’t block writes, and writes don’t block reads.

Instead of locking rows, PostgreSQL maintains multiple versions of each row.

This allows:

  • Readers to access a consistent snapshot
  • Writers to create new versions without blocking others

The Core Idea: Versioned Rows

Rather than modifying data in place, PostgreSQL treats every change as a new version.

Conceptually:

Time ─────────────▶
Version 1 → visible to older transactions
Version 2 → visible after commit

Each transaction sees a snapshot of the database at a specific point in time.

This is the foundation of MVCC.

Tuple Structure and System Columns

Every row (tuple) in PostgreSQL contains hidden metadata:

| user_data | xmin | xmax |
  • xmin: Transaction ID that created the tuple
  • xmax: Transaction ID that invalidated the tuple (0 if still valid)

These fields are not exposed in normal queries, but they drive all visibility decisions.

How Operations Work Internally

INSERT

A new tuple is created:

xmin = current TXID
xmax = 0

The row becomes visible after the transaction commits.

DELETE

The row is not physically removed:

xmax = current TXID

This marks the row as no longer valid for future transactions.

UPDATE (Important)

PostgreSQL does not support in-place updates.

Instead:

OLD ROW → xmax = current TXID
NEW ROW → xmin = current TXID

This means:

Every UPDATE creates a new version of the row.

This design is essential for enabling non-blocking reads.

Snapshots: The Heart of MVCC

Every transaction operates on a snapshot.

A snapshot captures the state of transactions at a specific moment:

xmin → oldest active transaction
xmax → next available transaction ID
xip_list → active transactions

This snapshot allows PostgreSQL to determine which rows are visible.

Visibility Rules (How PostgreSQL Thinks)

When executing a query, PostgreSQL evaluates each tuple using the snapshot.

Step 1: Check Creation (xmin)

If xmin ≥ snapshot_xmax → invisible (future data)
If xmin ∈ xip_list → invisible (not committed)
Else → potentially visible

Step 2: Check Deletion (xmax)

If xmax = 0 → visible
If xmax ≥ snapshot_xmax → visible (deleted in future)
If xmax ∈ xip_list → visible (deletion not committed)
Else → invisible (dead tuple)

Dead Tuples: The Cost of Versioning

Because PostgreSQL preserves old versions, outdated rows accumulate as:

Dead Tuples

These are:

  • Not visible to any transaction
  • Still stored on disk
  • Still scanned during queries

Example

Live rows: 1M
Dead tuples: 5M
Total scan: 6M rows

This leads to:

  • Increased I/O
  • Higher CPU usage
  • Slower queries

This phenomenon is known as table bloat.

VACUUM: Managing Data Lifecycle

To handle dead tuples, PostgreSQL uses VACUUM.

VACUUM acts as a garbage collector for the database.

Responsibilities

Space Reclamation

Dead tuples → marked reusable (Free Space Map)

Note: Disk size does not shrink.

Transaction ID Wraparound Prevention

Transaction IDs are 32-bit:

Max ≈ 2 billion transactions

To prevent wraparound issues, VACUUM freezes old tuples:

xmin → FrozenXID

Visibility Map Updates

VACUUM tracks pages that contain only visible rows, enabling:

  • Faster index-only scans
  • Better query planning

AutoVacuum: Continuous Maintenance

PostgreSQL runs VACUUM automatically using AutoVacuum.

Trigger ≈ 20% of table modified

However, defaults are not always optimal for large tables.

Example

Table size: 100M rows
Threshold: 20M changes

This delay can cause severe bloat.

Trade-offs of MVCC

MVCC improves concurrency, but introduces complexity:

Storage Overhead

Each tuple includes metadata (~23 bytes).

Write Amplification

Updates generate new rows and may update indexes.

Maintenance Requirement

Dead tuples require continuous cleanup via VACUUM.

Practical Considerations

Long-Running Transactions

A long-lived transaction holds an old snapshot:

VACUUM cannot remove old versions

Result:

  • Table bloat
  • Performance degradation

HOT Updates

If an update does not affect indexed columns:

Update stays within same page
Indexes untouched

This significantly improves performance.

Tuning AutoVacuum

For large tables:

Reduce autovacuum_vacuum_scale_factor
Increase frequency

This prevents large cleanup spikes.

Conclusion

MVCC is one of PostgreSQL’s most powerful features.

It enables:

  • High concurrency without blocking
  • Consistent reads across transactions
  • Scalable performance under load

However, it shifts complexity into:

  • Storage management
  • Visibility rules
  • Background maintenance

Understanding MVCC is essential for anyone working with PostgreSQL in production.

PostgreSQL does not hide complexity — it exposes it. And mastering MVCC is the key to mastering PostgreSQL.


메타데이터
post_id
b343a005ccc6
slug
a-deep-dive-into-mvcc-in-postgresql-how-concurrency-really-works-b343a005ccc6
url
https://medium.com/@navidbarsalari/a-deep-dive-into-mvcc-in-postgresql-how-concurrency-really-works-b343a005ccc6
canonical_url
https://medium.com/@navidbarsalari/a-deep-dive-into-mvcc-in-postgresql-how-concurrency-really-works-b343a005ccc6
author_url
https://medium.com/@navidbarsalari
status
ok
fetched_at
2026-06-20 20:29:01