はじめに
この記事では、PostgreSQLのMVCC(Multi-Version Concurrency Control)の内部実装を深掘りします。
前回の記事(InnoDB UNDO Log の内部構造を徹底解説)ではMySQLのMVCCを解説しました。PostgreSQLのMVCCはInnoDBと根本的に異なるアプローチを取ります。PostgreSQL 17のソースコードを参照しながら、以下を解説します:
- Tuple Versioning(Heap内にバージョンを保持する方式)
- xmin / xmax によるバージョン管理
- Snapshot(InnoDBのReadViewに相当)
- Visibility Map
- VACUUM(InnoDBのPurgeに相当)
InnoDBとの根本的な違い
InnoDBは「最新版をテーブルに、旧版をUNDO Logに」保持します。PostgreSQLは全バージョンをテーブル(Heap)内に保持します。
InnoDB:
Clustered Index: [id=1, salary=5000] ← 最新版のみ
UNDO Log: salary=4000 → salary=3000 ← 旧版はUNDO領域
PostgreSQL:
Heap Page: [id=1, salary=3000, xmin=100, xmax=150] ← 旧版(dead tuple)
[id=1, salary=4000, xmin=150, xmax=200] ← 旧版(dead tuple)
[id=1, salary=5000, xmin=200, xmax=0] ← 最新版(live tuple)
| InnoDB | PostgreSQL | |
|---|---|---|
| 旧バージョンの保管場所 | UNDO Log(別領域) | Heap内(テーブル本体) |
| UPDATEの動作 | レコードをin-place更新 | 新しいタプルを挿入 |
| テーブル肥大化 | しない | する(VACUUMで回収) |
| 旧版の削除 | Purge | VACUUM |
xmin / xmax:バージョンの生死を管理
PostgreSQLの各タプル(行)にはヘッダにxminとxmaxが記録されます:
typedef struct HeapTupleHeaderData {
TransactionId t_xmin; // このタプルを挿入したトランザクションID
TransactionId t_xmax; // このタプルを削除/更新したトランザクションID(0=未削除)
// ... 他のフィールド
} HeapTupleHeaderData;INSERT
INSERT INTO t VALUES (1, 'Alice');
→ xmin=100(挿入したTxID), xmax=0(まだ削除されていない)
DELETE
DELETE FROM t WHERE id = 1;
→ 既存タプルの xmax を 200(削除したTxID)に設定
→ タプル自体は消えない(dead tupleになる)
UPDATE = DELETE + INSERT
UPDATE t SET name = 'Bob' WHERE id = 1;
旧タプル: [id=1, name=Alice, xmin=100, xmax=200] ← xmaxが設定される
新タプル: [id=1, name=Bob, xmin=200, xmax=0] ← 新しいタプルが挿入される
InnoDBのUPDATEがin-place更新なのに対し、PostgreSQLは常に新しいタプルを作るのが最大の違いです。
Snapshot:可視性判定
InnoDBのReadViewに相当するのがSnapshotです。
Snapshotの構造
typedef struct SnapshotData {
TransactionId xmin; // この値未満のTxIDは確定済み → 見える
TransactionId xmax; // この値以上のTxIDは未来 → 見えない
TransactionId *xip; // アクティブなTxIDの配列 → 見えない
uint32 xcnt; // xip配列の要素数
} SnapshotData;InnoDBのReadViewとほぼ同じ構造です。
可視性判定のロジック
タプルが見えるか?
1. xmin がコミット済みか?
→ NO: 見えない(挿入トランザクションが未コミット)
→ YES: 次へ
2. xmax が設定されているか?
→ xmax = 0: 見える(削除されていない)
→ xmax > 0: xmax がコミット済みか?
→ YES: 見えない(削除済み)
→ NO: 見える(削除トランザクションが未コミット)
InnoDBがバージョンチェーンを辿るのに対し、PostgreSQLは各タプルのxmin/xmaxを直接チェックします。バージョンチェーンを辿る必要がない分シンプルですが、全バージョンがHeapに散在するため、テーブルスキャンのコストが増えます。
分離レベルとSnapshot
| 分離レベル | Snapshot取得タイミング | InnoDBとの対応 |
|---|---|---|
| READ COMMITTED | 各SQL文の開始時 | 同じ |
| REPEATABLE READ | トランザクション開始時 | 同じ |
| SERIALIZABLE | トランザクション開始時 + SSI | InnoDBはロックベース |
PostgreSQLのSERIALIZABLEはSSI(Serializable Snapshot Isolation)という方式で、ロックではなく依存関係の検出で直列化可能性を保証します。InnoDBのSERIALIZABLEがロックベース(全SELECTがSELECT … FOR SHARE相当)なのとは対照的です。
Visibility Map
全タプルのxmin/xmaxを毎回チェックするのはコストが高いです。Visibility Map(VM)はこれを最適化します。
Heap:
Page 0: [live tuple, live tuple] ← 全タプルが全トランザクションから見える
Page 1: [live tuple, dead tuple] ← dead tupleあり
Page 2: [live tuple, live tuple] ← 全タプルが見える
Visibility Map:
Page 0: 1 ← all-visible(可視性チェック不要)
Page 1: 0 ← チェック必要
Page 2: 1 ← all-visible
- all-visibleなページはIndex Only Scanで直接スキップできる(Heapアクセス不要)
- VACUUMがVisibility Mapを更新する
VACUUM:dead tupleの回収
InnoDBのPurgeに相当するのがVACUUMです。ただし、InnoDBのPurgeが自動的にバックグラウンドで動くのに対し、PostgreSQLのVACUUMはより目に見える存在です。
VACUUMの動作
1. 各ページをスキャン
2. どのSnapshotからも見えないdead tupleを特定
3. dead tupleの領域をFree Spaceとしてマーク(再利用可能に)
4. Visibility Mapを更新
通常のVACUUMはページ内の領域を再利用可能にするだけで、OSにディスク領域を返しません。テーブルのファイルサイズは縮まりません。
VACUUM FULL
テーブル全体を書き直してディスク領域を回収します。ただしテーブル全体をロックするため、本番環境では慎重に使う必要があります。
VACUUM FULL employees; -- テーブルロックあり、ディスク領域を回収
VACUUM employees; -- ロックなし、領域を再利用可能にするだけautovacuum
PostgreSQLはautovacuumデーモンが自動的にVACUUMを実行します:
-- 主要な設定
autovacuum = on -- デフォルトON
autovacuum_vacuum_threshold = 50 -- 最低50行変更で発動
autovacuum_vacuum_scale_factor = 0.2 -- テーブルの20%が変更で発動InnoDBのPurgeとの比較
| InnoDB Purge | PostgreSQL VACUUM | |
|---|---|---|
| 対象 | UNDO Log内の旧バージョン | Heap内のdead tuple |
| テーブル肥大化 | しない | する(VACUUMで緩和) |
| 自動実行 | 常時バックグラウンド | autovacuum(設定ベース) |
| ディスク回収 | 不要(UNDO領域のみ) | VACUUM FULLが必要 |
| 運用負荷 | 低い | 高い(チューニングが必要) |
まとめ
| トピック | InnoDB | PostgreSQL |
|---|---|---|
| バージョン保管場所 | UNDO Log(別領域) | Heap内(テーブル本体) |
| UPDATEの動作 | in-place更新 | 新タプル挿入 |
| バージョン管理 | Roll Ptr(チェーン) | xmin/xmax(各タプルに記録) |
| 可視性判定 | ReadView | Snapshot |
| SERIALIZABLE | ロックベース | SSI(依存関係検出) |
| 旧版の削除 | Purge(自動) | VACUUM(autovacuum) |
| テーブル肥大化 | しない | する |
| 最適化 | ― | Visibility Map、HOT |
次回はOracle UNDOを同じ切り口で解説します。