PostgreSQL MVCC 原理
解析 PostgreSQL 獨特的 MVCC 實作——tuple versioning、xmin/xmax、visibility check、snapshot,以及與 MySQL MVCC 的關鍵差異
PostgreSQL 的 MVCC:同表多版本

MySQL(InnoDB)的 MVCC 把舊版本存在 Undo Log,表裡只保留最新版本。PostgreSQL 完全不同——新舊版本都存在同一張表(heap)裡,靠每個 tuple 上的 xmin/xmax 判斷可見性。
上圖展示了 UPDATE 前後 heap page 的變化:UPDATE 前只有一個 live tuple(xmax=0);UPDATE 後舊 tuple 的 xmax 被設為當前交易 ID(標記為 dead),同時在同一個 page 中插入一個全新的 tuple。PostgreSQL 不像 MySQL 把舊值移到 Undo Log,而是新舊版本並存在 heap 中。好處是不需要 Undo Log、crash 不需要回滾;代價是表會膨脹,必須靠 VACUUM 清理 dead tuple(見 Vacuum 與維運)。
Tuple 的隱藏欄位
每一行(tuple)都帶有:
| 欄位 | 用途 |
|---|---|
xmin | 建立此 tuple 的交易 ID(txid) |
xmax | 刪除或更新此 tuple 的交易 ID(0 = 未刪除) |
ctid | 物理位置 (page, offset),UPDATE 後指向新 tuple |
t_infomask | 狀態 flag:committed、aborted、locked 等 |
可以直接查看:
SELECT xmin, xmax, ctid, * FROM orders WHERE id = 1;
-- xmin | xmax | ctid | id | amount
-- ------+------+-------+----+--------
-- 100 | 0 | (0,1) | 1 | 500
UPDATE = DELETE + INSERT
這是理解 PostgreSQL MVCC 最關鍵的一點。當你執行:
UPDATE orders SET amount = 600 WHERE id = 1;
PostgreSQL 做的是:
- 找到原本的 tuple(xmin=100, amount=500)
- 把它的
xmax設為當前交易 ID(例如 200),標記為「被刪除」 - 在 heap 中插入一個全新的 tuple(xmin=200, amount=600)
- 把舊 tuple 的
ctid指向新 tuple 的位置
如上圖右側所示,Page 0 中同時存在兩個版本:(0,1) 是舊版本(xmax=200,被標記為 dead),ctid 指向新版本 (0,2);(0,2) 是新版本(xmin=200,xmax=0,live tuple)。
DELETE 更簡單——只設 xmax,不插新 tuple。
與 MySQL 的差異
| 操作 | PostgreSQL | MySQL (InnoDB) |
|---|---|---|
| UPDATE | 舊 tuple 標記 xmax + 插入新 tuple | 原地修改 + 舊值存 Undo Log |
| DELETE | 設 xmax | 標記刪除 + 舊值存 Undo Log |
| 舊版本位置 | 同一張表(heap) | Undo Log(獨立空間) |
| 清理 | 需要 VACUUM | Purge Thread 自動回收 |
| 表膨脹 | 會 | 不會(Undo 獨立空間) |
Visibility Check(可見性判斷)
每個 Backend 讀取 tuple 時都要做 visibility check:
- 檢查
xmin:建立此 tuple 的交易是否已 committed?- 已 committed 且在我的快照之前 → 這個 tuple 存在
- 已 aborted → 這個 tuple 不存在(垃圾)
- 仍在執行中 → 看隔離等級決定
- 檢查
xmax:刪除此 tuple 的交易是否已 committed?xmax = 0→ 沒被刪除 → 可見- 已 committed 且在我的快照之前 → 已被刪除 → 不可見
- 已 aborted → 刪除無效 → 可見
- 仍在執行中 → 看隔離等級決定
Hint Bits
交易狀態存在 pg_xact(舊稱 pg_clog)中,是一個 bitmap:每個 txid 佔 2 bits(in-progress / committed / aborted / sub-committed)。每次 visibility check 都去查 pg_xact 很慢,所以 PostgreSQL 第一次查完後會把結果寫回 tuple 的 t_infomask(稱為 hint bits),下次就不用再查 pg_xact。
這代表普通的 SELECT 也可能產生磁碟寫入(設定 hint bits)——這是 PostgreSQL 特有的行為,MySQL 不會。
Snapshot
PostgreSQL 的快照(snapshot)記錄的是:
xmin:快照建立時最小的活躍交易 IDxmax:快照建立時下一個將分配的交易 IDxip_list:快照建立時所有活躍交易的 ID 列表
visibility check 用這三個資訊判斷任意 txid 是否「在快照之前已 committed」。原理和 MySQL 的 Read View 一樣,只是欄位名不同。
| PostgreSQL Snapshot | MySQL Read View |
|---|---|
xmin | min_trx_id |
xmax | max_trx_id |
xip_list | m_ids |
如果你已經理解了 MySQL MVCC 中的 Read View 可見性規則,PostgreSQL 的 snapshot 判斷邏輯完全一樣——只是版本不是存在 Undo Log 裡,而是存在同一張表的不同 tuple 中。
Dead Tuple 與表膨脹
UPDATE 和 DELETE 留下的舊 tuple 稱為 dead tuple。它們佔用磁碟空間但不會被任何交易看到。如果不清理:
- 表的物理檔案持續膨脹(一張 1GB 的表可能膨脹到 10GB)
- Sequential scan 要掃過大量 dead tuple
- Index 也會指向 dead tuple,增加無效 IO
這就是為什麼 VACUUM 是 PostgreSQL 最關鍵的維運機制——MySQL 不需要是因為它的舊版本在 Undo Log 裡,Purge Thread 自動回收。
詳見 Vacuum 與維運。