The idea: same snapshot, old versions in different places
PostgreSQL and InnoDB both use MVCC (multi-version concurrency control): a reader gets a snapshot and never waits for a writer. To do that, both must keep the old version of a row while someone may still need it. They keep it in different places. PostgreSQL writes every new version as a new tuple in the table and leaves the old one where it was. InnoDB changes the row in place and moves the old version into the undo log. The canvas runs the same statements on the same row in both engines, side by side.
PostgreSQL: a new tuple per UPDATE
Every heap tuple has a header with xmin (the transaction that created it) and xmax (the transaction that deleted or replaced it, 0 if none). An UPDATE never changes the old tuple's data. It writes a whole new tuple with xmin = its own xid, then stamps the old tuple's xmax and points its t_ctid at the new one. Whether a transaction committed is looked up in pg_xact, two bits per xid. (Demo: one UPDATE in both engines)
Here bal has no index, so the update is a HOT (heap-only tuple) update: the new tuple goes on the same page, the index entry keeps pointing at (0,1), and a lookup follows the t_ctid chain from there. See the PostgreSQL heap page for line pointers and HOT chains.
InnoDB: update in place, old version to undo
The clustered index holds one version of each row, the newest. Each record has two hidden columns: DB_TRX_ID (who wrote this version) and DB_ROLL_PTR (where its previous version is). An UPDATE first copies the old version into an undo record, then changes the record in place. Each undo record points to the one before it, so the row has a version chain in undo, newest first. See InnoDB undo logs and purge for the full mechanism.
Reading with a snapshot: who walks the chain
In this page no other transaction is running when the reader starts, so its snapshot is one number S: changes by transactions below S are visible, the rest are not. PostgreSQL stores it as xmin/xmax/xip; InnoDB as a read view (m_up_limit_id, m_low_limit_id, m_ids). Both engines return the same value. The work differs:
- An old snapshot. PostgreSQL finds the old tuple still in the table: it is the first one on the chain. InnoDB reads the newest record, finds
DB_TRX_IDtoo new, and rebuilds older versions one undo record at a time. (Demo: an old snapshot reads the old version) - A new snapshot. InnoDB is done after the record. PostgreSQL starts at
(0,1)and steps over each dead version until it reaches the newest. (Demo: old reader vs new reader: who walks the chain)
Clean-up: VACUUM vs purge
An old version can go once every snapshot sees the change that replaced it. Both engines compute this from the oldest open snapshot: PostgreSQL calls it OldestXmin, InnoDB the purge view. VACUUM (usually autovacuum) removes dead tuples and marks their space free for reuse; the table file does not shrink. When the root tuple (0,1) of a HOT chain goes, its line pointer stays as a redirect because the index still points at it. The InnoDB purge thread walks the history list oldest first and frees undo records. (Demo: VACUUM and purge clean up)
Long transactions: table bloat vs history list
A transaction that keeps its snapshot open holds the horizon back in both engines, and neither can clean up. The garbage piles up in different places. In PostgreSQL it is table bloat: dead tuples fill the pages, the table and its indexes grow, and every scan steps over them. In InnoDB it is a long history list: the table stays small, but the undo tablespace grows and old readers walk longer chains. (Demo: a long reader blocks VACUUM and purge)
ROLLBACK
In PostgreSQL a ROLLBACK only marks the xid aborted in pg_xact. The new tuple is dead at once and the old tuple's xmax is ignored; VACUUM removes the leftover later. InnoDB has to undo the change: it copies the old version back from the undo record into the row. Rolling back a large transaction in InnoDB can take as long as the transaction itself. (Demo: ROLLBACK: free in Postgres, work in InnoDB)
Side by side
| PostgreSQL | InnoDB | |
|---|---|---|
| UPDATE writes | a new tuple; old tuple gets xmax | old version to undo; record changed in place |
| row header | xmin, xmax, t_ctid | DB_TRX_ID, DB_ROLL_PTR |
| old versions live in | the table (heap) | undo tablespaces |
| reader with an old snapshot | finds the old tuple | rebuilds it from undo |
| reader with a new snapshot | steps over dead tuples | reads the record |
| clean-up | VACUUM / autovacuum | purge thread, history list |
| ROLLBACK | mark aborted in pg_xact | apply undo records |
| a long transaction causes | table and index bloat | long history list, long chains |
See also MVCC and isolation levels for what snapshots allow and forbid, and PostgreSQL xid wraparound for the other reason VACUUM must run.
What the page leaves out
Snapshots taken while other transactions run (a non-empty xip / m_ids), concurrent writers and row locks. PostgreSQL non-HOT updates, which add an index entry per version, opportunistic page pruning on reads, hint bits, the free space map and visibility map, and VACUUM FULL. InnoDB secondary indexes, purge by trx_no rather than trx id, batch purge over rollback segments and undo truncation. Real undo records store only the changed columns.