Chapter 07 · InnoDB Storage Architecture and Transaction Internals
Purge, History List, Long Transactions, MVCC Cleanup, and Storage Health
See why old row versions cannot always disappear immediately, how long-running snapshots hold back purge, and how to diagnose history-list and undo pressure before it becomes a production health problem.
Learning outcomes
MVCC lets readers see consistent row versions while writers continue to commit. That flexibility has a cost: InnoDB cannot discard an old version if an active read view may still need it. A forgotten transaction can therefore turn ordinary writes into growing undo history even when the transaction itself is “only reading.”
Explain the relationship between undo records, read views, history list, and purge.
Create a safe two-session example where a long REPEATABLE READ snapshot delays cleanup of older versions.
Use INNODB_TRX, SHOW ENGINE INNODB STATUS, and supported metrics to identify long transactions and history pressure.
Distinguish purge lag from table-file shrinkage and from binary-log purging.
Design application/session guardrails that prevent idle open transactions from becoming storage-health incidents.
A writer can commit while older versions remain in undo history. Purge removes obsolete history only after no active transaction/read view needs it. “Committed” therefore does not mean “all old physical versions were immediately erased.”
Create a small MVCC target
USE servicehub_innodb_lab;DROP TABLE IF EXISTS mvcc_health_demo;CREATE TABLE mvcc_health_demo ( id INT NOT NULL, status VARCHAR(20) NOT NULL, version_no INT NOT NULL, detail VARCHAR(200) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;INSERT INTO mvcc_health_demo VALUES (1,'open',0,'initial state');Open three mysql sessions: A holds a snapshot, B generates committed versions, and C observes. Keeping observation separate prevents your diagnostic query from accidentally changing the teaching transaction.
Session A: hold a consistent snapshot open
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;START TRANSACTION WITH CONSISTENT SNAPSHOT;SELECT CONNECTION_ID() AS session_a, id,status,version_no,detailFROM mvcc_health_demo WHERE id=1;-- Keep this transaction open; do not COMMIT yet.Session A’s read view must continue to produce a result consistent with its snapshot. It does not need to block Session B’s ordinary update to achieve that. Instead, InnoDB retains sufficient undo history so A can reconstruct the older version.
Session B: create committed history
USE servicehub_innodb_lab;UPDATE mvcc_health_demo SET version_no=version_no+1, detail='writer version 1' WHERE id=1;COMMIT;UPDATE mvcc_health_demo SET version_no=version_no+1, detail='writer version 2' WHERE id=1;COMMIT;UPDATE mvcc_health_demo SET version_no=version_no+1, detail='writer version 3' WHERE id=1;COMMIT;SELECT * FROM mvcc_health_demo WHERE id=1;Session B sees the newest committed version. Return to Session A and issue the same SELECT: under its existing REPEATABLE READ snapshot it should continue to see the older snapshot-compatible version. That difference is MVCC, not replication lag or a stale client cache.
SELECT id,status,version_no,detailFROM mvcc_health_demo WHERE id=1;Session C: find the transaction holding history alive
SELECT TRX_MYSQL_THREAD_ID, TRX_ID, TRX_STATE, TRX_STARTED, TIMESTAMPDIFF(SECOND,TRX_STARTED,NOW()) AS age_seconds, TRX_ISOLATION_LEVEL, TRX_ROWS_MODIFIED, TRX_QUERYFROM information_schema.INNODB_TRXORDER BY TRX_STARTED;SHOW ENGINE INNODB STATUS\GA read-only, nonlocking transaction may not always receive a conventional transaction ID immediately, but the diagnostic views can still reveal relevant active transaction/read-view state depending on execution. In SHOW ENGINE INNODB STATUS, locate the TRANSACTIONS section and its History list length. Do not memorize one “safe” number: workload write rate and duration matter. The important pattern is sustained growth that correlates with old transactions or snapshots.
What purge does—and what it does not do
Purge processes undo history for committed changes after those versions are no longer required for rollback or MVCC visibility. It can physically remove delete-marked records and free undo history for reuse. Purge is not the same as:
| Not the same as | Why |
|---|---|
| PURGE BINARY LOGS | That command manages replication/PITR binary-log files, not InnoDB MVCC history. |
| OPTIMIZE TABLE | A table rebuild/reorganization operation; it is not the background MVCC purge mechanism. |
| File shrink after DELETE | Removing obsolete records does not automatically mean every tablespace file shrinks at the filesystem level. |
| COMMIT | Commit makes a transaction durable/visible according to isolation; purge can occur later when old versions are no longer needed. |
The history list can grow under a write-heavy workload even without a single pathological transaction; the production signal is growth rate, persistence, transaction age, undo/tablespace pressure, and application behavior together.
Release the snapshot and watch cleanup become possible
COMMIT;SELECT @@autocommit AS autocommit_after_commit;Once Session A ends, its old read view no longer needs those earlier versions. Purge is asynchronous, so history-list length may not drop to a particular value immediately. Re-check after a short interval while no other old transaction is holding history:
SELECT NOW(3) AS captured_at;SELECT TRX_MYSQL_THREAD_ID,TRX_STARTED,TRX_STATE,TRX_ROWS_MODIFIEDFROM information_schema.INNODB_TRXORDER BY TRX_STARTED;SHOW ENGINE INNODB STATUS\GFailure drill: the “idle” transaction that is not harmless
An application checks out a pooled connection, runs START TRANSACTION, performs one SELECT, then waits for user input or an external API call for ten minutes. The database session may appear idle at the SQL layer, but its transaction/read view can remain open and preserve old versions while unrelated writers keep updating rows.
Keep database transactions short and bound to database work. Do not wait for users, network APIs, queues, or long CPU jobs while a transaction is open. Configure pool/transaction timeouts appropriate to the application, instrument transaction age, and explicitly commit/rollback on every error path.
Killing a suspicious production session is not the first diagnostic step. Identify the owning application, capture transaction age/query/account/host evidence, determine whether it is safe to abort, and understand rollback cost for write-heavy transactions.
Observe purge and undo configuration without cargo-cult tuning
SHOW VARIABLES WHERE Variable_name IN ('innodb_purge_threads','innodb_purge_batch_size', 'innodb_max_purge_lag','innodb_max_purge_lag_delay', 'innodb_undo_log_truncate','innodb_max_undo_log_size');SELECT NAME, COUNT, STATUS, COMMENTFROM information_schema.INNODB_METRICSWHERE NAME LIKE '%purge%' OR NAME LIKE '%undo%'ORDER BY NAME;Do not immediately raise purge threads or set purge-lag delays because the history list is high once. If an hour-old transaction is holding a read view, adding background capacity does not remove the correctness requirement to preserve versions that transaction can still see. Fix the transaction lifecycle first.
Storage-health runbook
| Question | Evidence | Action direction |
|---|---|---|
| Are there unusually old transactions? | INNODB_TRX start time, account/process context. | Trace to application owner; end/repair transaction lifecycle safely. |
| Is history growing persistently? | Repeated SHOW ENGINE INNODB STATUS captures / monitoring. | Correlate with write rate and old snapshots. |
| Is undo space growing? | Undo tablespace/filesystem metrics, InnoDB configuration. | Confirm purge can advance; assess truncation behavior and disk headroom. |
| Are transactions waiting/locking too? | Performance Schema data_locks/data_lock_waits, INNODB_TRX. | Separate lock contention from MVCC history retention. |
| Did file size fail to shrink after cleanup? | Tablespace type and filesystem size. | Understand reclaim semantics; do not assume purge truncates table files. |
Knowledge check
- Why can a read-only transaction cause undo history to remain longer?
- Does COMMIT immediately guarantee History list length becomes zero?
- Is PURGE BINARY LOGS related to InnoDB MVCC purge?
- What is usually a better first fix than increasing purge threads when one very old transaction exists?
- Why should transaction age be monitored alongside history-list length?
Reveal answers
- Its consistent read view may still need older row versions, so purge cannot discard them yet.
- No. Purge is asynchronous and other transactions/workload may still require or generate history.
- No. It manages binary-log files, not undo history.
- Repair/end the long transaction safely and fix the application lifecycle that left it open.
- It helps connect retained history to the sessions/read views that can prevent purge from advancing.
Long readers and long writers create different kinds of pressure
A long read transaction can retain an old read view without changing rows itself. That primarily affects MVCC cleanup: newer writers can commit, but their superseded versions may need to remain reconstructable. A long write transaction can add a second class of cost: it can retain locks, accumulate its own undo, increase rollback work, and still contribute to history pressure after it commits. Transaction age is therefore useful, but you also need rows modified, lock state, and application purpose.
| Pattern | Main risk | Diagnostic clue |
|---|---|---|
| Old read-only snapshot | Holds old versions needed for consistent reads; purge cannot fully advance. | Old transaction/read view, low rows modified, growing history under concurrent writes. |
| Old write transaction | Locks, undo growth, rollback cost, and delayed cleanup. | Old INNODB_TRX entry with modified rows/locks. |
| High write rate, no obvious old session | Purge may temporarily lag behind normal workload. | History growth correlates with DML rate; transaction ages remain bounded. |
| Idle pooled connection in transaction | Invisible application lifecycle bug preserving a snapshot. | Old transaction whose current SQL is NULL/idle at observation time. |
Purge cleanup and tablespace truncation are different
Purge makes obsolete row versions and delete-marked records no longer needed by MVCC. Undo tablespaces have their own truncation/reuse behavior controlled by separate mechanisms such as innodb_undo_log_truncate. A user table’s file-per-table .ibd file does not necessarily shrink just because purge removed obsolete records; free space may remain reusable inside the tablespace. Treat “history is cleaned” and “filesystem bytes decreased” as separate acceptance criteria.
Monitoring should detect trends, not worship one threshold
There is no portable magic History list length that means “incident.” A write-heavy server may naturally have a higher steady-state value than a lightly used application. Useful alerts combine sustained upward trend, transaction age, undo/disk growth, DML throughput, purge activity, and user-visible latency. The goal is to catch a cleanup system that is falling behind or being blocked, not to force every server to the same number.
Use explicit transaction scopes, commit/rollback in finally-style cleanup paths, pool leak detection, maximum transaction-age telemetry, and request tracing that records when a database transaction starts and ends. Preventing accidental long transactions is usually cheaper than tuning purge after the fact.
Chapter 07 summary and bridge to index engineering
You now have one connected InnoDB mental model. Tables and indexes live in tablespaces organized into pages and allocation structures. One clustered index stores each table’s rows, while secondary indexes carry the clustered key. The buffer pool caches those pages and accumulates dirty state. Redo protects durable changes across crashes; undo supports rollback and older read views; doublewrite protects page integrity; checkpoints bound redo reuse/recovery work; purge removes history only when MVCC no longer needs it.
Chapter 08 turns that mechanism-based foundation into Index Engineering and Access Path Design: selectivity, composite-key ordering, covering indexes, functional/multi-valued/full-text/spatial indexes, and how to design an index portfolio from workload evidence rather than intuition.