もう一度トランザクションのタイプをおさらいしよう!データベースのトランザクションの世界には、みんなの一日を台無しにする三大「悪役」がいるんだ:Dirty Read、Non-Repeatable Read、Phantom Read。これらの「異常」はトランザクションのIsolation Levelが足りないときに発生するよ。今日はこの悪役たちが何者なのか、どうやって現れるのか、そして—一番大事なこと—どうやって戦うかを見ていこう!
例に入る前に、それぞれが何を意味するのか思い出してみよう。
Dirty Read(ダーティリード/汚れた読み取り):
まだコミット(COMMIT)されていない、もしくはロールバック(ROLLBACK)されるかもしれないトランザクションによって変更されたデータを読んじゃうこと。たとえば、友達にお金を送って残高を見たらゼロになってて焦るけど、やっぱやめたってお金を戻したみたいな感じ。まさにマジック!Non-Repeatable Read(ノンリピータブルリード/繰り返し読めない読み取り):
同じトランザクションの中で同じデータを2回読んだのに、その間に他のトランザクションがデータを変えちゃって、2回目は違う値が見えちゃうこと。パスポートの誕生日を見て、友達に渡したら数字をこっそり変えられて、もう一度見たら誕生日が変わってた、みたいな。Phantom Read(ファントムリード/幻の読み取り):
同じクエリを2回実行したのに、2回目には他のトランザクションが追加した新しい行が見えちゃうこと。部屋の人数を数えてたら、誰かがこっそり友達を連れてきて、もう一度数えたら人数が増えてた、みたいな感じ。
Dirty Readの問題
例えば、accountsテーブルがあるとしよう:
CREATE TABLE accounts (
account_id SERIAL PRIMARY KEY,
owner TEXT NOT NULL,
balance NUMERIC(10, 2) NOT NULL
);
INSERT INTO accounts (owner, balance) VALUES ('Alice', 1000), ('Bob', 500);
トランザクション1が残高を変更したけど、まだ終わってなくて、同時にトランザクション2がそのデータを読もうとしてる。
トランザクション1:
BEGIN;
UPDATE accounts SET balance = balance - 200 WHERE owner = 'Alice';
-- Aliceの残高は800になったけど、まだトランザクションは終わってない。
トランザクション2:
BEGIN;
SELECT balance FROM accounts WHERE owner = 'Alice'; -- 残高が800に見える(ダーティリード)。
ROLLBACK; -- トランザクション1がロールバックされる。
これでトランザクション2は正しくないデータを使っちゃうことになる。なぜならトランザクション1が変更を取り消したから。これを防ぐにはREAD COMMITTEDレベルのIsolationを使えば、未コミットの変更は見えなくなるよ。
Non-Repeatable Readの問題
例えば、トランザクション1がデータを読んで、別のトランザクションがそのデータを変更し、トランザクション1がもう一度読むと値が変わってる場合。
トランザクション1:
BEGIN;
SELECT balance FROM accounts WHERE owner = 'Bob'; -- 残高は500に見える。
トランザクション2(同時進行):
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Bob';
COMMIT;
トランザクション1再び:
SELECT balance FROM accounts WHERE owner = 'Bob'; -- 残高は400に見える。
COMMIT;
同じトランザクション内でデータが変わっちゃってるのがわかるよね。実際のシナリオだと、例えば財務レポートとかでこれは致命的。これを防ぐにはREPEATABLE READみたいなもっと高いIsolation Levelを使おう。
Phantom Readの問題
例えば、ordersテーブルがあるとしよう:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer TEXT NOT NULL,
total NUMERIC(10, 2) NOT NULL
);
INSERT INTO orders (customer, total) VALUES ('Alice', 100), ('Bob', 200);
トランザクション1が注文数を数えて、別のトランザクションが新しい注文を追加し、トランザクション1がもう一度数えると…
トランザクション1:
BEGIN;
SELECT COUNT(*) FROM orders; -- 2が見える。
トランザクション2(同時進行):
BEGIN;
INSERT INTO orders (customer, total) VALUES ('Charlie', 300);
COMMIT;
トランザクション1再び:
SELECT COUNT(*) FROM orders; -- 3が見える。新しい「ファントム」注文が現れた!
COMMIT;
ファントムリードを防ぐには、SERIALIZABLEレベルのIsolationが必要。これなら並行して結果に影響する変更は完全にブロックされるよ。
異常を防ぐ方法
Isolation Level vs. 異常
| Isolation Level | Dirty Read |
Non-Repeatable Read |
Phantom Read |
|---|---|---|---|
Read Uncommitted |
❌ あり | ❌ あり | ❌ あり |
Read Committed |
✅ なし | ❌ あり | ❌ あり |
Repeatable Read |
✅ なし | ✅ なし | ❌ あり |
Serializable |
✅ なし | ✅ なし | ✅ なし |
どのIsolation Levelを選べばいい?
- データへの高速アクセスが必要で、異常がそこまで重要じゃない(例えば古いデータでの分析とか)なら
READ COMMITTEDを使おう。 - 1つのトランザクション内でデータの安定性が大事なら
REPEATABLE READを使おう。 - 最大限のIsolationと一貫性が必要なら
SERIALIZABLEを選ぼう。ただしパフォーマンスが落ちることもあるから注意!
実践的なアドバイス
自分のケースに合ったIsolation Levelでトランザクションを使おう。例えば:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT ...;
COMMIT;
インデックスを追加してテーブルロックを減らし、クエリの実行を速くしよう。
特にSERIALIZABLEレベルでは、長いロックを避けるためにクエリを最適化しよう。
よくあるエラーとその回避法
Isolation Levelの選択ミスでコンフリクトやパフォーマンス低下が起きることもある。例えば、SERIALIZABLEを大量の並行トランザクションがあるシステムで使うと、ロックや「トランザクションの飢餓」状態になることも。
これを避けるには、クエリを分析して、いろんなIsolation Levelでパフォーマンステストをして、適切なインデックスを使おう。
GO TO FULL VERSION