【実務・中級編】 MVCC可視性判定ルール – PostgreSQL

PostgreSQLのMVCC、その「見え方」の秘密に迫る!~先輩エンジニアが語るタプル可視性判定の裏側~

やあ、みんな!今日はPostgreSQLの、ちょっと奥深いけれど、知っておくと「なるほど!」って膝を打つような、MVCC(Multi-Version Concurrency Control)における「タプル可視性判定ルール」について、僕なりの経験も交えながら、ざっくばらんに話していこうと思う。

「MVCCって、なんとなくデータが複数バージョン持ってるやつだよね?」って思ってる人もいるかもしれない。もちろん、それは正しい。でも、その「複数バージョン」の中から、「今、このトランザクションから見ると、どのバージョンが見える(可視性がある)のか?」 をどうやって判断してるか、ってところまで掘り下げて考えてみたことあるかな?

これが、実はPostgreSQLのパフォーマンスを支える、めちゃくちゃ重要な仕組みなんだ。そして、この仕組みを理解しておくと、パフォーマンスチューニングのヒントが見えたり、デバッグが楽になったり、何よりPostgreSQLというデータベースへの理解がグッと深まるはずだよ。

なんで「可視性判定」なんて面倒なことをするのか?

まず、そもそもの話から。なんでPostgreSQLは、こんなに複雑な「可視性判定」なんてことをしているんだろう?

それは、ずばり「同時実行性」を高いレベルで実現するためなんだ。

多くのデータベースでは、複数のトランザクションが同時にデータを更新しようとすると、ロックという仕組みで「このデータは今、Aさんが触ってるから、Bさんは待っててね!」ってなる。でも、ロックってのは、どうしても処理のボトルネックになりやすいんだよね。待たされるトランザクションが増えれば増えるほど、システム全体のレスポンスが悪くなっちゃう。

そこで、PostgreSQLのMVCCの出番だ。MVCCは、データを更新するときに、古いデータを削除するんじゃなくて、新しいバージョンを「追加」するんだ。そして、各データ(タプルって呼ぶよ)には、それが「いつ」「どのトランザクションによって」作られたのか、あるいは「いつ」「どのトランザクションによって」無効化されたのか、といった情報がヘッダに記録されている。

この「ヘッダ情報」のおかげで、PostgreSQLはロックを最小限に抑えつつ、各トランザクションが「自分が見るべきデータ」を判断できるようになる。これが、PostgreSQLが多くのトランザクションを同時に、かつ効率的に捌ける秘密の一つなんだ。

タプルのヘッダ情報と「可視性判定」の主役たち

さて、ここからが本題。タプルの可視性を判定するために、PostgreSQLが注目しているヘッダ情報を見ていこう。

タプルのヘッダには、主にこんな情報が含まれている。

  • `xmin`: このタプルを生成したトランザクションID。
  • `xmax`: このタプルを無効化したトランザクションID。もし、まだ無効化されていなければ、この値は0になる。
  • `cmin`: このタプルを生成したトランザクションのコマンドID(トランザクション内でのコマンドの順番)。
  • `cmax`: このタプルを無効化したトランザクションのコマンドID。

「トランザクションID」とか「コマンドID」って言われても、ピンとこないかもしれないね。簡単に言うと、トランザクションIDは「いつ、どのトランザクションが動いたか」を識別するユニークな番号で、コマンドIDは「そのトランザクションの中で、何番目の操作か」を示すものだよ。

PostgreSQLは、これらの情報と、「現在のトランザクションのスナップショット」 を照らし合わせることで、あるタプルが「見える」のか「見えない」のかを判断しているんだ。

スナップショットって何?~トランザクションの世界線~

「スナップショット」っていう言葉も、MVCCを語る上では外せないキーワードだ。

PostgreSQLの各トランザクションは、開始した時点でのデータベースの状態を「スナップショット」として保持している。これは、ある意味で「そのトランザクションにとっての、その時点でのデータベースの『世界線』」みたいなものだ。

このスナップショットには、大きく分けて以下の情報が含まれている。

1. アクティブなトランザクションIDのリスト: 現在実行中の(コミットされていない)トランザクションのID。
2. コミット済みのトランザクションIDのリスト: 最近コミットされたトランザクションのID。
3. XID Wrap Around情報: トランザクションIDが枯渇しないようにするための仕組みに関連する情報。

で、このスナップショットと、さっき説明したタプルのヘッダ情報 (`xmin`, `xmax`) を使って、具体的にどうやって可視性を判定するのか、そのルールを見ていこう。

タプル可視性判定の具体的なルール(ここがキモ!)

さあ、いよいよ核心部分だよ!あるトランザクション(これを「現在のトランザクション」と呼ぼう)が、あるタプルを「見る」ことができるかどうかを判断するルールは、基本的に以下のようになる。

ルール1: タプルが「過去のトランザクション」によって生成され、かつ「過去のトランザクション」によって削除されていない場合

  • 条件:
  • `xmin` が、現在のトランザクションよりも前のトランザクションIDである。
  • かつ、`xmax` が `0` である(つまり、まだ無効化されていない) OR `xmax` が、現在のトランザクションよりも前のトランザクションIDである(つまり、過去のトランザクションによって無効化された)。
  • 結果: 見える(可視性あり)

これは、一番シンプルなケースだね。過去のトランザクションが作ったデータで、まだ消されていないか、あるいは過去のトランザクションによって消されたデータなら、現在のトランザクションから見れば「存在した」あるいは「削除された」と判断できる。

ルール2: タプルが「現在のトランザクション」によって生成された場合

  • 条件:
  • `xmin` が、現在のトランザクションIDと同じである。
  • 結果: 見える(可視性あり)

これは、「自分が今、書いているデータは、自分からは見える」という、当たり前だけど大事なルール。ただし、ちょっと注意点があって、`cmin` が現在のトランザクションの `cmin` より大きい場合、つまり、現在のトランザクション内で後で実行されたコマンドによって生成されたタプルは、そのコマンドよりも前の時点でのスナップショットからは見えない、ということもある。でも、基本的には「自分が作ったものは見える」と思っておこう。

ルール3: タプルが「現在のトランザクション」によって無効化された場合

  • 条件:
  • `xmax` が、現在のトランザクションIDと同じである。
  • 結果: 見えない(可視性なし)

「自分が今、削除(UPDATEやDELETE)しようとしているデータは、自分からは見えない」というルール。UPDATEの場合、実際には古いタプルに `xmax` が設定されて無効化され、新しいタプルが `xmin` を現在のトランザクションIDとして生成される。だから、更新途中のデータは、そのトランザクション内では見えなくなるんだ。

ルール4: タプルが「まだコミットされていない、別のトランザクション」によって生成された場合

  • 条件:
  • `xmin` が、現在のトランザクションよりも後のトランザクションIDである。
  • かつ、`xmax` が `0` である。
  • 結果: 見えない(可視性なし)

これは、「まだコミットされていない、未来のトランザクションのデータは、自分からは見えない」という、MVCCの基本原則だ。もし、これを許してしまうと、後でロールバックされたデータが見えてしまうことになり、データの整合性が保てなくなるからね。

ルール5: タプルが「まだコミットされていない、別のトランザクション」によって無効化された場合

  • 条件:
  • `xmax` が、現在のトランザクションよりも後のトランザクションIDである。
  • かつ、`xmax` が `0` でない。
  • 結果: 見えない(可視性なし)

これも同様に、「まだコミットされていない、未来のトランザクションによる削除は、自分からは見えない」というルール。

ルール6: XID Wrap Around に関する特殊なケース

これは少し高度な話になるんだけど、トランザクションIDは循環するので、古いトランザクションIDが新しいトランザクションIDのように見える場合がある。PostgreSQLはこの「XID Wrap Around」を考慮して、より正確な可視性判定を行う。この辺りは、普段の運用で意識することは少ないかもしれないけど、PostgreSQLの設計の堅牢さを示している部分だね。

具体的な使用例:UPDATE文の裏側

例えば、こんなUPDATE文があったとしよう。

UPDATE products SET price = 1200 WHERE id = 1;

このUPDATE文が実行されるとき、PostgreSQLの内部では何が起こるか、MVCCの観点から見てみよう。

1. 現在のトランザクションIDの取得: まず、このUPDATE文を実行しているトランザクションのID(仮に `T1` とする)が取得される。
2. 対象タプルの検索: `id = 1` の条件に合致するタプルが検索される。
3. 古いタプルの無効化: 検索されたタプル(仮に `P1` とする)の `xmax` に、現在のトランザクションID `T1` が設定される。これで、タプル `P1` は「トランザクション `T1` によって無効化された」状態になる。
4. 新しいタプルの生成: 新しい価格 `price = 1200` を持つ新しいタプル(仮に `P2` とする)が生成される。このタプルの `xmin` には、現在のトランザクションID `T1` が設定され、`xmax` は `0` のまま(まだ無効化されていない状態)。
5. 可視性判定:

  • タプル `P1` は、`xmax` が `T1` なので、トランザクション `T1` から見ると「無効化された」と判定され、見えなくなる。
  • タプル `P2` は、`xmin` が `T1` なので、トランザクション `T1` から見ると「自分が生成した」と判定され、見える。

このように、UPDATEは「古いタプルを無効化して、新しいタプルを生成する」という、2つの操作の組み合わせとして実現されているんだ。DELETEは無効化だけ、INSERTは生成だけ、というイメージだね。

スナップショットの生成プロセス

スナップショットは、トランザクションが開始されるたびに生成される。そのプロセスは、大まかに言うと以下のようになる。

1. トランザクションの開始: `BEGIN` または `START TRANSACTION` が実行される。
2. XIDの割り当て: 現在のトランザクションに、一意のトランザクションID (`xmin`) が割り当てられる。
3. スナップショット情報の構築:

  • アクティブなトランザクションID: 現在実行中の他のトランザクションのIDを収集する。
  • コミット済みトランザクションID: 最近コミットされたトランザクションのIDも参照し、それらのトランザクションが生成したタプルは、基本的には見えると判断する(ただし、現在のトランザクションより前にコミットされている必要がある)。
  • XID Wrap Around: 循環を考慮した情報もセットアップする。

このスナップショット情報が、そのトランザクションがデータにアクセスする際の「物差し」となるんだ。

実践的なアドバイスと注意点

さて、ここまでMVCCの可視性判定について説明してきたけど、これが実務でどう役立つか、いくつかポイントを挙げておくね。

  • パフォーマンスチューニングのヒント:
  • `VACUUM` の重要性: `xmax` が設定された(無効化された)タプルは、すぐにディスクから消えるわけじゃない。これらは「デッドタプル」と呼ばれ、ディスク容量を圧迫したり、インデックススキャンを遅くしたりする原因になる。`VACUUM` は、これらのデッドタプルを整理し、ディスクスペースを再利用してくれる非常に重要な処理なんだ。`VACUUM` が適切に実行されていないと、パフォーマンスが著しく低下することがあるから、定期的な実行やautovacuumの設定をしっかり確認しよう。
  • 長時間実行トランザクションの監視: 長時間実行されているトランザクションは、古いスナップショットを保持し続けるため、そのトランザクションが参照するタプルが `VACUUM` によって回収されにくくなる。これが「VACUUM debt」や、ディスク容量の増大につながることもある。`pg_stat_activity` などで長時間トランザクションを監視し、必要であれば原因を調査・改善することが重要だ。
  • デバッグの強力な武器:
  • 「データが見えない!」という時の原因究明: もし、他のトランザクションで更新したはずのデータが、自分のトランザクションから見えない場合、それはMVCCの可視性判定ルールに引っかかっている可能性が高い。
  • 自分のトランザクションが、その更新が行われる前のスナップショットを保持しているのかもしれない。
  • あるいは、更新したトランザクションがまだコミットされていないのかもしれない。
  • `pg_stat_activity` で、自分のトランザクションの `xact_start` (トランザクション開始時刻)や、他のトランザクションの `xact_start`、`state`(activeかidle in transactionかなど)を確認すると、原因特定の手がかりになる。
  • `pg_logical_slot_get_changes` や `pg_logical_slot_peek_changes` での挙動理解: 論理レプリケーションなどで、トランザクションの変更履歴を取得する際にも、この可視性判定の知識が役立つ。どのトランザクションの変更が、どのタイミングで取得されるのかを理解するのに、MVCCの仕組みは不可欠だ。
  • トランザクション分離レベルの理解:
  • PostgreSQLでは、`READ COMMITTED`(デフォルト)や `REPEATABLE READ` といったトランザクション分離レベルが設定できる。これらのレベルによって、スナップショットがどのように扱われるか、可視性判定にどのような影響があるかが変わってくる。例えば、`REPEATABLE READ` では、トランザクション開始時のスナップショットがずっと保持されるため、より一貫性のあるデータが見えるが、その分、競合が発生しやすくなる可能性もある。

まとめ

今日は、PostgreSQLのMVCCにおけるタプル可視性判定ルールについて、その基本的な考え方から具体的なルール、そして実務での活用方法まで、僕の経験を交えながら話してきた。

  • MVCCは、ロックを最小限に抑え、高い同時実行性を実現するための仕組み。
  • タプルのヘッダ情報 (`xmin`, `xmax`) と、トランザクションのスナップショットが可視性判定の鍵。
  • ルールを理解することで、「なぜデータが見えたり見えなかったりするのか」がクリアになる。
  • `VACUUM` の重要性や、長時間トランザクションの監視は、パフォーマンス維持に不可欠。

この知識は、PostgreSQLを「使う」だけでなく、「深く理解して使いこなす」ための強力な武器になるはずだ。ぜひ、日々の開発や運用で意識してみてほしい。

もし、もっと詳しく知りたい部分や、疑問に思ったことがあれば、いつでも聞いてくれ!現場で一緒に頑張っていこうぜ!

コメント

タイトルとURLをコピーしました