トランザクションの分離レベルは ANSI SQL で4段階(READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE)に定義されていますが、実際の DBMS の実装はこの規格通りには動いていません。本記事では分離レベルがどう実装されているかと、規格で捉えきれない異常現象(読み取りスキュー・更新ロスト・書き込みスキュー)への対処を整理します。なお、ACID 全体や ANSI SQL 4段階・3つの古典的異常(Dirty Read / Non-repeatable Read / Phantom Read)の概観は『ACIDトランザクションの4特性』で扱っているので、必要に応じて参照してください。
本記事のフォーカスは次の4点です。
- ANSI SQL の規格名と実装上の挙動の乖離(命名混乱)
- 実装ベースで見たときの「実用上の3段階」: Read Committed → スナップショット分離 → 直列化可能性
- 各段階の実装機構(行ロック / MVCC)と防げる異常・防げない異常
- 直列化可能性の3つの実装方式(順次実行 / 2PL / SSI)
ANSI SQL 4段階の限界と「実用上の3段階」
ANSI SQL は分離レベルを次の4段階で定義しています(弱い順)。
READ UNCOMMITTED(最弱): ダーティリード(他のトランザクションがまだコミットしていない値を読んでしまう異常)すら防がないREAD COMMITTED: ダーティリードを防ぐREPEATABLE READ: 同じ行を2回読んでも同じ値であることを保証SERIALIZABLE(最強): 完全な直列化を保証
しかし実装はこの枠に綺麗に収まっていません。また最弱の READ UNCOMMITTED は実用でほぼ使われないため、本記事では READ UNCOMMITTED を除き、実装ベースで以下の 「実用上の3段階」に再整理します。
- Read Committed: ダーティリード / ダーティライト(他のトランザクションがまだコミットしていない値を上書きしてしまう異常)を防ぐ最低ライン
- スナップショット分離: 上記に加えて読み取りスキューを防ぐ
- 直列化可能性: あらゆる並行性異常を防ぐ最強段階
番号が進むほど保証が強くなる階層で、Read Committed が最弱、直列化可能性が最強です。各段階は前段の保証を包含します(スナップショット分離は Read Committed の保証も提供する、など)。
ANSI SQL の規格名と DBMS の実態のズレ
分離レベルはトランザクションごとに指定できる
そもそも分離レベルは「DB ごとに固定で決まっている」ものではなく、アプリ側からトランザクション単位で指定可能です。
-- PostgreSQL の例
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- ... クエリ ...
COMMIT;
各 DBMS にはデフォルトの分離レベルがあり(PostgreSQL は READ COMMITTED、MySQL InnoDB は REPEATABLE READ など)、明示指定しなければデフォルトが使われます。同じアプリ内でも、トランザクションごとに違うレベルを指定して構いません。たとえば「日次バッチの集計だけ SERIALIZABLE にする」「通常の API 処理はデフォルトで動かす」といった使い分けが可能です。
同じキーワードでも DBMS によって挙動が違う
問題は、同じ ANSI SQL キーワードを指定しても DBMS によって実際の挙動が違うことです。下の表は「キーワードを指定したとき、その DBMS が実際にどの強さの保証を提供するか」をまとめたものです。
表の読み方は以下のとおりです。
- セルが規格名と同じ名前ならば、規格通りに動く(実装が規格と一致)
- セルが違う名前ならば、規格通りではなく、
規格より強い/弱いの注記でズレの方向を示す サポートなしの場合、そのキーワードを指定できない(エラーまたは別レベルにフォールバック)
| ANSI SQL の規格名 | PostgreSQL の実態 | MySQL InnoDB の実態 | Oracle の実態 |
|---|---|---|---|
| READ UNCOMMITTED | Read Committed(規格より強い) | Read Uncommitted | サポートなし |
| READ COMMITTED | Read Committed | Read Committed | Read Committed |
| REPEATABLE READ | スナップショット分離(規格より強い) | スナップショット分離(規格より強い) | サポートなし |
| SERIALIZABLE | 直列化可能性(SSI 実装) | 直列化可能性(2PL 実装) | スナップショット分離(規格より弱い) |
特に注意すべきは以下の点です。
- PostgreSQL や MySQL InnoDB の
REPEATABLE READは、ANSI SQL が想定する純粋な「同じ行を2回読んでも同じ値」よりも強いスナップショット分離を提供している - Oracle の
SERIALIZABLEは実態がスナップショット分離であり、規格が要求する直列化可能性より弱い(書き込みスキューを検出しない) - 同じ
SERIALIZABLEでも PostgreSQL は SSI、MySQL InnoDB は 2PL と内部実装が異なる(実装方式の違いは後述)
つまり、規格名で議論すると実装の挙動が見えなくなります。本記事では以降、実装ベースの呼び名(Read Committed / スナップショット分離 / 直列化可能性)で進めます。
Read Committed
Read Committed は最も広く使われる分離レベルで、PostgreSQL・Oracle・SQL Server のデフォルトです。先述の通りダーティリードとダーティライトを防ぐのがこのレベルの役割です。
ダーティリードは『ACIDトランザクションの4特性』で扱った ANSI SQL の3異常(Dirty Read / Non-repeatable Read / Phantom Read)のうちの1つですが、ダーティライトは ANSI SQL の3異常リストには含まれていません。Read Committed が防ぐ重要な異常なので、具体例で見ます。
ダーティライトの例
時刻 T1 T2
--- --------------------------------- ----------------------------------
1 BEGIN;
2 UPDATE accounts SET balance=500
WHERE id=1;
(T1 の中では balance=500、未コミット)
3 BEGIN;
UPDATE accounts SET balance=999
WHERE id=1;
(T2 で T1 の未確定値を上書きしようとする)
ダーティライトを許すと、T1 が ROLLBACK したときに「T1 の更新だけを巻き戻し、T2 の更新は残す」といった整合性の取れた巻き戻しができなくなります。Read Committed はこれを防ぎます。
Read Committed の実装
実装は概ね次の通りです。
- 書き込み
- 行レベルの排他ロックを取得し、トランザクション終了まで保持
- これでダーティライトを防ぐ
- 読み取り
- ロックを取らない
- 直前にコミットされた値を返す
読み取り側でロックを取らないことが重要で、長時間の書き込みトランザクションがあっても読み取りは待たされません。多くの DBMS はこれを MVCC という仕組みで実現しています。MVCC は次のスナップショット分離でも使われる共通基盤なので、ここで詳しく見ておきます。
MVCC とは — Read Committed と スナップショット分離 の共通基盤
MVCC(Multi-Version Concurrency Control、多版型並行性制御) は、各行について複数のバージョンを同時に保持することで、読み取りと書き込みの競合を回避する並行性制御の方式です。Read Committed もスナップショット分離もこの上に成り立っています。
素朴なロックベースの問題
並行性制御の単純な実装は「行ごとに共有/排他ロックを取る」方式です。
- 読み取り: 共有ロックを取る → 他の読み取りはOK、書き込みはブロック
- 書き込み: 排他ロックを取る → 他の読み書きをすべてブロック
この方式の最大の問題は 「長時間の読み取りが書き込みをブロックし、長時間の書き込みが読み取りをブロックする」ことです。たとえば集計のために大量行を読むトランザクションが走ると、その間 OLTP の書き込みが軒並み待たされます。
MVCC の発想: 古いバージョンを残しておく
MVCC は「行を更新するときに既存の値を上書きせず、新バージョンを別途追記する」方式です。古いバージョンも残し、進行中のトランザクションは「自分が見るべきバージョン」を選んで読みます。
(更新前)
| id | balance | 状態 |
| 1 | 500 | 有効 | ← T0 で作成
(T1 が balance=400 に更新中、まだ未コミット)
| id | balance | 状態 |
| 1 | 500 | 旧版・論理削除 | ← T0 作成 / T1 によって置換予定
| 1 | 400 | 新版・未コミット | ← T1 で作成
この状態で、T1 と並行して別の読み取り T2 が走っている場合を考えます。
- T2 が T1 開始前に始まっていた → 「T1 が作ったものは見えない」と判断し旧版(500)を読む
- T2 が T1 コミット後に始まる → 新版(400)を読む
読み取りはロックを取らずに自分が見るべきバージョンを選ぶだけなので、書き込みトランザクションがどれだけ長くても読み取りはブロックされません。逆も同じで、長い読み取りがあっても書き込みは新版を別レコードとして書くだけなので待たされません。
バージョンを区別するメタデータ
各バージョンは「どのトランザクションが作ったか / どのトランザクションで削除されたか」で識別します。PostgreSQL の場合、各タプル(行のバージョン)には次のシステム列が付きます。
xmin: そのバージョンを作成したトランザクション IDxmax: そのバージョンを削除(または更新で置換)したトランザクション ID
トランザクション T が読み取るときの可視性判定はおおむね以下です。
xminのトランザクションが T から見てコミット済みで、かつ T の開始より前xmaxがセットされていない、またはxmaxのトランザクションが T から見て未コミット
このルールにより、T はあたかも自分が始まった瞬間のデータベース状態を読んでいるように振る舞います。
MVCC のコスト
MVCC は「読み書きが互いをブロックしない」という大きな利点と引き換えに、次のような性能・運用上の負担があります。
- ストレージが膨らむ: 古いバージョンが残るためテーブルサイズが増える
- ガベージコレクションが必要: 誰からも見えなくなった古いバージョンを回収する仕組みが必要(PostgreSQL の
VACUUM、MySQL InnoDB の purge スレッド) - 長時間トランザクションの影響: 長時間トランザクションが古いバージョンを参照し続けるとテーブルが肥大化する(PostgreSQL ではこれを「テーブルブロート」と呼ぶ)
つまり並行性の改善はストレージ消費と GC の運用コストでまかなわれている、というトレードオフです。
Read Committed の限界 — 防げない異常
Read Committed はダーティリード/ダーティライトを防ぐだけで、それ以外の並行性異常は防げません。これらの異常を防ぎたい場合は、より強い分離レベル(次節以降のスナップショット分離・直列化可能性)を使う必要があります。
Read Committed では防げない異常は以下です。
- 読み取りスキュー(Read Skew): 同じトランザクション内で複数行を読んだとき、行ごとに違う時点のスナップショットを見てしまう
- 更新ロスト・書き込みスキュー(後の節で詳述)も防げない
特に読み取りスキューは集計や送金処理で問題になりやすく、これを防ぐのが後述するスナップショット分離です。
スナップショット分離
スナップショット分離(Snapshot Isolation)は、トランザクション開始時点のデータベース全体の一貫したスナップショットを読み取る分離レベルです。書き込みは他トランザクションをブロックしますが、読み取りは何もブロックしません。
PostgreSQL や MySQL InnoDB の REPEATABLE READ、Oracle の SERIALIZABLE は実態としてスナップショット分離です。
Read Committed との違い — MVCC の読みポリシー
Read Committed もスナップショット分離も同じ MVCC の上に成り立ちます。違いは「どのバージョンを読むか」というポリシーだけです。
- Read Committed
- クエリ実行時点で「最新のコミット済みバージョン」を読む
- 同じトランザクション内でも、SELECT のたびに「その瞬間の最新」が変わりうる
- スナップショット分離
- トランザクション開始時点のスナップショットから読む
- 同じトランザクション内では何度 SELECT しても同じバージョンを返す
この「同じトランザクション内で何度読んでも同じ値」という保証が、次に説明する読み取りスキューを防ぐ鍵になります。
読み取りスキューの典型例
口座 A・B にそれぞれ 500 円ある状態で、A から B に 100 円送金するトランザクション T2 が走っているときに、別のトランザクション T1 が残高合計を確認するとどうなるか。
sequenceDiagram
autonumber
participant T1 as T1(残高合計を確認)
participant DB as accounts
participant T2 as T2(送金)
T1->>DB: BEGIN
T1->>DB: SELECT balance WHERE id=A → 500
T2->>DB: BEGIN
T2->>DB: UPDATE balance=400 WHERE id=A
T2->>DB: UPDATE balance=600 WHERE id=B
T2->>DB: COMMIT
T1->>DB: SELECT balance WHERE id=B → 600
Note over T1: 合計 = 500 + 600 = 1100(!)
T1 が読んだ A の残高は送金前、B の残高は送金後となり、合計が壊れて見えます。これが読み取りスキューです。バックアップや分析クエリのように長時間かかる読み取りで頻繁に問題になります。
スナップショット分離の実装
MVCC の上に「自分のトランザクション開始時点で進行中だったトランザクション ID のリスト」をスナップショットとして持たせます。読み取り時の可視性判定で「xmin がそのリストに含まれていたら、たとえ後にコミットされていても見えない」というルールを足すことで、開始時点で見えていた状態だけを安定して返せます。
これにより、Read Committed とは違い、同じトランザクション内では何度 SELECT しても同じバージョンを返すようになります。
スナップショット分離の限界 — 防げない異常
スナップショット分離は読み取りスキューを防ぎますが、次の異常はまだ防げません。これらを防ぐには、次節の直列化可能性または個別の対策(FOR UPDATE / CAS など)が必要です。
- 更新ロスト(Lost Update)
- 書き込みスキュー(Write Skew)(その根本原因であるファントム現象も含む)
直列化可能性
スナップショット分離が防げない更新ロストと書き込みスキューを含む、あらゆる並行性異常を防ぐのが直列化可能性です。
直列化可能性(Serializability)は、並行実行されたトランザクション群が、何らかの直列実行と等価な結果になることを保証する分離レベルです。
ただし、直列化可能性を使わなくても、特定の異常には個別の対策(アトミック操作・SELECT FOR UPDATE・compare-and-set 等)があります。本節では「直列化可能性が防ぎたい異常」と「それぞれの対策(直列化可能性を含む)」を整理し、最後に直列化可能性そのものの3つの実装方式を見ていきます。
防ぐべき異常1: 更新ロスト(Lost Update)
更新ロストは、2つのトランザクションが「読み取り → 計算 → 書き込み(read-modify-write)」を並行実行したときに、片方の更新が消える異常です。
例: カウンタのインクリメント
sequenceDiagram
autonumber
participant T1
participant DB as counters
participant T2
T1->>DB: BEGIN
T1->>DB: SELECT counter WHERE id=1 → 100
T2->>DB: BEGIN
T2->>DB: SELECT counter WHERE id=1 → 100
T1->>DB: UPDATE counter=101<br/>COMMIT
T2->>DB: UPDATE counter=101<br/>COMMIT
Note over T2: 本来 102 になるはずが 101 になる
T1 と T2 が両方とも 100 を読んで 101 にインクリメントしてしまい、片方の更新が消えます。
対策1: アトミック操作
DB 側で read-modify-write を1ステップにまとめれば、競合自体が起きません。
UPDATE counters SET counter = counter + 1 WHERE id = 1;
カウンタ・在庫数・残高のような単純な数値演算ならこれが最も確実で速いです。
対策2: 明示的ロック(SELECT ... FOR UPDATE)
SELECT ... FOR UPDATE で読み取り時に行ロックを取り、トランザクション終了まで保持します。アプリ側で複雑な計算ロジックがあって SET counter = counter + 1 形式に書き直せない場合に使います。
BEGIN;
SELECT counter FROM counters WHERE id=1 FOR UPDATE;
-- アプリ側で計算
UPDATE counters SET counter=計算結果 WHERE id=1;
COMMIT;
詳細は『MySQL 8のSKIP LOCKED・NOWAIT(ロッキングリードオプション)を試してみる』も参照してください。
対策3: 自動検出(DBMS による更新ロスト検出)
PostgreSQL・Oracle・SQL Server のスナップショット分離は、更新ロストを自動検出してトランザクションを中断(abort)します。アプリは中断を検知してリトライする責任があります。MySQL InnoDB のスナップショット分離(REPEATABLE READ)はこの自動検出を行いません。
対策4: Compare-and-Set(楽観ロック)
UPDATE の WHERE 句に読み取った時点の値を含めることで、値が変わっていたら更新を拒否させます。
UPDATE counters SET counter = 101 WHERE id = 1 AND counter = 100;
-- 影響行数が 0 ならリトライ
バージョン列を使う形式(UPDATE ... SET version=version+1 WHERE id=? AND version=?)も同じ発想です。RDB だけでなく分散 KVS でも一般的に使えます。
防ぐべき異常2: 書き込みスキュー(Write Skew)
書き込みスキューは、複数行に対する前提条件(不変条件)に基づいて判断するときに、並行実行で前提が崩れる異常です。スナップショット分離では防げません。
例: 当直医の最低人数
「常に最低1人は当直医がいる」という制約があり、現在 Alice と Bob の2人が当直中です。両者が同時に「もう1人いるから自分は外れていい」と判断するとどうなるか。
sequenceDiagram
autonumber
participant T1 as T1(Alice が外れる)
participant DB as doctors
participant T2 as T2(Bob が外れる)
T1->>DB: BEGIN
T1->>DB: SELECT COUNT(*) WHERE on_call=true → 2
Note over T1: OK、外れてよい
T2->>DB: BEGIN
T2->>DB: SELECT COUNT(*) WHERE on_call=true → 2
Note over T2: OK、外れてよい
T1->>DB: UPDATE on_call=false WHERE name='Alice'<br/>COMMIT
T2->>DB: UPDATE on_call=false WHERE name='Bob'<br/>COMMIT
Note over DB: 当直医ゼロ
両者が「他にもう1人いる」というスナップショットを見て判断したのに、現実は両方が外れて当直医がゼロになります。
書き込みスキューが現れる典型ケース
- 会議室予約: 同じ時間帯に2件の予約が並行で入る
- ユーザー名のユニーク制約: 複合条件のユニーク性(DB のユニーク制約だけでは表現できないもの)が並行で破られる
- 口座残高がマイナスにならない条件: 残高 100 円から 60 円ずつ2回引き出される
- マルチプレイヤーゲーム: 同じマスを2人のプレイヤーが同時に取る
ファントム — 書き込みスキューの根本原因
書き込みスキューの根本原因はファントムにあります。ファントムとは「あるトランザクションでの書き込みが、他のトランザクションの検索結果を変える」効果です。当直医の例では、SELECT COUNT(*) WHERE on_call=true の結果が他トランザクションの UPDATE で変わります。
ファントムは個別の行に対するロックでは防げません。「条件にマッチする全行 + 将来マッチするかもしれない行」を仮想的にロックする必要があり、これが実装上の難所です。
書き込みスキューの対策
- 対策1:
SELECT ... FOR UPDATEで関連行をロック- 当直医の例なら
SELECT * FROM doctors WHERE on_call=true FOR UPDATEで当直中の行を全部ロックする
- 当直医の例なら
- 対策2: 衝突の実体化
- 会議室予約のように対象行が事前に存在しない場合、人為的に「予約スロット」の行を作っておきそれをロックする
- 対策3: 直列化可能性を使う
- DBMS に任せる方法。次の実装方式の節を参照
直列化可能性の3つの実装方式
「直列化可能」と一口に言っても、実装方式は DBMS によって大きく異なります。代表的な3方式を整理します。
方式1: 完全な順次実行
トランザクションを単一スレッドで1本ずつ順番に実行する方式です。VoltDB / H-Store / Redis / Datomic がこれを採用しています。
- インメモリ前提(ディスク I/O 待ちで他のトランザクションをブロックすると意味がない)
- ストアドプロシージャ前提(アプリ↔DB の往復通信を1回にまとめる)
- 短時間のトランザクション前提
並行性制御のオーバーヘッドが完全にゼロになり、コード側もシンプルになります。一方で CPU コア1個分のスループットが上限となるため、書き込み量が大きい場合は不向きです。スケールにはパーティショニングを使いますが、『データ指向アプリケーションデザイン』によると、複数パーティションをまたぐトランザクションは VoltDB で約 1,000 回/秒に落ちます。
方式2: 2相ロック(2PL: Two-Phase Locking)
1970年代から30年間、唯一の現実的な直列化可能性の実装でした。MySQL InnoDB の SERIALIZABLE モードや SQL Server の SERIALIZABLE がこれです。
ロックは以下の2モードで動きます。
- 共有モード(読み取り): 複数のトランザクションが同時に取得できる
- 排他モード(書き込み): 1つのトランザクションだけが取得でき、共有ロックも他の排他ロックも待たせる
ロックはトランザクション終了まで保持され、これが「2相」の名前の由来です(取得フェーズと解放フェーズ)。共有・排他ロックの詳細は『【MySQL】InnoDBの共有ロックと排他ロックの概要と挙動検証』を参照してください。
ファントム防止のためには、検索条件にマッチする全行をロックする述語ロック(predicate lock)が概念的に必要ですが、効率が悪いので多くの実装は next-key ロック / インデックス範囲ロックで近似します。
2PL は確実に動作する一方で、次の弱点があります。
- デッドロックが頻繁に発生する(DBMS が検出して片方を中断する)
- レイテンシが不安定で、高負荷時に p99 が大きく悪化する
方式3: 直列化可能スナップショット分離(SSI)
SSI(Serializable Snapshot Isolation)は Cahill の 2008 年の博士論文で提案された比較的新しい方式で、PostgreSQL 9.1 以降と FoundationDB が採用しています。
楽観的並行性制御(Optimistic Concurrency Control)に分類され、次のように動きます。
- スナップショット分離と同じく、各トランザクションは開始時点のスナップショットから読む
- 読み取りはブロックされない(スナップショット分離の利点を維持)
- コミット時に DBMS が「このトランザクションの判断は古い前提に基づいていないか」を検査
- 衝突を検出したらコミットを中断(abort)してアプリにリトライさせる
検査される代表的な衝突は以下の2つです。
- 古くなった前提の検出: トランザクション T が読んだ MVCC バージョンが、T のコミット時点までに他トランザクションによって更新されていないか
- 先行する読み取りに影響する書き込みの検出: T の書き込みが、進行中の他トランザクションの読み取り結果を変えていないか
書き込みトランザクションの中断率がパフォーマンスを左右するため、書き込みトランザクションは短く保つ必要があります。読み取り専用トランザクションは長くてもブロックされず、中断もされません。
直列化可能性の3実装比較
| 方式 | 採用 DBMS | 並行性 | 強み | 弱み |
|---|---|---|---|---|
| 完全な順次実行 | VoltDB / Redis / Datomic | なし(単一スレッド) | シンプル・並行性制御コストゼロ | CPU 1コア上限 |
| 2相ロック(2PL) | MySQL InnoDB / SQL Server | あり(ロック競合) | 古くから実績、確実 | デッドロック頻発、p99 不安定 |
| SSI | PostgreSQL 9.1+ / FoundationDB | あり(楽観的) | 読み取りブロックなし、長い読み取りに強い | 中断率がパフォーマンスに直結 |
主要 DBMS の SERIALIZABLE 実装比較
「SERIALIZABLE を指定したらどう動くか」は DBMS によって異なります。
| DBMS | SERIALIZABLE の実装 | 書き込みスキュー検出 |
|---|---|---|
| PostgreSQL 9.1+ | SSI | あり |
| MySQL InnoDB | 2PL | あり |
| SQL Server | 2PL | あり |
| Oracle | スナップショット分離(実態) | なし |
Oracle の SERIALIZABLE は実態がスナップショット分離なので、書き込みスキューは防げません。Oracle で書き込みスキュー対策をしたい場合は、明示的に SELECT ... FOR UPDATE でロックを取る必要があります。
実務での選び方
通常はデフォルト分離レベルで十分
PostgreSQL の READ COMMITTED、MySQL InnoDB の REPEATABLE READ(実態スナップショット分離)など、デフォルト設定で大半のアプリは問題なく動きます。安易に変更しないことです。
書き込みスキューが問題になる場面だけ対策
書き込みスキューは「複数行の前提条件に基づく判断」を行うときだけ発生します。先述の典型ケース(当直医・予約・残高制約・マルチプレイヤー)に該当する場合のみ、以下のいずれかを選びます。
SELECT ... FOR UPDATEで対象行をロック: 局所的かつ既存システムへの影響が小さいSERIALIZABLEに変更: 書き込みスキューを起こすコード全体を直さなくて済むが、性能影響と中断リトライの実装が必要- 衝突の実体化: 対象行が事前に存在しない予約系で有効
パフォーマンスのために安易に分離レベルを下げない
「ロック競合でスループットが落ちるから READ COMMITTED に下げる」は典型的なアンチパターンです。本記事で見たように、下のレベルになるほど(特に MySQL のデフォルトを READ COMMITTED に下げると)、読み取りスキュー・更新ロスト・書き込みスキューが新たに発生します。
スループット問題への対処は、分離レベルではなくロック保持時間の短縮やインデックス・クエリの改善が基本です。詳細は『ACIDトランザクションの4特性』の同論点も参照してください。
弱い分離レベルのバグは Jepsen で多数暴かれている
『データ指向アプリケーションデザイン』によると、分離レベルの実装は DBMS 間で大きくバラついており、Jepsen テストによって多くの DBMS の主張と現実の食い違いが暴かれてきたとされています。クリティカルなトランザクションを書く際は、ドキュメントを鵜呑みにせず、ベンダーの記述・公式 issue・テストレポートを併せて確認するのが安全です。
まとめ
- ANSI SQL 4段階は規格上の枠組みで、実装は規格通りには動かない。実装ベースでは Read Committed → スナップショット分離 → 直列化可能性 の3段階で考えるとよい
- Read Committed は行レベルロック+MVCC でダーティリード/ダーティライトを防ぐ。読み取りスキュー以降は防げない
- スナップショット分離(MVCC)はトランザクション開始時のスナップショットを読む。読み取りスキューを防ぎ、読み書きが互いをブロックしない。更新ロスト・書き込みスキューは防げない
- 更新ロストの対策は4つ: アトミック操作・
SELECT FOR UPDATE・自動検出・compare-and-set - 書き込みスキュー(根本原因はファントム現象)はスナップショット分離では防げない。
SELECT FOR UPDATE・衝突の実体化・直列化可能性が必要 - 直列化可能性は3実装: 完全な順次実行(VoltDB / Redis)・2相ロック(MySQL InnoDB / SQL Server)・SSI(PostgreSQL 9.1+ / FoundationDB)
- DBMS の
SERIALIZABLE指定は実装が違う。特に Oracle はスナップショット分離なので書き込みスキューを防げない