多版型同時実行制御(MVCC)
MVCC は一つの行の論理バージョンを複数残し、読み取りと書き込みを重ねて進める。読み取りは保存済みの最新値をそのまま取らない。スナップショットを取得し、新しい順にバージョンを調べ、作成者と置換者がそのスナップショットの可視性条件を満たす最初の版を返す。
PostgreSQL の autovacuum が動いていても、更新の多い表に不要な行バージョンが蓄積するなら、長時間トランザクションと古いスナップショットを調べる手掛かりになる。安定した読み取りは履歴を必要とし、回収境界が固定されると履歴が増え続ける。
既定例は残高 $100 の口座行だ。T1 が S1 を取得した後、T2 が $120 の版を作ってコミットする。T1 はまだ $100 を読み、後から取得した新スナップショットは $120 を読む。異なるコミット済み状態を参照するので、どちらも正しい。
MVCC の仕組み
PostgreSQL の考え方を取り入れた簡略ルールを使う。各論理行バージョンは、作成トランザクション、その進行中・コミット済み・中止の状態、版を置き換えたトランザクション、その置換者が読者のスナップショットで可視かを記録する。
二つの判定を通過する必要がある。作成者がスナップショット S でコミット済みであり、置換者が存在しないか S ではコミット済みでないことだ。新しい順に調べ、最初の通過版を選ぶ。
V0 はコミット済み T0 が作った $100。T1 の最初の SELECT は、コミット済み集合 {T0} の S1 を取る。V0 の作成者は含まれ、S1 で可視な置換者はいないので、両方を通る。
T2 の更新は V0 を上書きしない。V0 の置換者を T2 と記録し、T2 が作成した V1=$120 を追加する。T2 が進行中なら V1 は作成者の判定に失敗する。T2 は S1 のコミット済み集合にないため V0 は通り、このモデルの T1 は書き手を待たず $100 を返せる。
T2 のコミットは状態を変えるが、S1 は変えない。T1 にとって V1 の作成者は S1 に含まれず、V0 の置換者も含まれない。後の S2 が {T0,T2} を含むと、V1 がその読者の最新可視版になる。
保存状態と可視状態は異なる。V0 と V1 が共存し、V1 が新スナップショット向けの現在値でも、古い活動中の S1 には V0 が正しい答えとなる。
ステップ実行
- 初期値は V0=
$100。作成者 T0 はコミット済み。 - T1 を開始する。Repeatable Read では、この例の
BEGIN自体はスナップショットを取らず、最初の文が S1={T0}を確定する。 - T1 が V0 の二つの判定を通り、
$100を受け取る。 - T2 が
UPDATE balance = 120を始める。V0 を保存したまま、進行中の置換者を記録する。 - T2 が V1=
$120を追加する。T1 は V1 を先に調べるが、T2 が S1 でコミット済みでないため拒否し、V0 の$100を再び読む。 - T2 がコミットする。未来のスナップショットは T2 を含められるが、S1 は
{T0}のまま。 - T1 が再読する。V1 は全体としてコミット済みでも S1 にはなく、三度目の
$100を返す。 - 後の T3 が S2=
{T0,T2}を取り、V1 の両判定を通って$120を返す。 - 清掃が V0 を調べる。S1 は T2 の置換コミットより古く、まだ V0 を必要とするため回収できない。
- T1 が終了して S1 を解放する。V0 を回収し、V1 だけが残る。
最終台帳は T1 が $100 → $100 → $100、T3 が $120。
隔離レベルを Read Committed に変え、更新順序は維持する。T1 の最初の文は S1 で $100。T2 が進行中の文は新しい S2 を取るが、T2 を含まず $100。コミット後の文は S3={T0,T2} で $120 を返す。変わるのは更新ではなくスナップショットの寿命だ。この例の Repeatable Read はトランザクション全体で安定したビューを保ち、Read Committed は文ごとに新しいコミット済みビューを取る。
古い版が蓄積する理由
新値のコミットだけでは、全旧版を捨てられるとは限らない。可視性は「一つのスナップショットがどの版を選ぶか」、回収は「全体の境界が進み、不要な版を安全に除去できると証明できるか」を判定する。
Long snapshot プリセットは T1 の S1 を保持し、T2、T3、T4 が $120、$140、$160 をコミットする。新スナップショットは各新値へ進むが、S1 は全書き手を除外する。T1 は V3、V2、V1 を拒否して V0=$100 を選ぶ。
表示は stored 1 / obsolete 0 から stored 4 / obsolete 3 へ変わる。T2 後は {V0}、T3 後は {V0,V1}、T4 後は {V0,V1,V2} の回収を境界が妨げる。
S1 が直接選ぶのは V0 だけで、V1 と V2 は選ばない。それでも、この PostgreSQL 風の保守的な回収モデルでは S1 が置換トランザクションより前に境界を固定し、不要な三つの版を残す。T1 の終了で境界が進むと V0、V1、V2 を別々のステップで除去し、V3=$160 が残る。
履歴は安定した読み取りを支え、更新の継続は保存量と清掃作業を増やす。「コミット済み」「S1 で可視」「回収可能」は別の判断だ。
一行が V バージョンを保存する場合、モデルは最大 V 候補を調べる。固定プリセットは最大 4 版。実エンジンは独自のメタデータ、インデックス、ページ配置、清掃方式を使うため、全データベースに共通する実行時間の式ではない。
境界ケース
- 書き手が進行中なら V1 の作成者判定は失敗し、読者はスナップショットに合う旧版を選ぶ。
- スナップショット後のコミットは、全体では完了していても旧ビューに含まれない。Repeatable Read はこの区別を保つ。
- Read Committed の新しい文は、前の文が含まなかったコミットを含められる。同じトランザクションでも返すコミット済み値が変わり得る。
- Aborted update は V1 の作成者を中止にする。旧・新スナップショットとも V1 を拒否し、V0 を選ぶ。失敗した置換先のコミット済み値を消さず、中止版を回収できる。
- 一つの旧スナップショットは最古版だけを選んでも、境界を固定して中間の不要版も保持させる場合がある。
- 保護するトランザクションが終わる、または文単位のスナップショットが解放されると、清掃対象が回収可能になる場合がある。
よくある間違い
- 保存済みの最新値が全読者に可視だと考える。
- 全隔離レベルで
BEGINがスナップショット境界だと考える。この PostgreSQL Repeatable Read の流れは最初の文で確定する。 - コミットが既存スナップショットを書き換えると考える。変わるのは未来のスナップショットに含められる内容だ。
- MVCC なら待機が一切ないと考える。本課は一更新中の非ブロッキング読み取りを切り出し、書き手同士の競合などを省く。
- V1 のコミット直後に V0 を消し、S1 の再読を壊す。
- 可視性と回収を同じ判定にする。S1 が直接選ばない中間版も境界が残す場合がある。
- 行バージョン、トランザクション、スナップショットを混同する。T2 は版を作り、S1 はコミット済みビューを表す。
created byとsuperseded byを PostgreSQL タプルの実際の列名と見なす。教育用モデルの論理ラベルだ。
モデルの範囲
論理バージョンのカードと明示的なコミット済み集合を使う。物理 PostgreSQL タプルチェーン、undo ログ型エンジン、全 MVCC ルールを再現しない。WAL、バッファープール、インデックス、書き手同士の競合ロック、自分の書き込み、サブトランザクション、Serializable Snapshot Isolation、HOT チェーン、トランザクション ID 周回、実際の vacuum スケジューリングは省略する。
保存配置、可視性メタデータ、隔離保証、回収方式はエンジンごとに異なる。二つの判定で版を選ぶ処理と、保守的な PostgreSQL 風境界で不要版を回収する処理を分けており、PostgreSQL の厳密な vacuum 実装ではない。
続けて学ぶ
同じ手法につながるレッスンや、次の概念への入口です。
鍵マークのあるレッスンの操作には有料アクセスが必要です。解説はすべて無料で読めます。
誤りや気になる点を見つけましたか? フィードバックを送る。