返回文章列表
postgresql

PostgreSQL MVCC 原理

解析 PostgreSQL 獨特的 MVCC 實作——tuple versioning、xmin/xmax、visibility check、snapshot,以及與 MySQL MVCC 的關鍵差異

Aaron

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 做的是:

  1. 找到原本的 tuple(xmin=100, amount=500)
  2. 把它的 xmax 設為當前交易 ID(例如 200),標記為「被刪除」
  3. 在 heap 中插入一個全新的 tuple(xmin=200, amount=600)
  4. 把舊 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 的差異

操作PostgreSQLMySQL (InnoDB)
UPDATE舊 tuple 標記 xmax + 插入新 tuple原地修改 + 舊值存 Undo Log
DELETE設 xmax標記刪除 + 舊值存 Undo Log
舊版本位置同一張表(heap)Undo Log(獨立空間)
清理需要 VACUUMPurge Thread 自動回收
表膨脹不會(Undo 獨立空間)

Visibility Check(可見性判斷)

每個 Backend 讀取 tuple 時都要做 visibility check:

  1. 檢查 xmin:建立此 tuple 的交易是否已 committed?
    • 已 committed 且在我的快照之前 → 這個 tuple 存在
    • 已 aborted → 這個 tuple 不存在(垃圾)
    • 仍在執行中 → 看隔離等級決定
  2. 檢查 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:快照建立時最小的活躍交易 ID
  • xmax:快照建立時下一個將分配的交易 ID
  • xip_list:快照建立時所有活躍交易的 ID 列表

visibility check 用這三個資訊判斷任意 txid 是否「在快照之前已 committed」。原理和 MySQL 的 Read View 一樣,只是欄位名不同。

PostgreSQL SnapshotMySQL Read View
xminmin_trx_id
xmaxmax_trx_id
xip_listm_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 與維運