例えば、君がカフェで働いてるとしよう。一人のウェイターがキッチンのケーキの在庫をチェックしてて、もう一人が新しいお客さんのケーキ注文を入力してる。理想の世界なら、二人とも同じケーキの数を見てるはずで、「ダブルブッキング」みたいなミスは起きない。でも現実だと、並行して操作すると色々トラブルが起きるんだよね。
主に起こりうるイヤなことはこの3つ:
Dirty Read(ダーティリード/汚れた読み取り): あるクエリが、他のクエリによるまだコミットされてない変更を見ちゃうこと。もしその変更がロールバックされたら、最初のクエリは新卒エンジニアの初面接みたいにピュアなまま騙される。
Non-Repeatable Read(リピートできない読み取り): 同じデータを2回読んだのに、その間に誰かがデータを変えちゃうパターン。駅で時刻表見て、1分後に戻ったら電車がキャンセルされてた、みたいな感じ。あるいは、君が財布探してる間にチケット買われちゃったとかね :)
Phantom Read(ファントムリード/幻の読み取り): あるクエリが一部の行を見て、2回目の実行の間に誰かが新しい行を追加して結果が変わっちゃうこと。例えば、君の会社が入札で負けたのに、他の全ての申請(と市長の奥さんの申請)だけがキャンセルされた、みたいな。
トランザクション分離レベル
問題が分かったところで、PostgreSQLが用意してる解決ツール、つまりトランザクション分離レベルを見てみよう。これは並行トランザクション同士のルールを決めるもの。分離レベルが高いほど、トランザクション同士が邪魔し合わない保証が強くなる。でもその分「サービスの速さ」、つまりパフォーマンスが落ちるのがトレードオフ。
PostgreSQLの分離レベル
Read Uncommitted(未コミットデータの読み取り):
- まだコミットされてない変更も読めちゃう(そう、これがダーティリードの真骨頂)。
- PostgreSQLでは実際には
Read Committedとして実装されてて、このレベルは純粋にはサポートされてない。PostgreSQLはこのやり方が危なすぎるから実装しないんだ。
Read Committed(コミット済みデータの読み取り):
- ダーティリードを防ぐ。
- トランザクションは、そのコマンド実行時点でコミット済みのデータだけ見える。
- でも
Non-Repeatable ReadやPhantom Readは起こりうる。
Repeatable Read(リピート可能な読み取り):
- 読んだデータはトランザクション中ずっと変わらないことを保証。
- ダーティリードとリピートできない読み取りを防ぐ。
- でもファントム行はまだ起こる可能性あり。
Serializable(シリアライズ可能):
- トランザクションがまるで一つずつ順番に実行されたかのように振る舞うことを保証。
- ダーティリード、リピートできない読み取り、ファントム行、全部防ぐ。
- 一番厳しい、そして一番遅い分離レベル。
なぜトランザクション分離が大事なの?
例えば、ネットショップのDBで、1000人のユーザーが同時に注文しようとしてるとしよう。分離レベルがちゃんと設定されてないと、「消えた商品」や「重複注文」みたいなトラブルが山ほど起きる。
適切な分離レベルを選ぶことで、トランザクションの競合をうまくさばいて、パフォーマンスとデータの整合性のバランスを取れる。例えば:
- 分析系システムなら、データの正確さがそこまで重要じゃないから、最低限の分離レベル(例:Read Committed)を選ぶことが多い。
- 金融系システムなら、計算ミスや重複処理を避けるためにSerializableレベルが好まれる。
分離レベルの使い方例
- Read Committed
このレベルなら、ロールバックされるかもしれないデータを読むことは絶対にない。
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
-- アカウントのデータを読む。
SELECT balance FROM accounts WHERE account_id = 1;
-- 他のトランザクションが残高を更新したら、このデータもすぐ更新される。
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
COMMIT;
- Repeatable Read
このレベルなら、一度読んだデータはトランザクション中ずっと変わらない。
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- アカウントのデータを読む。
SELECT balance FROM accounts WHERE account_id = 1;
-- 他のトランザクションがこの残高を更新しても、君には見えない。
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
COMMIT;
- Serializable
このレベルだと、まるで自分のトランザクションだけがシステムで動いてるみたいな感じになる。
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- アカウントのデータを読む。
SELECT balance FROM accounts WHERE account_id = 1;
-- 他のトランザクションがこのデータを変更しようとしたら、君のトランザクションが終わるまでブロックされる。
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
COMMIT;
分離レベルの選び方は?
どの分離レベルを選ぶかは、君の要件次第:
- スピード重視で、ファントム行が出てもOKなら
Read Committedを選ぼう。 - 正確さも欲しいけど、パフォーマンスも捨てたくないなら
Repeatable Readを使おう。 - 100%データの正しさが必要で、スピードは二の次なら
Serializableがピッタリ。
注意!厳しい分離レベルはロックやパフォーマンス低下を招くこともある。パフォーマンスとデータ整合性のバランスを取るのが賢い選択だよ。
GO TO FULL VERSION