なぜその行は「見えない」のか?PostgreSQLのMVCCとタプル可視性の深淵
PostgreSQLを長く触っていると、一度は必ず直面する壁があります。「確かにデータはINSERTしたはずなのに、クエリで取得できない」「なぜか古いバージョンの行が残り続けている」。
多くのエンジニアはこれを「MVCC(多版同時実行制御)の仕様ですね」と片付けて終わらせてしまいます。しかし、データベースのパフォーマンスが極限まで求められる現場では、この「タプルの可視性判定」こそが、クエリの実行速度やVACUUMの効率、さらにはインデックスの肥大化を左右する最大の要因になるのです。
今日は、PostgreSQLが裏側でどうやって「このデータは今、このセッションから見えるべきか?」を判断しているのか、その冷徹なまでの論理構造を紐解いてみましょう。
—
物理構造に刻まれた「生存の証明」
PostgreSQLの行(タプル)は、単にユーザーのデータを持っているわけではありません。ページ内の物理的なヘッダー情報、つまり `HeapTupleHeaderData` には、可視性を判定するための重要なメタデータが刻まれています。
具体的には、以下のフィールドが判定の要となります。
- t_xmin: このタプルを作成したトランザクションID (XID)
- t_xmax: このタプルを削除、または更新したトランザクションID
- t_infomask: フラグ群(タプルがコミット済みか、更新中かなどを保持)
PostgreSQLのMVCCにおいて、可視性は「今のトランザクションのSnapshot」と「タプルのヘッダー情報」の照らし合わせという、非常にシンプルかつ高速な条件分岐で決定されます。
可視性判定の「ルール」
クエリが実行されるとき、PostgreSQLは `HeapTupleSatisfiesMVCC` という関数を呼び出します。ここで何が行われているかというと、要するに「今の自分(トランザクション)から見て、この行は生きていて、かつコミットされているか?」というチェックです。
判定の主なロジックは、直感に反するほどドライです。
1. xminの確認:
- もし `xmin` が「未コミット」なら、その行はまだ存在しないのと同じ(自分自身が挿入した場合を除く)。
- もし `xmin` が「アボート済み」なら、その行は最初から無かったものとして扱う。
2. xmaxの確認:
- `xmax` が空、あるいは「未コミット」であれば、その行は生きている。
- もし `xmax` が「コミット済み」であれば、その行は削除されているため、基本的には見えない。
この仕組みの面白いところは、「削除」という操作が物理的な削除を意味しないという点です。単に `xmax` を書き換えるという「更新」作業に過ぎません。これが、PostgreSQLにおいてVACUUMが不可欠である最大の理由です。
パフォーマンストラブルの「温床」:Visibility Mapの落とし穴
さて、ここからが現場のエンジニアとしての腕の見せ所です。
可視性の判定において、PostgreSQLは可能な限りI/Oを減らそうとします。そこで登場するのが Visibility Map (VM) です。VMは、各ページ内のすべてのタプルが「どのトランザクションから見ても完全に可視である」ことを保証するビットマップです。
もしVMが「このページは全タプル可視」と宣言していれば、PostgreSQLはわざわざタプルヘッダーを覗きに行くコストを省きます(Index-Only Scanの高速化の秘密ですね)。
しかし、ここでトラブルが発生します。
頻繁にUPDATEが行われるテーブルでは、ページ内のタプルが常に更新され続けるため、VMのビットがなかなか立ちません。結果として、「本来ならインデックスだけで結果を返せるはずなのに、いちいちヒープまでタプルを見に行かざるを得ない」という、Index-Only Scanの劣化が起こります。
現場で意識すべき「Snapshotの鮮度」
大規模なバッチ処理や、長期間実行されるトランザクションを扱う際に注意すべきなのが「Snapshotの寿命」です。
トランザクションが開始されると、その時点での「アクティブなXIDリスト」をSnapshotとして保持します。もし、このトランザクションが非常に長い時間をかけて動いていると、その間、PostgreSQLは「このトランザクションがまだ動いているから、古いバージョンの行も消してはいけない」と判断します。
これが俗に言う「不要なタプルの滞留」です。
- インデックスの肥大化: ヒープが掃除されず、インデックスもインデックスのまま放置される。
- VACUUMの無効化: オートバキュームが働こうとしても、古いSnapshotが障害となってデッドタプルを回収できない。
最後に:データベースと対話する
PostgreSQLの可視性管理は、非常に堅牢ですが、同時に「適切にメンテナンスしてやらないと牙を剥く」という側面を持っています。
システムが遅くなったとき、単に「クエリを直せ」と言うだけでは不十分です。その裏で、タプルがどれだけ滞留し、VMがどれだけ機能し、VACUUMがどの程度追いついているのか。そうした「PostgreSQLの呼吸」を感じ取れるようになると、チューニングの景色は劇的に変わります。
DBエンジニアの仕事は、SQLを書くことではありません。PostgreSQLという生命体と対話し、そのメモリとストレージの効率を最大化することです。
もし次に、原因不明の低速化に遭遇したら、まずは `pg_stat_user_tables` の `n_dead_tup` を見てみてください。そこに、あなたの知らない「見えないタプル」たちがひっそりと溜まっているはずですから。
コメント