FIG-076

トランザクション分離レベル — 他人の途中経過は、どこまで見えるのか

データベース 2026.08.31 公開 読了 約13分

データベースには、同じ瞬間にいくつものトランザクションが流れ込みます。問題は、まだ終わっていない他人の作業が、自分にどこまで見えてしまうかです。

見えすぎれば、取り消されるはずの値を読んでしまう。見えなさすぎれば、待たされて遅くなる。その加減を選ぶつまみが分離レベルです。下の図1で、2つのトランザクションを1ステップずつ動かしながら、レベルを切り替えて異常が起きる瞬間と、消える瞬間を見てください。

ISOLATION LEVEL — 他人の途中経過は、どこまで見えるのか
起こしたい異常
分離レベル
時間 → 12345
T-A
T-B
実際のテーブル 確定済みの状態
id=1 -
id=2 -
id=3 -
T-Aから見えている世界 最新をそのまま見る
id=1 -
id=2 -
id=3 -
T-Aの1回目 —
=
T-Aの2回目(同じSQL) —
RURCRRSER
ダーティリード ----
反復不能読み取り ----
ファントムリード ----
STEP 0 / 5
2つのトランザクションを、1ステップずつ動かす
T-A は残高を読むだけ、T-B はその残高を書き換えるトランザクションです。「次のステップへ」を押して、T-Aが2回読んだ結果が食い違うかを確かめてください。分離レベルを変えると、同じ手順でも結末が変わります。
図1 — 手順はまったく同じ。分離レベルだけを変えて、T-Aに何が見えるかを比べる

MVCC — 待たせずに「昔の姿」を見せる

素朴に考えれば、他人の書き換えを見せない方法はロックです。読んでいる間は誰にも書かせない。しかしそれでは、読む人が増えるほど書く人が待たされ、データベースは止まってしまいます。

そこで多くのDB(PostgreSQL、MySQL/InnoDB、Oracleなど)が採るのが MVCC(多版型同時実行制御) です。更新するとき古い値を上書きせず、新しい版を別に書き足す。すると、それぞれのトランザクションは自分に合った時点の版を読めばよくなります。図1の右パネルが REPEATABLE READ で凍りついたまま動かないのは、T-Aが「BEGINした瞬間のスナップショット」を見続けているからです。

ここがMVCCの効きどころで、読む人は誰も待たず、書く人も邪魔されない。「読み取りは書き込みをブロックせず、書き込みは読み取りをブロックしない」とよく言われるのは、この仕組みのことです。

REPEATABLE READ とファントムの、教科書とのズレ

SQL規格では、REPEATABLE READ ではファントムリードが起こりうると定められています。ところが図1で REPEATABLE READ を選ぶと、ファントムは起きません。矛盾ではなく、規格は「最低限これだけは防げ」という下限を決めているだけだからです。

スナップショット方式のDBでは、行の更新も新しい行の追加もまとめて「BEGIN以降の変更」として除外されるので、ファントムも自然に防がれてしまう。マトリクスの △ はこの意味です。「規格上どうか」ではなく「使っているDBが実際にどう振る舞うか」で判断する――分離レベルを扱ううえで、いちばん実務的な注意点です。

用語ミニ辞書
ダーティリード
まだコミットされていない値を読んでしまうこと。取り消されれば「存在しなかった値」になる。
反復不能読み取り
同じ行を2回読んだのに、値が変わっていること。途中で他人がコミットしたため。
ファントムリード
同じ条件で2回検索したのに、行数が変わっていること。他人が行を追加・削除したため。
MVCC
更新時に古い版を残す方式。読み手を待たせずに一貫した過去の姿を見せられる。
スナップショット
あるトランザクションが見続ける「時点」。REPEATABLE READ ではBEGIN時点に固定される。

まとめ

分離レベルは「正しさ」と「同時に動ける量」の交換レートです。緩めれば速いが他人の途中経過が漏れ、締めれば正しいが待ちが増える。実務では READ COMMITTED か REPEATABLE READ を既定にし、在庫の引き当てや残高の増減のように「読んだ値を根拠に書き込む」処理だけ、SERIALIZABLE か明示的なロックで守るのが定石です。まず自分のDBの既定レベルを確認するところから始めてください。