例えば、君がオンラインゲームをしている時に、どこかのチーターがコードをいじって自分のキャラを強化したり、図書館で本を読んでいる時に誰かがページを差し替えたり、新しい章を挿入したり、そもそも本自体をすり替えたりすることを想像してみて。めっちゃ嫌な状況だよね?まさにそういう「サプライズ」から守ってくれるのがREPEATABLE READの分離レベルなんだ。
REPEATABLE READは、1つのトランザクション内で見えるデータがトランザクション終了まで変わらないことを保証してくれる。他のトランザクションがそのデータを更新しようとしても、君のトランザクションはその変更から守られるよ。
主な特徴:
Dirty Read(まだ確定していないデータの読み取り)を防ぐ。- そして一番大事なのは、
Non-Repeatable Readを防ぐこと。つまり、トランザクションの最初にデータを読んだら、もう一度読んでも同じデータが返ってくる。他のユーザーが変更しても大丈夫。
ただし、REPEATABLE READはPhantom Readは防げない。他のトランザクションが新しい行を追加した場合、再度クエリした時にその新しい行が結果に現れることがある。この異常も防ぎたいならSERIALIZABLEレベルが必要だけど、それはまた後で話すね。
REPEATABLE READの分離レベルを設定する方法
例に入る前に、PostgreSQLでこの分離レベルをどうやって有効にするか確認しよう。主に2つのやり方がある:
特定のトランザクションだけ分離レベルを設定する:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; -- 君のクエリ COMMIT;今のセッション全体に分離レベルを設定する:
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
2つ目の場合は、そのセッション内の全てのトランザクションがREPEATABLE READを使うことになるよ。
例:Non-Repeatable Readの防止
例えば、accountsというテーブルがあって、こんな構造だとしよう:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
);
INSERT INTO orders (customer_name, status)
VALUES ('Alice', 'pending'), ('Bob', 'pending');
まずは、1つのトランザクションがデータを変更し、もう1つが読み取るという基本的なシナリオから始めよう。
REPEATABLE READなし(READ COMMITTEDレベル)のシナリオ
トランザクション1が開始:
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- 取得結果: 100
その間にトランザクション2がデータを変更:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
トランザクション1が続行:
SELECT balance FROM accounts WHERE account_id = 1;
-- 取得結果: 150(データが変わった!)
COMMIT;
見ての通り、READ COMMITTEDレベルだと、1つのトランザクション内で2回読み取った時にデータが変わることがある。これがNon-Repeatable Readだよ。
REPEATABLE READありのシナリオ
今度は同じ例で、分離レベルをREPEATABLE READにしてみよう。
トランザクション1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- 取得結果: 100
トランザクション2:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
トランザクション1が続行:
SELECT balance FROM accounts WHERE account_id = 1;
-- まだ取得結果: 100(データは変わらない!)
COMMIT;
他のトランザクションが変更しても、トランザクション1は開始時点のデータしか見えない。こうしてNon-Repeatable Readが防止されるんだ。
REPEATABLE READの仕組み
PostgreSQLはMVCC(Multi-Version Concurrency Control)という仕組みでREPEATABLE READの分離レベルを実現している。MVCCの基本は、各トランザクションが「スナップショット」として安定したデータベースの状態を持ち、トランザクションが終わるまでその状態が変わらないこと。これは複数バージョンの行を作って管理することで実現されている。
トランザクションが始まると、その時点のデータだけが見える。他のトランザクションが変更を加えても、PostgreSQLは新しいバージョンの行を作るだけで、既存のバージョンはそのまま残る。だから、他のトランザクションが使っている間は古いバージョンも消えない。
この仕組みのせいで、トランザクションは遅くなったりメモリを多く使ったりする。だからこそ、最強の分離レベルを常に使う人は少ないんだ。信頼性は高いけど、パフォーマンスは落ちるからね。
REPEATABLE READの制限:Phantom Read
さっきも言ったけど、REPEATABLE READはPhantom Readを防げない。これがどういうことか、範囲クエリを使った例で見てみよう。
例えば、ordersというテーブルがあるとする:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
amount NUMERIC NOT NULL
);
INSERT INTO orders (amount)
VALUES (50), (100), (150);
トランザクション1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- 取得結果: 2
トランザクション2:
BEGIN;
INSERT INTO orders (amount) VALUES (200);
COMMIT;
トランザクション1が続行:
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- 取得結果: 3(新しい行がクエリ結果に現れた!)
COMMIT;
この場合、他のトランザクションが追加した新しい行(amount = 200)が、トランザクション1のクエリ結果に「ファントム」のように現れてしまう。これがREPEATABLE READでも防げないPhantom Readだよ。
Phantom Readも防ぎたいならSERIALIZABLEレベルを使うしかないけど、パフォーマンスとのトレードオフになる。
REPEATABLE READのメリット・デメリット
REPEATABLE READの分離レベルは、トランザクション中にデータが変わらないことを保証したい時にめっちゃ便利。1回読んだ値はCOMMITまでずっと同じ。他の誰かが変更しようとしても関係ない。
このやり方はdirty read(汚い読み取り)やnon-repeatable read(繰り返し不可な読み取り)も防いでくれる。最初に読んだデータと同じものをずっと使えるから、途中で予期しない更新が入ることはない。レポート作成や、一貫性が大事な意思決定の時に特に役立つよ。
でも、REPEATABLE READは「ファントム」(phantom read)には弱い。つまり、同じクエリを繰り返しても新しい行が結果に現れることがある。それに、負荷が高い時はトランザクション同士の競合が起きやすく、同じデータにアクセスすることが多いとロックやロールバックが増える。クエリ自体は正しくても、そういうことが起きるんだ。
まとめると、REPEATABLE READは信頼性とパフォーマンスのバランスが良いけど、競合が激しいシーンでは追加のチューニングや注意が必要になることもある。
便利なヒントとよくあるミス
- 分離レベルの選択はパフォーマンスに影響することを忘れずに。データの不変性が本当に必要な時だけ
REPEATABLE READを使おう。 REPEATABLE READとSERIALIZABLEの混同はよくあるミス。再クエリで新しい行が見えたら、それはREPEATABLE READでは普通の挙動だよ。- 長いトランザクションを使う時はロック競合に注意。長引くトランザクションは他の操作をブロックしちゃうことがある。
PostgreSQLはいろんなトランザクション分離レベルを選べる。REPEATABLE READは、1つのトランザクション内で既に読んだデータの不変性が大事な時にピッタリだよ。
GO TO FULL VERSION