MVCC の内部実装
MVCC が読み取りを止めない理由を、行に隠れたバージョン情報と可視性判定まで分解して理解できます。PostgreSQL と InnoDB の実装差を押さえれば、肥大化やパージ遅延の原因も見抜けます。
- 1.MVCC は1行を上書きせず複数バージョンを残し、各行のトランザクション ID とスナップショットを突き合わせて見える版を決める。
- 2.PostgreSQL は古い版を本体テーブルに直接残す追記型で VACUUM が掃除役、InnoDB は最新版を本体に置き旧版を undo ログから再構成しパージが掃除役。
- 3.長時間トランザクションは古い版の回収を止め、テーブル肥大化や undo ログ膨張を招くため短く保つのが鉄則。
MVCC を「内部」から見る
MVCC(多版同時実行制御)の概念は「行を上書きせず複数バージョンを持ち、読み取りを止めない」ことです(→ ロックと MVCC)。ここでは概念の一段下、各行に隠れているメタ情報・可視性判定アルゴリズム・古い版の回収を、PostgreSQL と InnoDB の実装差を交えて掘り下げます。
要点は2つだけです。第一に、どのトランザクションも自分のスナップショットに照らして「この版は自分に見えるか」を判定する。第二に、見えなくなった古い版を誰かが**回収(GC)**しないとストレージが膨れ続ける。この2つの仕組みが製品ごとにどう違うかが、運用の勘所を分けます。
行に埋め込まれたバージョン情報
MVCC を実現するには、各行が「いつ作られ、いつ消されたか」を持つ必要があります。これがバージョンチェーンの土台です。
| PostgreSQL(ヒープタプル) | InnoDB(クラスタ化インデックス行) | |
|---|---|---|
| 旧版の置き場所 | 本体テーブル内に新タプルとして追記 | 本体は最新版のみ。旧版は undo ログに記録 |
| 生成トランザクション | xmin(作成した XID) | DB_TRX_ID(最後に更新した TRX ID) |
| 削除/旧版化の印 | xmax(削除・更新した XID) | DB_ROLL_PTR(旧版を再構成する undo へのポインタ) |
| バージョンの連結 | ctid で新タプルを指す | roll pointer で undo を辿る |
PostgreSQL では UPDATE が「古いタプルに xmax を書き、新しいタプルを別の物理位置に追記する」という追記型です。同じ行の歴代バージョンが本体テーブルに点在し、ctid で鎖のように繋がります。
InnoDB は逆で、本体(クラスタ化インデックス)には常に最新版だけを置きます。更新時は変更前のイメージを undo ログに退避し、行ヘッダの DB_ROLL_PTR がそこを指します。古い版が必要な読み手は、最新行から roll pointer を辿って undo を適用し直し、過去のイメージをその場で再構成します。
PostgreSQL(旧版も本体に残る追記型)
heap: [v1 xmin=100 xmax=150] -> ctid -> [v2 xmin=150 xmax=0]
InnoDB(本体は最新、旧版は undo から再構成)
row: [latest DB_TRX_ID=150 DB_ROLL_PTR=*]
|
undo: [prev image TRX=100]
可視性判定:その版は「自分に見えるか」
トランザクションが行を読むとき、バージョンチェーンの各版について「これは自分から見える版か」を判定します。基準になるのがスナップショットで、ざっくり「この瞬間に確定済み(コミット済み)の世界」を表します。
PostgreSQL のスナップショットは概ね次の3要素です。これで「ある XID がスナップショット時点でコミット済みか」を判定します。
xmin : これ未満の XID は全て完了済みとみなす境界
xmax : これ以上の XID は未来(未開始)とみなす境界
xip : xmin〜xmax の間で「まだ実行中だった」XID の集合
ある版が見えるかの判定は、おおむね次の擬似コードになります。
visible(tuple, snapshot):
# 作成側: xmin がスナップショットから見てコミット済みか
if not committed_before(tuple.xmin, snapshot): return false
# 削除側: xmax が未設定、または未コミットなら、まだ生きている
if tuple.xmax is empty: return true
if not committed_before(tuple.xmax, snapshot): return true
return false # xmin/xmax ともにコミット済み = もう消えた版
つまり「作成は見える時点まで、削除はまだ見えない時点」の版だけが、自分に見える1版です。InnoDB も考え方は同じで、行の DB_TRX_ID を read view(自分のスナップショットに相当)と突き合わせ、見えなければ roll pointer を辿って見える版に到達するまで過去へ遡ります。
READ COMMITTED は文ごとに新しいスナップショットを取り、REPEATABLE READ はトランザクション開始時の1枚を最後まで使います。同じ MVCC でも、可視性判定に渡すスナップショットの鮮度が違うだけで挙動が変わる、という理解が重要です(→ トランザクション分離レベル)。
古い版の回収:VACUUM とパージ
可視性判定の結果「もう誰にも見えない」版は、いずれ物理的に片付けないとストレージが膨れます。ここが両者で最も性格が異なる部分です。
| 観点 | PostgreSQL(VACUUM) | InnoDB(purge) |
|---|---|---|
| 掃除する対象 | 本体テーブルに残った死んだタプル(dead tuple) | 不要になった undo ログのレコード |
| 膨らむ場所 | テーブル/インデックス本体(bloat) | undo テーブルスペース |
| 実行主体 | autovacuum(自動)/ 手動 VACUUM | purge スレッド(バックグラウンド) |
| 回収の判断 | 最古スナップショットより古い死タプルを再利用可能に | どの read view からも参照されない undo を解放 |
PostgreSQL の VACUUM は、死んだタプルが占めていた領域を同じテーブル内で再利用可能にします(ファイル自体は基本縮まらず、空きを次の追記に回す)。怠ると本体もインデックスも肥大化し、走査コストが上がります。加えて XID は約42億で一周する有限値なので、VACUUM には古い行の XID を「凍結(freeze)」してラップアラウンドを防ぐ重要な役目もあります。
InnoDB の purge は、最新版を本体に置く構造ゆえ本体は太りにくい一方、解放できない undo が溜まると undo テーブルスペースが膨張します。どちらも「最も古い読み手がどこまで遡る可能性があるか」が解放可能ラインを決めます。
読みっぱなしの長いトランザクションが1本でもあると、「その読み手がまだ古い版を見るかもしれない」と判断され、PostgreSQL では死タプルが回収されず bloat が進み、InnoDB では古い undo が解放されずログが膨張します(PostgreSQL では pg_stat_activity に残る古い xact_start や VACUUM が回収できない死タプル数の増加、InnoDB では SHOW ENGINE INNODB STATUS の History list length の増大が典型的な兆候)。アイドルのまま開きっぱなしの BEGIN が最大の敵で、対策は単純にトランザクションを短く保つことです。
実装差が運用に効く場面
同じ MVCC でも、ストレージ構造の違いがそのまま性能特性に表れます。
| 場面 | PostgreSQL(追記型 + VACUUM) | InnoDB(最新版 + undo) |
|---|---|---|
| 大量 UPDATE 直後 | 死タプルが増え、VACUUM 完了まで bloat が残る | 本体は据え置き、undo が一時的に増える |
| 大量行の長距離読み | 本体を読むだけで版が揃い読みは軽い | roll pointer で undo を多数再構成し読みが重くなりうる |
| インデックスの版 | 古いタプルへの参照が残り得る(HOT で緩和) | セカンダリは主キー経由で最新版を辿り直す |
PostgreSQL の HOT(Heap-Only Tuple)更新は、インデックス対象列を変えない更新でインデックス更新を省き、bloat を抑える最適化です。InnoDB は「読みのたびに過去を再構成する」コストがある代わり、本体が締まっているため更新の局所性に強い、という対照になります。どちらが優れているかではなく、膨らむ場所と重くなる操作が逆だと捉えると、チューニングの当たりが付けやすくなります。
まとめ
- MVCC の心臓部は、各行の
xmin/xmax(PostgreSQL)やDB_TRX_ID/DB_ROLL_PTR(InnoDB)と、スナップショットを突き合わせる可視性判定にある。 - PostgreSQL は旧版を本体に残す追記型で、掃除役は
VACUUM、太るのはテーブル本体。 - InnoDB は本体に最新版だけを置き旧版をundo から再構成し、掃除役は purge、太るのは undo ログ。
- いずれの実装でも、最古の読み手が回収可能ラインを決めるため、長時間トランザクションを避けることが肥大化対策の本質。
ACID の I(独立性)が MVCC でどう支えられるかは ACID も合わせて参照してください。
データベース Article
MVCC の内部実装を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
データベース
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
PostgreSQL は古い版を本体テーブルに直接残す追記型で VACUUM が掃除役、InnoDB は最新版を本体に置き旧版を undo ログから再構成しパージが掃除役。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「データベース / MVCC」に近いか確認する。
- 強みである「MVCC は1行を上書きせず複数バージョンを残し、各行のトランザクション ID とスナップショットを突き合わせて見える版を決める。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。