はじめに
PostgreSQLのトランザクション分離レベルにおいて、デフォルトの READ COMMITTED の1つ上に位置するのが「REPEATABLE READ(リピータブルリード)」です。
PostgreSQLの REPEATABLE READ は非常に優秀で、これを設定するだけで「値が変わる(ノンリピータブルリード)」ことも「件数が増える(ファントムリード)」ことも防いでくれます。
なぜそんなに優秀なのか?そして、実務で使う上で絶対に知っておくべき「書く時の厳しいルール(同時更新エラー)」とは何なのか。Docker環境を使ったハンズオンで、その正体を完全に理解しましょう!
1. REPEATABLE READの正体は「自分専用の写真」
PostgreSQLにおける REPEATABLE READ を一言で表すなら、「トランザクション開始時にデータベース全体の『写真』を撮り、終了するまでその写真の世界だけで作業するモード」です。(専門用語でMVCC:多版同時実行制御と呼ばれます)
読むとき(Read)のルール:写真は変わらない
自分が BEGIN を宣言して写真を撮った後、他の人が現実のデータベースをどう書き換えようと、自分の手元にある「写真」は絶対に変化しません。だからこそ、途中でデータが変わることも増えることもなく、安全にデータを読み続けることができます。
書くとき(Write)のルール:写真と現実のズレは許さない
読むだけなら平和ですが、データを「更新(UPDATE)」しようとした瞬間に厳しいルールが発動します。
自分が古い写真を元にデータを上書きしようとした時、もし現実のデータがすでに他人に書き換えられていた場合、PostgreSQLは「現実と写真がズレているから上書きさせない!」と判断し、エラーを出して処理を強制終了させます。(これを「ロストアップデート」の防止と呼びます)
2. シーケンス図で見る:更新衝突(コンフリクト)の瞬間
商品A(1000円)の値段を更新する「Aさん」と「Bさん」のタイムラインです。
もしここでエラーにせず上書きを許してしまうと、Bさんが5000円に変更した事実が「1500円」で上書きされ、完全に消え去ってしまいます。これを防ぐためのエラーなのです。
3. ハンズオン準備(Docker Compose)
実際にDocker環境で「同時更新エラー」を体験してみましょう。
services:
db:
image: postgres:15
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: testdb
ports:
- "5432:5432"
コンテナを起動し、データベースに接続します。
docker compose up -d
docker compose exec db psql -U user -d testdb
テスト用の「商品テーブル」を作成します。
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(50),
price INT
);
INSERT INTO products VALUES (1, '高級キーボード', 10000);
4. 実験:同時更新エラー(シリアライズエラー)を発生させる
ターミナルを2つ(Terminal A, B)開いて実験します。
Step 1: Terminal Aでレベルを上げて確認
-- Terminal A
BEGIN;
-- ★トランザクション分離レベルを厳しくする!
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT price FROM products WHERE id = 1;
-- 結果: 10000 (★1万円の写真をゲット)
Step 2: Terminal Bが(裏で)価格を更新して確定
Terminal Aが考えている間に、Terminal Bが価格を「15000円」に値上げしてしまいます。
-- Terminal B
BEGIN;
UPDATE products SET price = 15000 WHERE id = 1;
COMMIT;
-- 現実のデータベースは15000円になりました
Step 3: Terminal Aが古い写真を元に更新を試みる
TerminalBで更新後、Terminal Aでもう一度状態を確認します。
testdb=*# SELECT price FROM products WHERE id = 1;
price
-------
10000
(1 row)
別で更新がされていても、最初の状態の10000が保たれています。
繰り返し読み返せるから、REPEATABLE READになっています。
次にTerminal Aはまだ手元の写真(10000円)を見ているつもりで、そこから「+2000円」の更新をかけようとします。
-- Terminal A
UPDATE products SET price = price + 2000 WHERE id = 1;
Enterを押した瞬間、以下の強烈なエラーが発生します!
ERROR: could not serialize access due to concurrent update
(翻訳:同時更新が行われたため、アクセスの直列化に失敗しました)
この瞬間、Terminal Aのトランザクションは「ABORT(失敗)」状態となり、これ以上何もできなくなります(ROLLBACKするしかなくなります)。
5. まとめ
-
REPEATABLE READは、開始時に自分専用の写真(スナップショット)を撮るモード。 - 読むときは、他人の更新に邪魔されず、最初から最後まで一貫したデータを読める。
- 書くときは、現実のデータが他人に書き換えられていた場合、ロストアップデートを防ぐために必ずエラーになる。
- エラーが起きることを前提に、アプリケーション側でリトライ処理を実装するのがプロの設計。
単にデータを安全に読みたい時や、「現在の値を読み込んで、複雑な計算をしてから更新する」ような重要処理の際に、ぜひ REPEATABLE READ を活用してみてください!



