トランザクション分離レベルと 3 つの読み取り異常
トランザクション分離レベル (isolation level) とは、並行実行されるトランザクションが互いの未確定・確定済みの変更をどこまで観測しうるかを規定する設定であり、SQL 標準では許容する読み取り異常の種類によって READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE の 4 段階が定義されている。
分離レベルとは — 「他人の途中経過がどこまで見えるか」の設定
データベースは複数のトランザクションを同時に走らせる。このとき、他人がまだ書き換えている途中のデータが自分にどこまで見えるかを決めるのが分離レベルである。
完全に見えなくすれば安全だが、その分だけ待ち時間が増えて遅くなる。 逆にすべて見せれば速いが、存在しなかった値を読んでしまう。分離レベルは、この安全性と性能のトレードオフをどこで切るかを選ぶダイヤルで、 SQL 標準では 4 段階が定義されている。
4 段階 — 緩い順に、見える範囲が狭くなる
まず 4 段階それぞれが何なのかを、見え方だけで押さえておく。 上に行くほど緩く、下に行くほど厳しい。
| 分離レベル | ひとことで | 何が見えるか |
|---|---|---|
READ UNCOMMITTED | 書きかけまで見える | 他のトランザクションがまだ確定していない、書きかけの変更まで見える。最も制限が緩く、取り消される予定の値まで読んでしまう。 |
READ COMMITTED | 確定したものだけ見える | 確定 (COMMIT) された変更だけが見える。ただし見えるのは「読んだその瞬間」の最新状態なので、同じトランザクションの中でも読むたびに結果が変わりうる。 |
REPEATABLE READ | 読み始めた時点を見続ける | 自分が読み始めた時点の状態を、トランザクションが終わるまで見続ける。途中で他人が確定させても自分の視界は変わらない。 |
SERIALIZABLE | 順番に実行したのと同じ | 並行して走っていても、トランザクションを 1 つずつ順番に実行した場合と同じ結果になることを保証する。最も厳しく、最もコストが高い。 |
仕組みから見ると「読み取りロックをどれだけ長く持つか」の違い
この見え方の差は、どこから生まれるのか。 ロックで実装した場合の古典的なモデルで見ると、4 段階の正体がはっきりする。
| 分離レベル | 読み取りロック | 書き込みロック | 結果として |
|---|---|---|---|
READ UNCOMMITTED | 取らない | 終了まで保持 | 書き込み中の行もそのまま読めるので、確定していない値が見える。 |
READ COMMITTED | 取るが、読んだ直後に解放 | 終了まで保持 | 読む瞬間だけは他人の書き込みと衝突しない。ただし解放後は他人が更新できるので、次に読むと値が変わる。 |
REPEATABLE READ | 読んだ行に対して終了まで保持 | 終了まで保持 | 一度読んだ行は誰にも変更されない。ただしロックを掛けられるのは既にある行だけなので、新しく挿入される行は止められない。 |
SERIALIZABLE | 行に加えて、検索条件の範囲にも掛ける (述語 / ギャップロック) | 終了まで保持 | 「条件に合う行が後から増える」ことまで止まる。その代わり待ちと競合が最も多い。 |
見るべきなのは、書き込みロックの列が 4 段階すべてで同じだということ。 書き込みは常にトランザクション終了まで排他される。 つまり4 段階の違いは、読み取りロックをどれだけ長く持つかだけに現れる。 分離レベルが「読み取り側の設定」と言われるのはこのためである。
※ これはロックで実装した場合のモデルで、標準がロックの掛け方を 規定しているわけではない。主要な DBMS の通常の読み取りはロックを取らない方式で動いている (後述)。どの行を明示的にロックするかを自分で制御する話は同時実行制御で扱う。
緩いほど待たされず速いが、その分だけ「本来読むべきでないもの」が見えてしまう。 では具体的に何が見えると困るのか。それを整理したのが、次の 3 つの読み取り異常である。
3 つの読み取り異常
- ダーティリード (dirty read) — まだ COMMIT されていない、後で取り消される値を読んでしまう。
- ノンリピータブルリード (non-repeatable read) — 同じ行を 2 回読んだのに、間に他人が COMMIT したせいで値が変わる。
- ファントムリード (phantom read) — 同じ条件で 2 回検索したのに、間に他人が INSERT したせいで行数が変わる。
文章で読むと似て見えるが、実際に起きている順序を見ると別物だと分かる。 下のシミュレーターで、2 つのトランザクション (T1 = 自分 / T2 = 他人) の操作を 1 ステップずつ進めてみてほしい。
「次のステップ」を押すと、T1 と T2 の操作が 1 つずつ進む。 分離レベルを切り替えると、同じ操作列のまま T1 の見え方だけが変わる。
まだ読んでいない
| id | item | qty |
|---|---|---|
| 1 | ノート | 5 |
| 2 | 消しゴム | 8 |
操作列は変えずに分離レベルだけを切り替えられるようにしてある。 同じ順序で同じ SQL を実行しているのに、READ UNCOMMITTED では見えていたものがREAD COMMITTED では見えなくなる。これが分離レベルの働きそのものである。
4 段階 × 3 異常のマトリクス
SQL 標準の定義をまとめると次のようになる。「起きうる」は許容されているという意味で、 実装が実際に起こすとは限らない (次の節)。
| 分離レベル | ダーティリード | ノンリピータブルリード | ファントムリード |
|---|---|---|---|
READ UNCOMMITTED | 起きうる | 起きうる | 起きうる |
READ COMMITTED | 起きない | 起きうる | 起きうる |
REPEATABLE READ | 起きない | 起きない | 起きうる |
SERIALIZABLE | 起きない | 起きない | 起きない |
表を上から下へ読むと、防げる異常が 1 つずつ増えていくのが分かる。 分離レベルが 4 段階なのは、3 つの異常を順に潰していった結果である。
ここでファントムリードだけが 1 段階遅れて防がれていることに気付く。 理由は前に見たロックの表にある。行ロックは既に存在する行にしか掛けられないので、REPEATABLE READ で読んだ行を全部ロックし続けても、まだ存在しない行は誰もロックできず INSERT を止められない。 条件に合う範囲そのものを押さえる別の道具 (述語ロック / ギャップロック) が要る。 3 つのうちファントムだけ防ぐのが遅れるのは、必要な道具が違うからである。
標準の定義と、実際の DBMS の挙動は違う
ここが分離レベルの解説でいちばん混乱しやすい。SQL 標準が定めているのは「そのレベルで最低限これは防ぐ」という下限であって、 実装がそれより強く防ぐことは違反ではない。
しかも手段も標準が決めているわけではない。前節はロックで説明したが、 PostgreSQL や InnoDB の通常の SELECT は読み取りロックを取らない。行の複数バージョンを保持しておき、 自分に見えるべきバージョンを選んで返す方式 (MVCC) で同じ保証を実現している。 読み取りが書き込みを待たせないぶん速い。
- PostgreSQL の
REPEATABLE READはファントムリードも防ぐ。 トランザクション開始時点のスナップショットを固定するため、 標準が許容しているファントムリードも実際には現れない - PostgreSQL には
READ UNCOMMITTEDが実質的に存在しない。 指定はできるがREAD COMMITTEDとして扱われるため、ダーティリードは起こらない - MySQL (InnoDB) の既定は
REPEATABLE READ。 PostgreSQL の既定 (READ COMMITTED) と違う。 さらに InnoDB はギャップロックによって範囲検索の隙間も押さえるため、ファントムリードを防ぐ
つまり「REPEATABLE READ ならファントムリードが起きる」は標準の話であって、 主要な DBMS の実挙動ではない。試験では標準の定義が問われ、 実務では使っている DBMS の挙動が効く。この 2 つを分けて覚えておくと混乱しない。
実務でどれを選ぶか
既定のまま (READ COMMITTED または REPEATABLE READ) で動かし、必要な処理だけ局所的に上げるのが基本方針になる。
- 大半の処理: 既定のまま。読んで表示するだけの処理に強い分離は要らない
- 読んだ値を根拠に書き込む処理 (残高チェックしてから引き落とす、 在庫を見てから確保する): ここだけ引き上げるか、
SELECT ... FOR UPDATEで明示的に行をロックする SERIALIZABLEを使うならリトライ処理を書く。 競合したトランザクションは待たされるのではなく失敗することがある。 「遅くなる」より「エラーが増える」ほうが実装上の影響は大きい
なお、分離レベルを上げても防げない事故もある。2 人が同じ行を読んで別々に書き戻し、 後の書き込みが先の書き込みを消してしまう更新消失 (Lost Update)は、分離レベルではなくロックの設計で対処する問題である。
よくある疑問
もっと学びたい方へ(おすすめ書籍)
テーブル設計と正規化、パフォーマンス考慮のインデックス設計まで実務レベルで学べる定番書。第2版ではクラウド対応も強化。
ER 図をどう「使える設計」に落とすか、実務の判断まで踏み込んだ入門書。エンティティの切り出しから多対多の扱いまで具体例が豊富。
SQLの本質的な使い方と、インデックスが効くクエリの書き方を学べる。ウィンドウ関数など現代SQLも網羅。
実務でやりがちなSQL・DB設計のアンチパターンとその回避策を体系的に学べる。
PostgreSQLの内部構造・ストレージ・インデックス機構を丁寧に解説。設計と運用計画の鉄則が学べる。
IPAデータベーススペシャリスト試験の総合対策書。インデックス関連は本サイトと合わせて学ぶと理解が深まる。
リレーショナルモデルの理論から、インデックス設計を含む実務で使えるSQLまで解説。
本セクションはAmazonアソシエイトのリンクを含みます。
もっと深くDBを学びたい方へ。
たいてっくが、SQL・データベース設計・パフォーマンスチューニング・IPAデータベーススペシャリスト対策まで、1対1で学習をサポートします。まずは無料相談から。
「教え方も上手で、お人柄も良いメンターです。DB周りの知識はもちろん、何より、しっかり教えてあげようという姿勢がとてもありがたかったです。データベース、SQLの学習を考えている方にはおススメです。」
— H 様(DB・SQL コース受講)「体系的に知識を教えてくださり、実際の業務でも大変役立っております。特に短い時間で効率よく知識の習得や、練習をできているのは期待以上でした。」
— M 様(DB・SQL コース受講)「大変充実したコンテンツでわかりやすいご説明をありがとうございました。基本的な質問にも丁寧にご説明いただき、また業務のご相談にも乗って頂き大変有意義な時間でした。」
— K 様(DB・SQL コース受講)