「今、何が見えているのか?」PostgreSQLのスナップショットを紐解く
やあ。データベースの運用や設計で悩んでいないかな?
PostgreSQLを触っていると、必ず一度は「トランザクション分離レベル」という壁にぶつかるはずだ。「なぜ他のトランザクションで更新したデータが見えないんだ?」「読み取り専用のクエリなのに、なぜブロックされるんだ?」……そんな疑問の根本には、いつも「スナップショット(Snapshot)」という存在がいる。
今日は、PostgreSQLの心臓部の一つであるこの「スナップショット」について、現場の視点から少し深掘りしてみよう。
—
スナップショットは「タイムカプセル」だ
PostgreSQLにおいて、スナップショットを一言で表すなら「トランザクションが世界を観測するためのタイムカプセル」だ。
PostgreSQLは、データに行を上書きするのではなく、新しいバージョンを書き込む(MVCC:多版同時実行制御)。テーブルの中に同じ主キーの行がいくつも存在できるのは、この仕組みがあるからだ。
トランザクションが開始された瞬間、PostgreSQLは現在のデータベースの状態を切り取る。その瞬間に「どのトランザクションがコミットされていて、どれが進行中なのか」を記録したリストを作るんだ。これがスナップショットの正体だよ。
実践:スナップショットの挙動を覗いてみる
言葉だけだと抽象的だから、実際に挙動を確認してみよう。2つのターミナルを開いて、以下の順序で試してみてほしい。
1. ターミナルA(トランザクション開始)
BEGIN;
SELECT FROM users WHERE id = 1; — ここでスナップショットが生成される
2. ターミナルB(データを更新)
UPDATE users SET name = ‘New Name’ WHERE id = 1;
COMMIT;
3. ターミナルA(再確認)
SELECT FROM users WHERE id = 1;
— さあ、名前はどうなっていると思う?
答えは、「更新前の古い名前」のままだよね。
ターミナルAで `BEGIN` した瞬間に取得されたスナップショットは、「ターミナルBのコミット」をまだ知らない(あるいは無視するようルール化されている)からだ。これがPostgreSQLの「Read Committed」の基本挙動だね。
なぜこれが重要なのか?
現場のエンジニアとして知っておいてほしいのは、「長いトランザクションは、古いスナップショットを抱え続ける」というリスクだ。
もし、トランザクションを開いたまま重いバッチ処理や外部APIの呼び出しをしていると、その間ずっと古いスナップショットを保持し続けることになる。すると、PostgreSQLは「この古いスナップショットからデータが見える可能性があるから、古いバージョンの行を消せないな」と判断して、不要になった行データ(Dead Tuple)の掃除(VACUUM)をサボり始めてしまうんだ。
結果として何が起きるか?
- テーブル肥大化: ストレージ容量が圧迫される。
- インデックス効率の低下: スキャンが遅くなり、パフォーマンスがガタ落ちする。
- トランザクションIDの周回問題: 極端なケースだけど、最悪の場合はデータベースが停止する。
だから、「トランザクションはできるだけ短く」という鉄則は、単なる綺麗事じゃなくて、スナップショットの寿命を管理するための生存戦略なんだよ。
実務で意識すべきポイント
- Repeatable Read / Serializable を使う時:
これらの分離レベルでは、トランザクションの最初から最後まで「同じスナップショット」を使い続ける。一貫性は保証されるけれど、他のトランザクションとの衝突(Serialization Failure)が増えることを覚悟しておく必要がある。エラーハンドリングの実装は必須だ。
- バックアップツールとの関係:
`pg_dump` などのツールも、裏側でスナップショットを活用している。一貫性のあるバックアップを取る際、PostgreSQLはどうやって「点」を打っているのか。内部的には `pg_export_snapshot()` なんていう関数もあって、複数のセッションで同じスナップショットを共有することだってできるんだ。
—
まとめ:データベースの視界をコントロールする
スナップショットは、データベースとアプリケーションの間の「視界の共有」を調整する仕組みだ。
「今、どのデータが見えているべきか?」
そう自問自答したとき、スナップショットの仕組みを思い出してみてほしい。そうすれば、複雑なバグやパフォーマンスのボトルネックが、少し違った角度から見えるようになるはずだ。
もし、スナップショットの仕組みについてもっと深く知りたくなったら、まずは `pg_stat_activity` で「今、どのセッションがいつからトランザクションを開いているか」を眺めてみることから始めてみないか?
現場からは以上だ。また何かあればいつでも聞いてくれよ。
コメント