株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
このシリーズについて
「知ってはいる。けど、人に説明しろと言われると詰まる」——そんな歯がゆいDB用語を、1記事1用語・図解中心で解消していくシリーズです。
第1回で、ACIDの I(独立性) は「あるかないか」ではなく 「どこまで効かせるかを選ぶダイヤル」 だと書きました。今回はそのダイヤルの中身です。
まず自己診断
READ COMMITTED という名前は読めます。「コミット済みのものが読める」——それはそう。では:
同じトランザクションの中で同じSELECTを2回実行したら、同じ結果になりますか?
READ COMMITTED で、答えは NO です。ここが即答できなかったら、この記事の対象読者です。
結論:この1枚を覚えれば終わり
分離レベルは 「4つのレベル × 3つの困った現象」のマップ です。これだけです。
| 分離レベル | ダーティリード | ノンリピータブルリード | ファントムリード |
|---|---|---|---|
| READ UNCOMMITTED | 起きる | 起きる | 起きる |
| READ COMMITTED | 防ぐ | 起きる | 起きる |
| REPEATABLE READ | 防ぐ | 防ぐ | 起きる※ |
| SERIALIZABLE | 防ぐ | 防ぐ | 防ぐ |
※これは標準SQLの定義での話。実際のPostgreSQL / MySQLでは REPEATABLE READ でもファントムリードが起きません(後述。ここが最大の歯がゆさポイント)。
レベルが上がるほど安全になり、そのぶん同時実行性能が落ちます。
3つの現象を図で押さえる
1. ダーティリード:まだCOMMITしてない値が見える
一番わかりやすい事故です。ロールバックされたら、その値はこの世に一度も存在しなかったことになります。
2. ノンリピータブルリード:同じ行を2回読むと値が違う
「合計を出してから明細を出したら、金額が合わない」系の不具合はこれが原因になりがちです。
3. ファントムリード:件数が変わる
2との違いは 「既存の行の値が変わる」か「該当する行の数が変わる」か。ここが区別できると、説明が一段しっかりします。
実測:PostgreSQL 16 で確かめる
口で説明するより速いので、実際に動かします。PostgreSQL 16.14(Docker postgres:16) で、2つのセッションを並行させた実測結果です。
-- 準備
CREATE TABLE members (id INT PRIMARY KEY, name TEXT, points INT);
INSERT INTO members VALUES (1, 'sato', 100), (2, 'suzuki', 100);
セッション1が読んでいる最中に、セッション2が「更新」と「追加」をしてCOMMITします。
-- セッション2(途中で割り込む側)
BEGIN;
UPDATE members SET points = 500 WHERE id = 1;
INSERT INTO members VALUES (3, 'takahashi', 100);
COMMIT;
セッション1は、その前後で同じSELECTを2回実行します。
-- セッション1
BEGIN ISOLATION LEVEL READ COMMITTED; -- ここを付け替えて比較する
SELECT points FROM members WHERE id = 1;
SELECT count(*) FROM members WHERE points >= 100;
-- ここでセッション2がCOMMIT
SELECT points FROM members WHERE id = 1; -- 同じSQL
SELECT count(*) FROM members WHERE points >= 100; -- 同じSQL
COMMIT;
READ COMMITTED の結果:
T1-1回目 sato:100
T1-1回目 100pt以上の人数:2
T1-2回目 sato:500 ← 値が変わった(ノンリピータブルリード)
T1-2回目 100pt以上の人数:3 ← 件数が変わった(ファントムリード)
REPEATABLE READ の結果(BEGIN ISOLATION LEVEL REPEATABLE READ; に変えただけ):
T1-1回目 sato:100
T1-1回目 100pt以上の人数:2
T1-2回目 sato:100 ← 変わらない
T1-2回目 100pt以上の人数:2 ← 件数も変わらない
同じSQL・同じ割り込み。違うのは1行目だけです。READ COMMITTED は「1文ごと」に最新のコミット済みデータを見に行き、REPEATABLE READ は「最初の読み取り時点のスナップショット」を最後まで見続ける——この差が全部です。
ここは
BEGINした瞬間ではない点に注意。スナップショットが確定するのは PostgreSQL ではトランザクション内の最初の文、MySQL/InnoDB では**最初の一貫性読み取り(最初のSELECT)**の時点です。実際、BEGIN直後に何も読まずに待っていると、その待ち時間中に他トランザクションがコミットした値が見えます(PostgreSQL 16 で確認)。上の実測はT1が先に読んでいるので結果は変わりません。
なお MySQL 8.0.46 でも同じ手順を実行し、まったく同じ結果になることを確認しました。
歯がゆさの正体:エンジンごとに話が違う
ここからが本題です。「表を覚えたのに実務で話が噛み合わない」のは、エンジンが標準どおりに実装していないからです。
差1:PostgreSQLに READ UNCOMMITTED は実質存在しない
PostgreSQLで READ UNCOMMITTED を指定しても、ダーティリードは起きません。実測してみます。セッション2が未コミットのまま 999 に更新している最中に、セッション1が読みます。
READ UNCOMMITTED指定時の実際のレベル: read uncommitted
T1が読んだ値: 100 ← 999は見えない
面白いのは、設定値としては read uncommitted を受け付けて、そう report してくるのに、振る舞いは READ COMMITTED という点です。公式ドキュメントにも「READ UNCOMMITTED を要求しても READ COMMITTED として扱われる」と明記されています。
同じことを MySQL 8.0 でやると:
T1が読んだ値: 999 ← 未コミットの値が見えた(その後ROLLBACKされ、消滅)
同じSQLで結果が違う。 「ダーティリードって実際どうなるの?」の答えがエンジンで割れるので、会話が噛み合わなくなるわけです。
差2:デフォルトが違う
BEGIN としか書かなかったとき、どのレベルで動くか。これも割れます。
| エンジン | デフォルト | 根拠 |
|---|---|---|
| PostgreSQL 16 | READ COMMITTED | 実測(SHOW default_transaction_isolation) |
| MySQL 8.0 (InnoDB) | REPEATABLE READ | 実測(SELECT @@GLOBAL.transaction_isolation) |
| Oracle Database | READ COMMITTED | 公式ドキュメント(未実測) |
MySQLだけ1段階強いというのは、押さえておくと事故が減ります。
差3:そもそも4つない(Oracle)
Oracle Database が提供するのは READ COMMITTED(デフォルト)と SERIALIZABLE の2つだけで、READ UNCOMMITTED と REPEATABLE READ はサポートされていません。公式ドキュメントも「Oracle はダーティリードを決して許可しない」と明言しています。
差4:REPEATABLE READ でファントムが起きない
冒頭の表に付けた「※」の回収です。標準SQLでは REPEATABLE READ でファントムリードが「起きうる」とされていますが、先ほどの実測どおり PostgreSQL でも MySQL でも起きませんでした。両者ともスナップショットを使う実装で、標準が要求するより強い保証になっているためです。
つまり冒頭の表は「標準の定義」であって「目の前のDBの挙動」ではありません。表は地図、実測は現地。ここを混同していると、いつまでも歯がゆいままです。
なお本記事の実測はすべてロックを取らない通常の
SELECTでの結果です。SELECT ... FOR UPDATEのようにロックを取る読み取りは挙動が変わります。
まとめ:1行で説明するなら
「分離レベルは、同時に走るトランザクションの干渉をどこまで許すかのダイヤルです。READ COMMITTED は『他人のコミット済みの変更が、自分のトランザクションの途中でも見える』レベル。だから同じSELECTを2回打つと結果が変わりえます。それが困るなら REPEATABLE READ にします。ただし標準の定義と実際のエンジンの挙動はズレるので、最後は使うDBで確かめます」
READ COMMITTED で「結局なにが読めるのか」への答えは、「実行したその瞬間にコミット済みだったもの。だから読むたびに変わる」 です。
次回
第3回:ロックとデッドロック ——「誰が何をどこまでロックしているのか」を図解します。
(シリーズ全11回の予定は第1回に掲載しています)
参考
- PostgreSQL 16 Documentation - Transaction Isolation
- PostgreSQL 16 Documentation - SET TRANSACTION
- MySQL 8.0 Reference Manual - Transaction Isolation Levels
- MySQL 8.0 Reference Manual - SET TRANSACTION
- Oracle Database Concepts - Data Concurrency and Consistency
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!