「トランザクション分離レベル、とりあえずデフォルトのREAD COMMITTEDでいっか」
……もし君がそう思っているなら、今日の話は少し耳が痛いかもしれないね。でも大丈夫。ここを理解すれば、君は「ただSQLが書けるエンジニア」から「データベースの挙動を支配できるエンジニア」へ一歩近づけるはずだ。
PostgreSQLのトランザクション分離レベル。これ、単なる設定値じゃなくて、「システムの性能と整合性のトレードオフをどこで妥協するか」という設計の要なんだ。
今日は、現場でよくハマるポイントを交えながら、この「見えない壁」の正体を紐解いていこうか。
—
1. READ COMMITTED:みんなの「デフォルト」の裏側
PostgreSQLのデフォルトは `READ COMMITTED` だ。これは非常にバランスがいい。文単位で最新のコミット済みデータを読みに行くから、基本的にはこれで困らない。
でも、ここには一つ大きな落とし穴がある。「同じトランザクション内でも、クエリごとに見える世界が変わる」ということだ。
— トランザクション開始
BEGIN;
SELECT count() FROM orders; — 100件
— この間に別の処理が10件追加してコミットしたとする
SELECT count() FROM orders; — 110件(!?)
COMMIT;
これを「ファジーリード(非再現読み取り)」と言う。もし君がバッチ処理で「データの集計」と「その後の更新」を一つのトランザクションでやる場合、この「読み取りのズレ」が致命的なバグを生むことがあるんだ。
2. REPEATABLE READ:一貫性の確保と「シリアライズ失敗」の代償
「一つのトランザクション中は、ずっと同じデータが見えていてほしい」。そう思うなら `REPEATABLE READ` の出番だ。これは、トランザクション開始時点の「スナップショット」を固定する。
ただし、ここでエンジニアを悩ませるのが 「シリアライズ失敗(Serialization Failure)」 というエラーだ。
— REPEATABLE READ モード
BEGIN;
UPDATE inventory SET count = count – 1 WHERE id = 1;
— 別のセッションが先に同じ行を更新してコミットしていた場合…
— 次の操作で「ERROR: could not serialize access…」が発生する
PostgreSQLは楽観的ロックに近い挙動をとる。つまり、「後から来たやつは、データの整合性が壊れる可能性があるならエラーにして弾く」という姿勢なんだ。
現場のアドバイス: このレベルを使うなら、アプリケーション側で「エラーが出たらリトライする」というロジックが必須になる。これを実装していないと、本番環境で突然「謎のエラー」が多発して泣きを見ることになるから注意してくれ。
3. SERIALIZABLE:最強の盾、あるいは諸刃の剣
最高峰の分離レベル `SERIALIZABLE`。これは、「複数のトランザクションを、まるで1つずつ順番に実行したかのような結果」を保証する。
仕組みとしては、SSI(Serializable Snapshot Isolation)という賢い技術が動いている。データの読み書きの依存関係をDBが監視し、競合が発生しそうになると先回りしてエラーを吐く。
性能への影響:
正直に言おう。`SERIALIZABLE` はめちゃくちゃ重い。競合を監視するためのオーバーヘッドが凄まじいから、アクセスが集中するテーブルでこれを使うと、DBのCPU使用率が跳ね上がる。「整合性さえ守ればいい」と思って気軽に使うと、システム全体が悲鳴を上げることになるんだ。
—
現場でどう使い分けるか?
僕の経験上、こんなふうに考えるのが一番だ。
1. 基本は `READ COMMITTED` で戦う
- ほとんどのWebサービスはこれで十分。もし一貫性が必要なら、`SELECT FOR UPDATE` を使って、必要な行だけ明示的にロックすればいい。
2. レポート作成や整合性が命のバッチ処理には `REPEATABLE READ`
- ただし、リトライ処理の実装はセット。これを忘れないこと。
3. どうしても `SERIALIZABLE` が必要な場合
- それは設計を見直すサインかもしれない。本当にそのテーブルを全ロックする必要があるか? アプリケーション層で楽観ロック(バージョンカラムなど)を実装して解決できないか? 一度立ち止まって考えてみてほしい。
—
最後に:データベースは「正直」だ
PostgreSQLは非常に優秀だ。でも、分離レベルを上げるということは、データベースに対して「厳密に監視しろ」と命令すること。それは必ずCPUやメモリといったリソースの消費量に直結する。
「性能が出ない」と嘆く前に、まずは自分の書いたクエリがどの分離レベルで動き、どの程度のロックを握っているのか。`pg_stat_activity` や `pg_locks` を眺めてみることから始めてみよう。
データベースの挙動を理解することは、君の書くコードをより堅牢で、より速くするための近道だ。
さて、今日はここまで。また何か深いところまで掘り下げたくなったら、いつでも聞きに来てくれよな。現場からは以上だ!
コメント