「またデータベースのロックでハマったのか?」
そうぼやきたくなる気持ち、よく分かります。PostgreSQLを触り始めると誰もが一度は通る道、それが「トランザクション分離レベル」です。
教科書を読めば「ダーティリードがどうの」「非再現読み取りがどうの」と小難しい言葉が並んでいますが、実際の現場で大切なのは「どのレベルを選べば、自分の書いたコードが安全に、かつパフォーマンスを落とさずに動くか」という一点に尽きます。
今日は、現場で後輩に教えるつもりで、この「分離レベル」の正体を紐解いていきましょう。
—
なぜ「分離レベル」という概念が必要なのか
まず前提として、データベースは「複数のユーザーが同時に同じデータを触る」場所です。もし、全員が勝手に書き込みを始めたら、データは一瞬で壊れますよね。
そこで、トランザクションという単位で処理を隔離するわけですが、「隔離を厳しくすれば安全だけど遅くなる」「隔離を緩くすれば速いけど危ない」というトレードオフが生まれます。この「どこまで隔離するか」を決めるのが分離レベルです。
まずは、PostgreSQLでよく耳にする3つのレベルを整理しましょう。
—
1. Read Committed(デフォルトの安心感)
PostgreSQLのデフォルト設定です。「コミットされたデータだけが見える」という、直感的で一番使いやすいレベルです。
- 防げる現象: ダーティリード(コミット前の他人のゴミデータを見ない)
- 許容する現象: 非再現読み取り(同じクエリを2回打つと、途中で誰かに書き換えられて結果が変わる)
どんな時に使う?
基本はこれだけでOKです。「同じトランザクション内で、同じ行を2回参照した時に値が変わっていても困らない」ような処理なら、迷わずこれでいきましょう。
— トランザクションA
BEGIN;
SELECT balance FROM accounts WHERE id = 1; — 1000円
— (この間に他人が1000円を2000円に更新してコミット)
SELECT balance FROM accounts WHERE id = 1; — 2000円になってしまう!
COMMIT;
—
2. Repeatable Read(一貫性の守護者)
「同じトランザクション内なら、いつ何度読んでも同じ結果を返す」という約束です。
- 防げる現象: ダーティリード、非再現読み取り
- 許容する現象: 直列化異常(※ただしPostgreSQLは賢いので、もし競合があればエラーを返して止めてくれます)
どんな時に使う?
「集計処理」や「整合性が厳密に求められるレポート作成」などで重宝します。トランザクション開始時のスナップショットで固定されるので、途中で他人がデータを書き換えても、自分には影響が及びません。
注意点: 誰かが先に書き換えた行を自分が更新しようとすると、「could not serialize access」というエラーが出ます。これを食らったら、「アプリケーション側でリトライ処理を書く」のが定石です。
—
3. Serializable(最強の盾)
「複数のトランザクションを、まるで順番に1つずつ実行したかのように」振る舞わせる、最強のレベルです。
- 防げる現象: すべての不整合
- メリット: 考えることがない。絶対安全。
- デメリット: パフォーマンスが落ちるし、競合時のリトライが頻発する。
どんな時に使う?
「絶対にミスが許されない金融系の勘定処理」など、特殊な要件以外ではあまり使いません。現場で「とりあえずSerializableにしておけば安心だろ」と考えるのは、パフォーマンス地獄への入り口です。まずは上の2つで解決できないか検討するのが、プロの仕事です。
—
結局、現場ではどう判断すべき?
僕の経験則を伝えておきますね。
1. 基本は `Read Committed`: これで十分。ほとんどのWebアプリはこれで動いています。
2. 同じトランザクション内で複数回読み取り、かつ矛盾が困るなら `Repeatable Read`: ただし、エラーが起きた時のリトライロジックを必ずセットで実装すること。
3. `Serializable` は最終手段: どうしても競合を制御しきれない、複雑な整合性チェックが必要な時だけ。
「どの分離レベルにするか」という問いは、実は「その処理の整合性を、どこまでアプリケーション(コード)側でカバーできるか」という問いでもあります。
データベースを「魔法の箱」だと思わず、賢く使いこなすこと。そうすれば、深夜のデータ不整合対応に怯えることもなくなるはずです。
さて、そろそろ次のタスクに行きましょうか。何か詰まったら、いつでも聞いてくださいね。
コメント