分離レベルとアノマリー対応表
どの分離レベルがどの異常を防ぐかを、規格の定義と実装の差まで一枚の地図にできます。Write Skew のように規格表に載らない罠まで原理から見抜けます。
- 1.SQL標準は Dirty Read・Non-Repeatable Read・Phantom の3異常を防ぐかどうかで4レベルを定義するが、表の境界は規格の最低保証であって実装の実力ではない。
- 2.MVCC のスナップショット分離は3異常を全て防ぐが Write Skew を防げず、規格の4レベルとは別軸の保証である点が落とし穴。
- 3.Write Skew は読んだ集合を更新せず別の行を書くため衝突が見えず、SERIALIZABLE か明示的な行ロックでしか塞げない。
対応表を「原理」で読む
分離レベルの一覧表(→ トランザクション分離レベル)は暗記対象になりがちですが、表の各マスがなぜ「防ぐ/起きうる」になるのかは、異常の定義と各レベルがいつ・何を見るかから決まります。ここではマスの根拠を分解し、規格表に載らない Write Skew まで含めた原理レベルの対応関係を整理します。
要点を先に置きます。第一に、SQL標準の表は3つの異常(Dirty Read・Non-Repeatable Read・Phantom)だけを基準にした最低保証であり、実装の実力ではありません。第二に、MVCC のスナップショット分離はこの3異常を全て防ぐのに Write Skew を防げず、規格の4レベルとは別軸にあります。
アノマリーの定義(境界条件)
各異常は「2回のアクセスの間に、他のトランザクションの何が割り込むか」で区別されます。定義が曖昧だと表も読めないので、まず境界を固定します。
| 異常 | 成立条件 | 見分け方 |
|---|---|---|
| Dirty Read | 他がまだコミットしていない変更を読む | 読んだ値が後でロールバックされ消えうる |
| Non-Repeatable Read | 同じ1行を2回読む間に他のコミット済み更新が挟まる | 同じ主キーの値がブレる |
| Phantom | 同じ条件で2回検索する間に他が行を挿入/削除 | ヒットする行数が変わる |
| Write Skew | 各々が読んだ集合を検証し、互いに別の行を書く | 個別には正しいが合算で不変条件が壊れる |
Non-Repeatable Read と Phantom の違いは「既存の1行の値が変わる」か「条件にヒットする行の集合が変わる」かです。前者は行ロックや行単位のバージョンで塞げますが、後者は「まだ存在しない行」が相手なので、範囲を覆う仕組み(次節)が要ります。
なぜそのマスになるか:レベル別の根拠
SQL標準の4レベルが各マスで「防ぐ/起きうる」になる理由は、そのレベルがコミット済みしか読まないか、読み取りの一貫点をいつ固定するか、範囲をロックするかの3点で説明できます。
- READ UNCOMMITTED: 未コミットの版も読めるため Dirty Read が起きうる。最も緩い。
- READ COMMITTED: 各文の実行時点で確定済みだけを読むので Dirty Read は消える。だが一貫点が文ごとに更新されるため、再読すると別の確定版を見て Non-Repeatable Read が残る。
- REPEATABLE READ: 一貫点をトランザクション開始時に固定するので、既存行の再読はブレず Non-Repeatable Read まで防ぐ。規格上は範囲を保護しないので Phantom は残りうる。
- SERIALIZABLE: 結果が何らかの直列実行と等価になることを保証し、Phantom を含む全異常を防ぐ。
READ COMMITTED : 一貫点 = 文ごとに取り直す -> 再読で値が動く
REPEATABLE READ : 一貫点 = Tx開始時に固定 -> 既存行の再読は安定
SERIALIZABLE : 直列化可能を保証 -> 範囲・順序まで含めて安全
つまり「いつスナップショット(一貫点)を取り直すか」がそのまま Dirty/Non-Repeatable の境界を決め、「範囲まで守るか」が Phantom の境界を決めます(→ MVCC の内部実装)。
規格表(最低保証)
ここまでの根拠を SQL標準の最低保証としてまとめると、おなじみの表になります。重要なのは、これが「ここまでは必ず防ぐ」という下限だという点です。
| 分離レベル | Dirty Read | Non-Repeatable | Phantom |
|---|---|---|---|
| READ UNCOMMITTED | 起きうる | 起きうる | 起きうる |
| READ COMMITTED | 防ぐ | 起きうる | 起きうる |
| REPEATABLE READ | 防ぐ | 防ぐ | 起きうる(規格上) |
| SERIALIZABLE | 防ぐ | 防ぐ | 防ぐ |
この表は「最低限ここまでは防ぐ」という線引きで、実際の製品はもっと強いことが多いです。たとえば PostgreSQL の REPEATABLE READ はスナップショット分離で実装され、規格では残りうる Phantom まで実質的に防ぎます。一方で MySQL InnoDB の REPEATABLE READ は、純粋な読み取りと現在読み取り(ロック読み取り)で見える版が違うなど、製品固有の挙動があります。自分が使う製品が実際に何を保証するかは必ずドキュメントで確認してください。
別軸の罠:スナップショット分離と Write Skew
ここからが上級の本題です。多くの MVCC 実装の REPEATABLE READ はスナップショット分離 (SI) で、トランザクション開始時の一枚のスナップショットを読み続けます。SI は規格の3異常を全て防ぎますが、規格の4レベルとは別系統の保証で、Write Skew という異常を防げません。
Write Skew は「各トランザクションが同じ集合を読み、その読みに基づいて互いに異なる行を書く」ときに起きます。書く対象が重ならないので書き込み衝突として検知されず、しかし合算すると不変条件が壊れます。
不変条件: オンコール医は常に1名以上いること(現在2名: AliceとBob)
Tx1: SELECT count(*) FROM doctors WHERE on_call; -- 2 を読む -> 1名残る判断
UPDATE doctors SET on_call=false WHERE name='Alice';
Tx2: SELECT count(*) FROM doctors WHERE on_call; -- 2 を読む -> 1名残る判断
UPDATE doctors SET on_call=false WHERE name='Bob';
両者ともスナップショットでは「自分が降りても相手が残る」と見える。
書く行は別(Alice と Bob)なので衝突せず両方コミット成功。
結果: オンコール 0 名 -> 不変条件が破れる。
各トランザクションは単独では完全に正しく、Dirty/Non-Repeatable/Phantom のどれにも該当しません。それでも整合性が壊れるのは、読んだ前提(相手が残る)が、相手の書き込みで覆されたのに検知されないからです。SI が「読み取りスナップショットの一貫性」しか保証せず、「読みと書きの間の論理的依存」を見ていないことが根因です。
SQL標準の異常はロックベースの実装を念頭に定義され、Write Skew は SI が普及してから整理された概念です。そのため規格の4×3表には現れません。「REPEATABLE READ(=多くの実装で SI)は Write Skew を防げない」「SERIALIZABLE で初めて防げる」をセットで覚えるのが要点です。
拡張対応表:Write Skew を加える
規格の3異常に Write Skew を足し、代表的な実装の実際の挙動を並べると、対応関係が立体的になります。
| 実装レベル | Phantom | Write Skew | 防ぐ仕組み |
|---|---|---|---|
| READ COMMITTED | 起きうる | 起きうる | 文ごとのスナップショット |
| スナップショット分離(SI) | 実質防ぐ | 起きうる | Tx開始時の一枚のスナップショット |
| SERIALIZABLE (SSI) | 防ぐ | 防ぐ | 読み書き依存を追跡し危険な交差で中断 |
| SERIALIZABLE (2PL) | 防ぐ | 防ぐ | 範囲ロック等で読みの前提を物理的に保護 |
PostgreSQL の SERIALIZABLE は SSI(直列化可能スナップショット分離) で、SI に「読み書き依存の追跡」を足し、直列化を壊しうる危険な依存の交差を検出したトランザクションを中断します。ロックを増やさず楽観的に検知する方式のため、競合時はシリアライズ失敗が返り、アプリ側はリトライ前提で書く必要があります(→ ロックと MVCC)。
Write Skew を SERIALIZABLE 無しで塞ぐ
SERIALIZABLE の中断・再試行コストを避けたい場合、読んだ前提を明示的にロックして「読みと書きの依存」を物理化できます。
-- 読んだ集合に書き込み意図のロックを掛け、相手を待たせる
SELECT count(*) FROM doctors
WHERE on_call FOR UPDATE; -- 読んだ行を更新ロック
-- ここで相手はブロックされ、片方ずつ直列化される
UPDATE doctors SET on_call=false WHERE name='Alice';
SELECT ... FOR UPDATE は読んだ既存行を更新ロックし、相手の更新を待たせます。ただし「まだ存在しない行」が前提に絡む場合(挿入による Write Skew)は、行ロックでは覆えず、範囲を覆う仕組みか一意制約などの宣言的制約が必要です。どこまでがロックで塞げ、どこからが SERIALIZABLE 必須かは、前提が「既存行」か「行の不在」かで分かれます。
まとめ
- 規格の対応表は Dirty/Non-Repeatable/Phantom の3異常に対する最低保証で、境界は「一貫点をいつ取るか」「範囲を守るか」で決まる。
- 実装は規格より強いことが多く、多くの MVCC の REPEATABLE READ は SI として Phantom まで実質防ぐ。
- SI は規格の3異常を防ぐが Write Skew を防げない。読んだ前提が相手の別行への書き込みで覆されても検知されないため。
- Write Skew を塞ぐのは SERIALIZABLE(PostgreSQL は SSI で楽観検知、リトライ前提)か、
SELECT ... FOR UPDATE等での明示ロック。ただし行の不在が前提なら範囲保護が要る。
分離レベルの正しさは ACID の I(独立性)の度合いそのものです。表のマスを暗記するのではなく、各異常の成立条件と実装が見るスナップショットの粒度から、対応関係を都度導けるようにしておくのが上級者の読み方です。
データベース Article
分離レベルとアノマリー対応表を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
分離レベル
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
MVCC のスナップショット分離は3異常を全て防ぐが Write Skew を防げず、規格の4レベルとは別軸の保証である点が落とし穴。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「分離レベル / アノマリー」に近いか確認する。
- 強みである「SQL標準は Dirty Read・Non-Repeatable Read・Phantom の3異常を防ぐかどうかで4レベルを定義するが、表の境界は規格の最低保証であって実装の実力ではない。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。