MVCC ── 読み書きが互いをブロックしない仕組み
このレイヤーの役割:トランザクション間のデータの可視性を制御する。「読み取りが書き込みをブロックしない」を実現する RDB の核心技術。

この章で何ができるようになるか:「なぜ SELECT は UPDATE をブロックしないのか」を内部動作から説明でき、分離レベルごとの可視性ルールを理解できるようになる。
なぜ MVCC が必要か
MVCC なし(単純ロック):
TX1: SELECT * FROM users WHERE id = 1; ← 読み取りロック取得
TX2: UPDATE users SET name = 'Bob' WHERE id = 1; ← TX1 のロック解放を待つ(ブロック)
→ 読み取りが書き込みをブロック → スループット低下
MVCC あり:
TX1: SELECT * FROM users WHERE id = 1; ← TX1 開始時点のスナップショットを読む
TX2: UPDATE users SET name = 'Bob' WHERE id = 1; ← 新しいバージョンを作成
→ TX1 は古いバージョンを、TX2 は新しいバージョンを、それぞれ独立に参照
→ 読み取りと書き込みが互いをブロックしない
PostgreSQL の MVCC 実装
PostgreSQL は「タプル(行)に複数のバージョンを持つ」アプローチ。
タプルの可視性判定
各タプルが持つ情報:
t_xmin: このタプルを INSERT したトランザクション ID
t_xmax: このタプルを DELETE/UPDATE したトランザクション ID(未削除なら 0)
t_infomask: コミット済みか、アボートかなどのビットフラグ
可視性ルール(Read Committed の場合):
1. t_xmin がコミット済み AND t_xmax が 0(未削除)→ 見える
2. t_xmin がコミット済み AND t_xmax がコミット済み → 見えない(削除済み)
3. t_xmin が未コミット → 見えない(まだ確定していない)
4. t_xmin が自分自身の TX → 見える(自分が INSERT した行)
# 可視性判定の擬似コード
def is_visible(tuple, current_tx, snapshot):
xmin = tuple.t_xmin
xmax = tuple.t_xmax
# 挿入した TX がアボートされた → 見えない
if is_aborted(xmin):
return False
# 挿入した TX がまだ実行中(自分以外)→ 見えない
if is_in_progress(xmin) and xmin != current_tx:
return False
# 挿入した TX がコミット済み
if is_committed(xmin):
# 削除されていない → 見える
if xmax == 0:
return True
# 削除した TX がコミット済み → 見えない
if is_committed(xmax):
return False
# 削除した TX がまだ実行中 → 見える(削除はまだ確定していない)
if is_in_progress(xmax):
return True
return False
UPDATE の内部動作
PostgreSQL の UPDATE は**「旧タプルを削除 + 新タプルを挿入」**として実装される。
-- 初期状態
-- Tuple A: (id=1, name='Alice', t_xmin=100, t_xmax=0)
BEGIN; -- TX ID = 200
UPDATE users SET name = 'Bob' WHERE id = 1;
-- 内部で起きること:
-- 1. 旧タプル A の t_xmax を 200 に設定(削除マーク)
-- 2. 新タプル B を作成: (id=1, name='Bob', t_xmin=200, t_xmax=0)
COMMIT;
-- 結果:
-- Tuple A: (id=1, name='Alice', t_xmin=100, t_xmax=200) ← Dead Tuple
-- Tuple B: (id=1, name='Bob', t_xmin=200, t_xmax=0) ← Live Tuple
Dead Tuple が蓄積する → テーブルが肥大化する(Bloat)→ VACUUM が回収する(Ch.11 で詳述)。
トランザクション分離レベル
-- 分離レベルの設定
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- デフォルト
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Read Committed(デフォルト)
各文(Statement)の開始時にスナップショットを取得。同じ TX 内でも文ごとに最新のコミット済みデータを読む。
-- TX1
BEGIN;
SELECT * FROM users WHERE id = 1; -- name = 'Alice'
-- TX2 が UPDATE + COMMIT
-- TX2: UPDATE users SET name = 'Bob' WHERE id = 1; COMMIT;
SELECT * FROM users WHERE id = 1; -- name = 'Bob'(最新が見える)
COMMIT;
Repeatable Read
TX 開始時に1つのスナップショットを取得し、TX 全体でそれを使い続ける。
-- TX1
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM users WHERE id = 1; -- name = 'Alice'
-- TX2 が UPDATE + COMMIT
SELECT * FROM users WHERE id = 1; -- name = 'Alice'(TX開始時のスナップショット)
COMMIT;
Serialization Anomaly:Repeatable Read では Phantom Read は防げるが、書き込みスキューは防げない。
Serializable(SSI: Serializable Snapshot Isolation)
PostgreSQL の Serializable は **SSI(Serializable Snapshot Isolation)**で実装されている。
SSI の仕組み:
Repeatable Read のスナップショットを基盤に、
トランザクション間の「読み書きの依存関係」を追跡する。
シリアライズ不可能な依存グラフ(rw-antidependency のサイクル)を
検出したら、1つの TX をアボートする。
利点:
- ロックベースの Serializable(2PL)と違い、読み取りがブロックされない
- MVCC の「読み書き非ブロック」を維持したまま Serializable を実現
コスト:
- 依存関係の追跡オーバーヘッド
- False Positive でアボートが増えることがある
分離レベルの比較
| 異常現象 | Read Committed | Repeatable Read | Serializable |
|---|---|---|---|
| Dirty Read | ✅ 防止 | ✅ 防止 | ✅ 防止 |
| Non-Repeatable Read | ❌ 起きる | ✅ 防止 | ✅ 防止 |
| Phantom Read | ❌ 起きる | ✅ 防止※ | ✅ 防止 |
| Serialization Anomaly | ❌ 起きる | ❌ 起きる | ✅ 防止 |
※ PostgreSQL の Repeatable Read は MVCC ベースのため Phantom Read も防止する(SQL 標準より厳しい)
MySQL InnoDB との MVCC 比較
| PostgreSQL | MySQL InnoDB | |
|---|---|---|
| バージョン管理 | ヒープに複数バージョンが共存 | Undo ログに旧バージョンを退避 |
| Dead Tuple | ヒープに残る → VACUUM で回収 | Undo ログから自動パージ |
| Bloat | 起きやすい(VACUUM 依存) | 起きにくい(Undo ログは自動回収) |
| Index の更新 | HOT で回避可能 | クラスタードインデックスの構造上必要 |
| 長時間 TX の影響 | Dead Tuple が回収できない | Undo ログが肥大化(history list length) |
MVCC のコスト
利点:
- 読み取りと書き込みが互いをブロックしない
- 一貫性のあるスナップショットを読める
- ロックの待ち時間がない
コスト:
- Dead Tuple の蓄積(Bloat)→ VACUUM が必要
- タプルヘッダのオーバーヘッド(23B/行)
- 可視性判定のCPUコスト
- 長時間トランザクションが Dead Tuple の回収を妨げる
まとめ
| 概念 | PostgreSQL での実装 |
|---|---|
| バージョン管理 | タプルに t_xmin / t_xmax を保持 |
| UPDATE | 旧タプルに削除マーク + 新タプル挿入 |
| 可視性判定 | TX ID + スナップショットで判定 |
| Read Committed | 文ごとにスナップショット |
| Repeatable Read | TX ごとにスナップショット |
| Serializable | SSI(依存グラフで検出) |
| Dead Tuple 回収 | VACUUM(Ch.11 で詳述) |