PostgreSQLのMVCCを深掘りする:なぜスナップショットは「魔法」のように見えるのか
PostgreSQLのアーキテクチャを語る上で、避けて通れないのがMVCC(多版同時実行制御)の核心である「スナップショット分離」です。多くのエンジニアが「Read Committedなら文単位で、Repeatable Readならトランザクション単位でスナップショットを取るんでしょ?」と理解していますが、その実装の深淵まで覗き込むと、単なるルール以上の「エレガントな設計」が見えてきます。
今日は、PostgreSQLがどのようにしてこの複雑な並行世界を平穏に保っているのか、その内部構造に切り込んでいこうと思います。
可視性判定の司令塔:`HeapTupleSatisfiesMVCC`
PostgreSQLのデータページには、行のバージョン情報として`xmin`(挿入したトランザクションID)と`xmax`(削除したトランザクションID)が刻まれています。これらを見て「この行は今見えているか?」を判定するのが、お馴染みの`HeapTupleSatisfiesMVCC`関数です。
この関数は、単に「IDの大小」を比較しているわけではありません。その裏側では、SnapshotData構造体に保持された「その瞬間の世界の定義」が全てを握っています。
- xmin/xmaxの解釈: 単なる数値の比較ではなく、トランザクションのコミット状況を記録したCLOG(Commit Log)と照らし合わせる必要があります。
- InProgress(実行中)の扱い: まだコミットされていないトランザクションの行をどう扱うか。これが分離レベルごとの「味付け」の差になります。
分離レベルを分かつ「スナップショットの呼吸」
分離レベルごとの挙動の違いを、実装の観点から整理してみましょう。
1. Read Committed:文ごとの「再起動」
このモードでは、クエリの実行開始ごとに新しいスナップショットを取得します。つまり、一つのトランザクション内であっても、クエリが違えば世界が書き換わっている可能性があるわけです。
- 内部実装の妙: 文が開始されるたびに`GetSnapshotData()`が呼ばれ、その時点での「実行中のトランザクションリスト」がスナップショットに焼き付けられます。これにより、直前のクエリでコミットされた変更が、次のクエリで見えるようになります。
2. Repeatable Read:一貫性の固定
ここでは、トランザクション開始時に一度だけスナップショットを取得し、以降はそれを使い回します。
- パフォーマンストラブルの種: ここで厄介なのが「直列化失敗」です。もし更新しようとした行が、自分のスナップショットより新しいものに書き換えられていたら、PostgreSQLは迷わずエラーを投げます。これは、データの一貫性を守るための「強固な防壁」です。
3. Serializable:SSI(Serializable Snapshot Isolation)の魔法
これはPostgreSQLの中でも最も美しい実装の一つです。単純なロックによる排他制御ではなく、「SIREADロック」という特殊な追跡手法を用います。
- 何をしているのか: 読み取ったデータと書き込んだデータの「依存関係」をグラフ構造で管理しています。このグラフに「循環(サイクル)」が検知された瞬間、初めて「直列化不可能」と判断してエラーを返します。
- メリット: 読み取り専用のトランザクションが、書き込みトランザクションをブロックすることがほとんどありません。非常に高い並行性を維持しながら、理論上の完全な直列化を実現しています。
現場で遭遇する「罠」とエンジニアの視点
運用現場で「なぜか特定のトランザクションだけ遅い」「Serializableで頻繁に再試行が発生する」といったトラブルに遭遇したことはありませんか?
原因の多くは、スナップショットの寿命にあります。
- 長大なトランザクション: トランザクションを長時間開いたままにすると、`xmin`が古いまま固定されます。すると、Autovacuumはそれより新しいデータの削除(デッドタプル)を片付けることができず、テーブルの肥大化(Bloat)を招きます。これが「スナップショットがVACUUMを殺す」という現象の正体です。
- SSIのオーバーヘッド: Serializableレベルを多用しすぎると、SSIロックを管理するための共有メモリ(SIREADロック領域)が枯渇する恐れがあります。`max_pred_locks_per_page`のチューニングが必要になる場面は、まさに「高負荷なシステムを構築している」という証左でもあります。
最後に:データベースは「時間」を管理する芸術
PostgreSQLのMVCC設計が優れているのは、それが単なる「隔離」ではなく、「時間的な一貫性をいかに効率よく再構成するか」という問いに対する一つの回答だからです。
コードを追うときは、ぜひ`src/backend/storage/lmgr/predicate.c`あたりを覗いてみてください。数千行のコードの中に、データの整合性を守ろうとする先人たちの執念が詰まっています。
データベースを深く理解するということは、単にクエリを速くすることではありません。システムが「今、何を見ているのか」という時空の感覚を持つことこそが、一流のエンジニアへの近道だと私は信じています。
皆さんのデータベースに、今日一日、デッドロックの神が微笑みますように。
コメント