PostgreSQL MVCC Explained: How It Works and Isolation Levels

Multi-version concurrency control (MVCC) in Postgres SQL

How can a database handle multiple requests simultaneously while maintaining data consistency and minimizing delays?

The answer lies in Multi-Version Concurrency Control (MVCC), a key PostgreSQL feature that helps manage concurrent transactions without locking every record for every read. Understanding how MVCC works is essential for recognizing how PostgreSQL balances performance, data consistency, and concurrent access.

What Is MVCC in PostgreSQL?

Multi-Version Concurrency Control (MVCC) is a database concurrency control technique that allows multiple transactions to access data simultaneously while maintaining consistency. Instead of immediately overwriting data or blocking readers during writes, MVCC maintains multiple data versions, allowing transactions to read an appropriate version based on the database snapshot and isolation-level rules.

In traditional lock-based systems, read and write operations may block each other, reducing performance under heavy workloads. MVCC reduces this read-write blocking, improving throughput and responsiveness in multiuser systems. However, it does not eliminate all locks, waiting, or transaction conflicts. The level of consistency depends on the configured isolation level.

PostgreSQL also supports Serializable Snapshot Isolation (SSI), which detects potentially dangerous read-write dependencies between concurrent transactions. If these dependencies could lead to a result inconsistent with any serial execution, PostgreSQL may abort a transaction to preserve serializability.

What Are the Four Transaction Isolation Levels?

The four standard transaction isolation levels are:

  • Read Uncommitted
  • Read Committed
  • Repeatable Read
  • Serializable

Serializable guarantees results equivalent to executing transactions one at a time in some order. The other isolation levels define which issues, or phenomena, may occur when transactions interact concurrently.

What Issues Can Occur When Transactions Run Concurrently?

The following issues can occur when multiple transactions interact concurrently:

  • Dirty read: Occurs when a transaction reads uncommitted changes made by another transaction.
  • Non-repeatable read: Occurs when a transaction reads the same row twice but gets different values because another transaction updates and commits the data between the two reads.
  • Phantom read: Occurs when a transaction executes the same query twice but gets a different set of rows because another transaction inserts or deletes rows that match the query condition.
  • Serialization anomaly: Occurs when concurrent transactions produce a result that would not be possible if they were executed one at a time in some order.

Which Anomalies Are Allowed at Each Isolation Level?

Isolation Levels and Their Anomalies
Isolation Level Dirty Read Non-repeatable
Read
Phantom Read Serialization
Anomaly
Read Uncommitted Allowed (not in PostgreSQL) Possible Possible Possible
Read Committed Not possible Possible Possible Possible
Repeatable Read Not possible Not possible Allowed under the SQL standard, but prevented in
PostgreSQL
Possible
Serializable Not possible Not possible Not possible Not possible

The Serializable isolation level provides the strictest isolation. It ensures that successfully committed transactions produce results equivalent to executing them one at a time in some order. PostgreSQL may abort a transaction if needed to maintain this guarantee.

In PostgreSQL, Read Uncommitted behaves like Read Committed because its MVCC architecture prevents transactions from reading uncommitted data.

PostgreSQL's Repeatable Read level also prevents phantom reads, providing stronger protection than the SQL standard requires. The standard defines the minimum anomalies each isolation level must prevent, so stronger protection is allowed.

Use the SET TRANSACTION command to set a transaction's isolation level. Note that sequence changes, including counters used by SERIAL columns, are immediately visible to other transactions and are not rolled back if a transaction fails.

How Does the Read Committed Isolation Level Work in PostgreSQL?

In the Read Committed isolation level, each SQL statement sees only data committed before that statement begins. If another transaction commits changes while your transaction is running, the next query can see those changes. As a result, different queries within the same transaction may return different results because each query gets a fresh snapshot of committed data.

Create a table with data

-- Table
CREATE TABLE account (
    id INT PRIMARY KEY,
    balance INT
);

INSERT INTO account VALUES (1, 1000);

Transaction A (Session 1)

BEGIN;
SELECT balance FROM account WHERE id = 1;
-- Output: 1000

Transaction B (Session 2)

BEGIN;

UPDATE account
SET balance = 2000
WHERE id = 1;

COMMIT;

Back to Transaction A

SELECT balance FROM account WHERE id = 1;
-- Output: 2000

How Does the Repeatable Read Isolation Level Work in PostgreSQL?

In Repeatable Read, all queries within a transaction see the same snapshot of data taken at the start of the transaction. Changes committed by other transactions after that snapshot are not visible to your transaction, ensuring consistent results across its queries.

Example -

Create a table with data

-- Table
CREATE TABLE account (
    id INT PRIMARY KEY,
    balance INT
);

INSERT INTO account VALUES (1, 1000);

Transaction A (Session 1)

BEGIN;

SELECT balance FROM account WHERE id = 1;
-- Output: 1000

Transaction B (Session 2)

BEGIN;

UPDATE account
SET balance = 2000
WHERE id = 1;

COMMIT;

Back to Transaction A

SELECT balance FROM account WHERE id = 1;
-- Output: 1000 (same as before)

How Does the Serializable Isolation Level Work in PostgreSQL?

Serializable is the highest isolation level in PostgreSQL. It ensures that concurrent transactions produce results equivalent to executing them one at a time in some order. PostgreSQL uses Serializable Snapshot Isolation (SSI) to detect potentially unsafe transaction dependencies. If a conflict could lead to a result that is not equivalent to serial execution, PostgreSQL may abort a transaction with a serialization error.

Create a table with data

-- Table
CREATE TABLE account (
    id INT PRIMARY KEY,
    balance INT
);

INSERT INTO account VALUES (1, 1000);

Transaction A (Session 1)

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SELECT balance FROM account WHERE id = 1;
-- Output: 1000

Transaction B (Session 2)

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

UPDATE account
SET balance = balance + 500
WHERE id = 1;

COMMIT;

Back to Transaction A

UPDATE account
SET balance = balance + 300
WHERE id = 1;

-- Might fail with serialization error
-- because PostgreSQL detects conflict

What Are the Limitations of MVCC?

  • Increased storage usage: Multiple row versions require additional disk space.
  • Table bloat: Old row versions, called dead tuples, can accumulate if they are not cleaned up.
  • Need for VACUUM: Regular vacuuming helps reclaim space and maintain performance.
  • Write overhead: UPDATE and DELETE operations create new row versions, adding processing overhead.
  • Index maintenance: Some updates require index maintenance, increasing overhead.
  • Long-running transactions: Old transactions can delay the cleanup of obsolete row versions, contributing to table bloat.
  • Memory usage: Transaction snapshots and visibility tracking require system resources.
  • Complex visibility rules: PostgreSQL must determine which row version is visible to each transaction, adding processing overhead.

Conclusion

MVCC helps PostgreSQL handle concurrent transactions efficiently by allowing readers and writers to work with different versions of data. It supports consistent reads across isolation levels while reducing read-write blocking. However, it also introduces trade-offs, including additional storage requirements, table bloat, and the need for regular vacuuming. Understanding MVCC and transaction isolation levels helps developers build scalable applications while maintaining data consistency.