PostgreSQLのトランザクション分離レベル:MVCCの深淵と「正しさ」のトレードオフ
PostgreSQLを長年触っていると、ふと立ち止まって考えたくなることがあります。「結局のところ、このトランザクション分離レベルをどこまで信じていいのか?」という問いです。
多くの入門書は、ANSI SQLの規格をなぞって「ダーティリードがどうの」「ファントムリードがどうの」と解説しますが、現場で戦う我々にとって重要なのは、「そのレベルを選択したとき、PostgreSQLのMVCC(多版同時実行制御)が内部でどのような挙動をとるのか」という物理的な実装の理解です。
今日は、理論的な定義の先にある、パフォーマンスと整合性の境界線について少し深掘りしてみましょう。
—
1. Read Committed: 「デフォルト」という名の現実解
PostgreSQLのデフォルト設定である `Read Committed`。これは多くの場合、パフォーマンスと整合性のバランスが最も取れた賢い選択です。
内部挙動の核心
このレベルでは、ステートメント(SQL文)単位で新しいスナップショットが取得されます。つまり、同一トランザクション内でも、文と文の間に他のコミットが挟まれば、その変更が見えてしまうわけです。
- 防げる現象: ダーティリード(コミット前の未確定データは決して見えない)。
- 注意点: 非再現読み取り(Non-repeatable Read)が許容されます。これは障害ではなく「仕様」です。
エンジニアの視点:
高負荷なシステムで「なぜか集計結果が微妙にズレる」という相談を受けると、大抵はここが原因です。もしトランザクション内で一貫したデータセットを読み取る必要があるなら、迷わず `REPEATABLE READ` か、あるいはアプリケーション側での設計変更を検討すべきです。
—
2. Repeatable Read: 幻想としての「静止画」
`REPEATABLE READ` に切り替えると、トランザクション開始時のスナップショットが最後まで維持されます。
内部挙動の核心
ここで特筆すべきは、PostgreSQLの `Repeatable Read` はANSI SQLの仕様よりも強力だということです。いわゆる「ファントムリード」も発生しません。
しかし、ここで忘れてはならないのが「シリアライゼーション失敗(Serialization Failure)」という例外処理です。
- 競合の検知: 読み取った行が、後続の更新によって変更されていた場合、PostgreSQLはトランザクションを即座にロールバックさせます。
- エラーコード: `40001` が返されます。
エンジニアの視点:
これを使う場合、アプリケーションコード側に「リトライロジック」が必須であることを忘れてはいけません。DBが整合性を守ろうとしてくれた結果、「失敗しました、もう一度やり直して」と言っているわけですから、そこをハンドリングしてあげないとシステムは脆くなります。
—
3. Serializable: 理論上の頂点と、その代償
最強の分離レベルである `Serializable` は、SSI(Serializable Snapshot Isolation)という、非常に洗練されたアルゴリズムで実装されています。
内部挙動の核心
単純なロックによる排他制御ではなく、依存関係グラフを用いて「事実上の直列実行」を監視します。これは非常に賢い実装ですが、アプリケーションが競合を検知する頻度は爆発的に上がります。
エンジニアの視点:
「整合性が完璧なら全部 Serializable にすればいいのでは?」と考えるのは危険です。高負荷な環境でこれを有効にすると、あちこちでトランザクションがアボートし、システムが「リトライの嵐」に飲み込まれます。
私が現場でアドバイスするのは、「どうしても不可避な競合がある場合だけ、特定のテーブルや特定の処理に限定して使う」という戦略です。全体に適用するのは、多くの場合、パフォーマンスの自殺行為に近い。
—
トラブルシューティングの勘所
パフォーマンスが出ない、あるいは謎のデッドロックやアボートが発生している場合、以下の順で確認することをお勧めします。
1. 分離レベルの再考: `Serializable` や `Repeatable Read` を使っているなら、その業務ロジックで本当にそのレベルが必要か? `READ COMMITTED` に戻した上で、`SELECT FOR UPDATE` を適切に使うほうが、制御が明確で効率的なケースが多々あります。
2. トランザクションの長さ: トランザクションが長すぎるのが最大の悪です。MVCCのゴミ(Dead Tuple)が溜まり、VACUUMの負荷が増大します。分離レベルが高いほど、この影響は顕著に現れます。
3. ロック待機: `pg_stat_activity` を見て、どの分離レベルでどの程度の待機が発生しているか。ロック競合なのか、それともMVCCのバージョン管理コストなのかを切り分けましょう。
—
最後に:完璧な分離など存在しない
PostgreSQLは非常に堅牢ですが、データベースは魔法の箱ではありません。分離レベルの選択は、「データの正確さ」と「スループット」の間のトレードオフをどうハンドリングするかという、エンジニアの意志決定そのものです。
仕様書を丸暗記するのではなく、その裏にある「PostgreSQLがどうやって整合性を担保しようとしているのか」という設計思想に寄り添ってみてください。そうすれば、デバッグの際に見える景色が少し変わるはずです。
さて、あなたの目の前のトランザクションは、本当にそのレベルが必要ですか?
コメント