📸
概念 #データ設計 #MVCC #トランザクション #PostgreSQL #並行性制御 #DDIA 📚 データ志向アプリケーション設計(DDIA)

MVCC(多版型同時実行制御)

PostgreSQLがスナップショット分離をどう実装するか。行の複数バージョンを保持することでロックなしに並行読み書きを実現するMVCCの仕組みを理解する

定義

MVCC(Multi-Version Concurrency Control):データの複数バージョンを同時に保持することで、読み取りと書き込みが互いをブロックしないようにする並行性制御の手法。

「書き込み中のデータを読もうとするとロックで待たされる」という問題を、「読み取りは常に自分が開始した時点のスナップショットを見る」ことで解決する。

問題:ロックベース並行性の限界

ロックベース(悲観的):
  TxA: 行Xを読みたい → TxBが書き込み中 → ロック待ち
  TxB: 行Xを書いている
  
  書き込みが読み取りをブロックする
  高トラフィックのシステムでボトルネックになる

MVCCは「古いバージョンを残しておく」ことでこれを回避する。

PostgreSQLのMVCC実装

行バージョンの物理的な表現

PostgreSQLの各行には隠しシステムカラムがある:

xmin: この行バージョンを作ったトランザクションID
xmax: この行バージョンを削除(更新)したトランザクションID(0なら生存中)
-- 隠しカラムを直接参照できる
SELECT xmin, xmax, id, name FROM users WHERE id = 1;

-- 結果例
xmin  | xmax | id | name
------+------+----+-------
  100 |    0 |  1 | Alice   ← TxID=100が作成、まだ生存中

UPDATEは「削除 + 挿入」

TxID=200がAliceをBobに更新する:

Before:
  行1: xmin=100, xmax=0,   name="Alice"  ← 生存中

After:
  行1: xmin=100, xmax=200, name="Alice"  ← TxID=200が削除済みとマーク
  行2: xmin=200, xmax=0,   name="Bob"   ← TxID=200が作成した新バージョン

古い行は物理的にはまだディスクにある。これがデッドタプル(dead tuple)

スナップショットの仕組み

トランザクション開始時、PostgreSQLは「スナップショット」を取得する:

スナップショットの構成:
  xmin:  このIDより小さいTxはすべてコミット済み
  xmax:  このIDより大きいTxはまだ開始されていない
  xip[]: この範囲内でまだ実行中のTxのIDリスト

可視性ルール:
  行が見えるのは:
    行のxmin < スナップショットのxmin(確実にコミット済み)
    かつ
    行のxmax = 0(削除されていない)
    または行のxmax > スナップショットのxmax(自分より後のTxが削除した)
TxA(xmin_snapshot=150で開始)が読み取る:
  行1: xmin=100 → 150より小さいのでコミット済みと判断 → 可視
  行2: xmin=160 → 150より大きいので未来のTx → 不可視
  行3: xmin=130, xmax=145 → コミット済みのTxが削除済み → 不可視

Read Committed vs Repeatable Read の違い

Read Committed:
  クエリごとに新しいスナップショットを取得
  → 同じTx内でも2回目の読み取りは別の結果になりうる

Repeatable Read(Snapshot Isolation):
  トランザクション開始時に1度だけスナップショットを取得
  → Tx内は常に同じスナップショットから読む
  → ノンリピータブルリードを防ぐ

VACUUM:デッドタプルの回収

MVCCでは古いバージョンが蓄積し続ける。VACUUMがそれを回収する。

VACUUM の動作:
  1. どのTxからも見えないデッドタプルを特定
  2. 物理的に削除してスペースを解放
  3. インデックスのデッドエントリも削除

AUTOVACUUM:
  PostgreSQLはデフォルトで自動的にVACUUMを実行
  テーブルの変更率が閾値を超えるとトリガー

問題:トランザクションIDの周回(XID Wraparound)

PostgreSQLのTxIDは32bit整数(約42億)。使い切ると周回して古いデータが「未来のTx」に見えてしまう。定期的なVACUUMが必須。

MVCCとWrite Skew

トランザクションで説明したWrite Skewが起きる理由もMVCCで説明できる:

TxA: SELECT count(*) WHERE on_call = true
  → スナップショット時点: 2人
TxB: SELECT count(*) WHERE on_call = true  
  → スナップショット時点: 2人(同じスナップショット)

TxA: UPDATE SET on_call = false(自分のスナップショットでは2人いる、条件満たす)
TxB: UPDATE SET on_call = false(同様)

両方コミット → 実際は0人

MVCCは「読んだ行が変わっていないか」のチェックを各行単位でしか行わない。他の行の変更が前提条件を壊したことを検出できない(これを検出するのがSSI)。

MVCCを使うDBエンジン

DBMVCC実装の特徴
PostgreSQLヒープ内に複数バージョン。VACUUM必要
MySQL InnoDBUndo Logに古いバージョンを保存
OracleUndo Tablespaceに保存
CockroachDBHLCタイムスタンプベースのMVCC

MySQLのUndo Log方式では現在のレコードは常に最新版。古いバージョンはUndo Logから逆算して構築する(PostgreSQLとは逆のアプローチ)。

関連概念

出典・参考文献

  • Martin Kleppmann, Designing Data-Intensive Applications (2017) Chapter 7
  • PostgreSQL Documentation, “MVCC” — postgresql.org/docs/current/mvcc.html
  • Dan R. K. Ports & Kevin Grittner, “Serializable Snapshot Isolation in PostgreSQL” (2012)
  1. 1. 🗄️データ志向アプリケーション設計:概要
  2. 2. 🧩データモデルとクエリ言語
  3. 3. 💾ストレージエンジンとインデックス
  4. 4. 🔁レプリケーション
  5. 5. 🍕パーティショニング(シャーディング)
  6. 6. 🔒トランザクションとACID
  7. 7. 分散システムの本質的な問題
  8. 8. 🤝一貫性と分散合意
  9. 9. 📦バッチ処理
  10. 10. 🌊ストリーム処理
  11. 11. 📋エンコーディングとスキーマ進化
  12. 12. 🔗Sagaパターンと分散トランザクション
  13. 13. 🏗️データシステムの統合設計
  14. 14. 📸MVCC(多版型同時実行制御)
  15. 15. 📊列指向ストレージとOLAP設計
  16. 16. 🕰️ベクタークロックと因果順序
  17. 17. 🔀CRDT(競合なし複製データ型)
  18. 18. 🔍クエリオプティマイザーと実行計画
  19. 19. キャッシュ戦略とRedis設計
  20. 20. 🔎全文検索と転置インデックス
  21. 21. 🌐NewSQL(分散ACIDデータベース)
  22. 22. 📝WALと論理レプリケーション
  23. 23. 🔌コネクションプーリング
  24. 24. 🚧ゼロダウンタイムマイグレーション
  25. 25. 🆔分散ID生成
  26. 26. 🔄N+1問題とDataLoaderパターン
  27. 27. 📈タイムシリーズDB
  28. 28. 🛡️Row Level Security(行レベルセキュリティ)
  29. 29. 📤Outboxパターン(トランザクショナルアウトボックス)
  30. 30. 💾DBバックアップとPITR
  31. 31. ⚠️データベース設計アンチパターン
  32. 32. 🕸️グラフDB深掘り
  33. 33. 🔋バックプレッシャーとサーキットブレーカー

出典: Martin Kleppmann, 'Designing Data-Intensive Applications' (2017) Chapter 7