MVCC Explained

Multi-Version Concurrency Control (MVCC) lets InnoDB provide consistent reads without blocking writers, and writers without blocking readers, by keeping multiple row versions and determining which version a transaction is allowed to see. This article explains how MVCC works in InnoDB and how it relates to isolation levels and undo logs.

Overview

  • Idea: Instead of locking a row for the whole transaction, InnoDB keeps older versions of the row in the undo log. A read sees a snapshot of data (a consistent view) based on transaction start and visibility rules; writers create new versions without making readers wait.
  • Read view: Each transaction (for repeatable read or read committed) gets a read view that determines which row versions are visible (by comparing transaction IDs and the view’s low-water marks).
  • Undo: When you UPDATE or DELETE, the old row version is stored in the undo log and linked from the current row. Long-running transactions or large updates can grow undo and history.

Example

Example 1: Two transactions, one updates while the other reads

SQL
-- Transaction A (start time T1)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- sees 100

-- Transaction B (start time T2)
UPDATE accounts SET balance = 50 WHERE id = 1;
COMMIT;

-- Transaction A reads again (still in same transaction)
SELECT balance FROM accounts WHERE id = 1;  -- still sees 100 (snapshot)
COMMIT;
  • Under REPEATABLE READ, A’s read view is fixed at the first read; it keeps seeing the old version (100). B’s update creates a new row version; A never sees it until A commits and starts a new transaction.

Example 2: Read committed — each statement sees latest committed

SQL
-- Session 1
SET SESSION transaction_isolation = 'READ-COMMITTED';
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- 100

-- Session 2
UPDATE accounts SET balance = 50 WHERE id = 1;
COMMIT;

-- Session 1
SELECT balance FROM accounts WHERE id = 1;  -- 50 (new read view per statement)
  • With READ COMMITTED, each statement gets a fresh read view, so the second SELECT sees the committed 50.

Example 3: How visibility is decided (simplified)

Your trx_idRow version creator trx_idVisibility (RR)
10099 (committed)Visible
100101 (not committed)Not visible
100100 (your changes)Visible
  • InnoDB compares row version’s transaction ID with the read view’s up_limit_id, low_limit_id, and active list to decide visibility.

Core Mechanism / Behavior

  • Row structure: Each row has hidden columns: DB_TRX_ID (last modifier), DB_ROLL_PTR (pointer to undo for previous version), DB_ROW_ID (if no primary key).
  • Undo chain: Updates and deletes append undo records; the current row’s DB_ROLL_PTR walks back to older versions. Purge removes undo that is no longer needed by any read view.
  • Read view (RR): Created at first read in the transaction; reused until commit. So repeatable read is implemented by a fixed snapshot.
  • Read view (RC): New read view per statement; you see the latest committed state at each statement.

Key Rules

  • Long-running transactions keep read views and undo alive; avoid holding transactions open for a long time.
  • Heavy updates/deletes generate a lot of undo; monitor undo tablespace and history list length. Purge can lag under load.
  • Use the right isolation level: RR for consistent snapshots, RC if you want to see committed changes immediately per statement.

What's Next

For isolation levels and anomalies, see Transactions - Isolation & Common Anomalies. For locking (row vs gap), see Locks - Row Lock vs Gap Lock.