TL

ファントムと述語ロック・ギャップロック・next-keyロック

範囲検索の結果が同一トランザクション内で増える――この厄介なファントムを、InnoDB がギャップロックでどう封じるかを理屈から押さえれば、謎のロック待ちやデッドロックの原因が読み解けます。

応用データベーストランザクション同時実行制御InnoDBMySQL分離レベル最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.ファントムは行ロックでは防げない。存在しない行の挿入を止めるには、述語が指す範囲そのものをロックする必要がある。
  • 2.理想は述語ロックだが計算コストが高い。InnoDB はインデックス上のギャップロックとnext-keyロック(行ロック+直前ギャップ)で範囲を近似的にロックし、REPEATABLE READでファントムを防ぐ。
  • 3.ギャップロックは挿入だけを止め、互いに共存できる。デッドロックや想定外のロック範囲は、検索がどのインデックスをどう走査したかで決まる。

ファントムは行ロックでは防げない

ファントムリード(phantom read)とは、同一トランザクション内で同じ範囲述語のクエリを2回実行したら、2回目に行が増減している現象です。たとえば SELECT ... WHERE age BETWEEN 20 AND 30 を2回走らせる間に、別トランザクションが age = 25 の行を挿入してコミットすると、1回目には無かった行が2回目に現れます。

ここが本質的に難しいのは、まだ存在しない行をロックできない点です。読み取りロック(共有ロック)は「いま存在する行」にしか掛けられません。最初の検索時に該当行を全部ロックしても、age = 25 の行はその時点で存在しないのでロック対象がなく、挿入を止められません。ダーティリードやノンリピータブルリードが既存行の値に対する異常であるのに対し、ファントムは集合の要素数(述語を満たす行の集合)に対する異常です。だから行単位のロックでは構造的に対処できません。

ノンリピータブルリードとの違い

ノンリピータブルリードは「同じを2回読むと値が変わる」異常で、既存行への共有ロック保持で防げます。ファントムは「同じ述語を2回評価すると行数が変わる」異常で、止めるべきは既存行ではなく述語を満たす行の出現(INSERT)そのものです。ANSI SQL の定義上も両者は別アノマリーとして区別されます(→ トランザクション分離レベル)。

理想解:述語ロック

理論的に正しい解は述語ロック(predicate lock)です。これは個々の行ではなく「age が 20 以上 30 以下」という述語(条件式)そのものをロック対象にします。あるトランザクションがこの述語で読み取りロックを取っているとき、別トランザクションがその述語を満たす行を挿入・更新・削除しようとすると衝突として待たされる、という発想です。存在しない行も「将来その述語を満たすかどうか」で判定するので、ファントムを原理的に封じられます。

問題は実装コストです。挿入を試みるたびに、現在ロック中の全述語に対して「この行はその述語を満たすか」を評価しなければなりません。一般の述語の包含判定(2つの条件が重なるか)は計算が重く、述語が増えるほどコストが膨らみます。

述語ロックの判定(理想):
  INSERT する行 r について
    for each 保持中の述語ロック P:
      if r が P を満たす:  ロック衝突 → 待機 or デッドロック
  ※ 任意の述語の充足判定は高コスト。実用 RDB はこれをそのまま実装しない

そこで多くの RDB は、述語ロックをインデックス構造の上で近似します。「条件を満たす行が入りうる場所」をインデックスのキー範囲として特定し、その範囲をロックするのです。InnoDB のギャップロックはこの近似の代表例です。なお PostgreSQL の SIREAD ロックも述語ロックの一種ですが、あちらは挿入を待たせずrw-依存を検出して片方を中断する楽観的な実装である点が異なります(→ 直列化可能スナップショット分離(SSI)の検出アルゴリズム)。

InnoDB の3種のロック

InnoDB のロックは、述語ロックを B+木インデックスのキー空間で近似するために設計されています。粒度は次の3つです。

ロック種別対象止めるもの
レコードロック (record lock)インデックス上の特定レコード(行)その行へのUPDATE/DELETE。挿入は止めない
ギャップロック (gap lock)インデックス上の隣接2キーの間の隙間(既存行は含まない)その隙間へのINSERTのみ。既存行の読み書きは止めない
next-keyロック (next-key lock)レコードロック+そのレコード直前のギャップロックその行の更新と、直前の隙間への挿入の両方

ポイントは、InnoDB のロックがテーブルの行ではなくインデックスのレコードに掛かることです。ロックの範囲はクエリが走査したインデックスのキー順序で決まります。ギャップロックは「隙間」というキー値が存在しない領域への印なので、複数のトランザクションが同じギャップに対するギャップロックを同時に保持できます(互いに共存する)。ギャップロックが衝突するのは「ギャップをロックする側」と「そのギャップに挿入する側」の間だけ、という非対称な設計です。

next-keyロックは半開区間 (前のキー, 自分] を守る

next-keyロックは「レコード本体」と「直前のギャップ」をまとめたもので、概念的にはインデックス順で (直前のキー, 自分のキー] という左開右閉の区間をロックします。InnoDB の REPEATABLE READ がインデックススキャンで既定採用するのがこの next-keyロックで、走査して通過した各レコードにこれを掛けることで、ヒットした行の更新も、ヒット行どうしの隙間への挿入も同時に封じます。

ギャップロックがファントムを防ぐ原理

具体例で見ます。id に主キー(インデックス)があり、既存値が 10, 20, 30 だとします。あるトランザクション T1 が次を実行したとします。

-- REPEATABLE READ
SELECT * FROM t WHERE id BETWEEN 15 AND 25 FOR UPDATE;

T1 は B+木を走査し、述語 15 以上 25 以下 が指す範囲をカバーするように next-keyロックを掛けます。具体的には、既存行 20 にレコードロックを掛けるだけでなく、その前後のギャップ――(10, 20)(20, 30) の隙間――にギャップロックを掛けます。

インデックス: ... 10 ──gap── 20 ──gap── 30 ...
T1 が WHERE id BETWEEN 15 AND 25 でロックする範囲:
   (10, 20] = 20 へのレコードロック + (10,20) のギャップ
   (20, 30) = 20 の次のギャップ(上限25を含む隙間)
ここに別 Tx が INSERT id=17 / id=23 しようとすると → ギャップロックで待機

この状態で別トランザクション T2 が INSERT INTO t VALUES (17) を試みると、17 が入るべき隙間 (10, 20) に T1 のギャップロックがあるため、T2 は挿入意図ロック(insert intention lock)の取得で待たされます23 も同様に (20, 30) で待たされます。結果として、T1 が同じ範囲検索を再実行しても新しい行は現れません。これが存在しない行の挿入を、その行が入る隙間のロックで止めるというファントム防止の中核です。

挿入意図ロックとギャップロックの非対称性

INSERT は対象の隙間に対し挿入意図ロックを取ろうとします。挿入意図ロックどうしは(別の位置なら)両立しますが、既存のギャップロックとは衝突します。一方でギャップロックどうしは衝突しません。つまり「読んで範囲を押さえる側」が複数いても互いに邪魔せず、唯一止まるのは「その範囲に割り込んで挿入する側」だけ、という設計です。この非対称性がギャップロックの読み取り並行性を保っています。

ロック範囲は検索の走査方法で決まる

next-keyロックの厄介な点は、ロックされる範囲がクエリの述語そのものではなく、実行時にインデックスをどう走査したかで決まることです。述語ロックの「近似」である以上、近似がゆるい方向(広め)に倒れることがあります。

状況InnoDB のロック挙動
一意インデックスで等値検索し1行ヒットギャップロックは不要と判断し、レコードロックのみに退化(挿入を止める必要がないため)
非一意インデックスでの等値・範囲検索ヒット行+前後のギャップにnext-keyロック。重複値の間への挿入も止める
インデックスが無い列での検索全行を走査するため実質的に全レコード+全ギャップをロックし、並行挿入をほぼ全面的に止める
READ COMMITTED 分離レベルギャップロックを基本的に使わずレコードロックのみ。代わりにファントムは許容される

特に重要なのはインデックスが無い検索で、走査が全表に及ぶため、述語が1行しか指していなくても実質テーブル全体のギャップがロックされ、無関係な挿入まで止まります。ロック範囲を述語に近づける――つまりギャップロックを最小化する――には、WHERE 句に効く適切なインデックスを張ることが直接効きます。検索が触れるインデックスのキー範囲が狭いほど、ロックする隙間も狭くなるからです。

REPEATABLE READ なのにファントムが防げる理由(MySQL固有)

ANSI SQL の定義では REPEATABLE READ はファントムを許容します。しかし InnoDB の REPEATABLE READ はロック読み取り(FOR UPDATE / FOR SHARE / UPDATE / DELETE)に対して next-keyロックを使うため、ロックを伴う範囲検索についてはファントムを防げます。一方、ロックを取らない通常の SELECT は MVCC のスナップショットを読むので、そもそもファントムを見ません(→ MVCCの内部実装)。「スナップショットで見ない読み取り」と「ギャップロックで止める書き込み系読み取り」の二段構えで、InnoDB は REPEATABLE READ でも実質的に直列化に近い挙動を実現しています。

デッドロックとの関係・実務上の注意

ギャップロックは挿入を止めるため、複数トランザクションが同じ隙間に対して挿入しようとして待ち合い、デッドロックになりやすい温床でもあります。典型は、2つのトランザクションがそれぞれ範囲検索でギャップを共有保持し(ギャップロックは共存する)、その後それぞれが同じ隙間に挿入しようとして、互いの挿入意図ロックを待つケースです。InnoDB は待ちグラフの閉路を検出して片方を犠牲者としてロールバックします(→ トランザクション分離レベル のロック方式と同じく、デッドロックは2PL系で残る別問題です)。

実務でのポイントを整理します。

  • ギャップロックを減らしたいなら適切なインデックスを張る。走査範囲が狭まればロックする隙間も狭まり、並行挿入との衝突とデッドロックが減る。
  • READ COMMITTED にすればギャップロックはほぼ消えるが、その代償としてファントムを許容する。挿入競合が激しいワークロードで意図的に選ぶことがある。
  • 一意インデックスでの単一行検索はギャップロックを伴わずレコードロックのみに退化することが多く、点指定の更新は範囲ロックを生まない。範囲条件やインデックスの効かない検索が広いロックを生む。
  • InnoDB のロックはあくまでインデックス上のキー範囲への印であり、述語ロックの近似である――この前提を押さえると、SHOW ENGINE INNODB STATUS で見える想定外のロック待ちが「どのインデックスのどの隙間か」として読み解ける。

まとめ

  • ファントムは述語を満たす行の集合の要素数が変わる異常で、まだ存在しない行を行ロックでは止められない。止めるには述語が指す範囲そのものをロックする必要がある。
  • 理想解は述語ロックだが任意述語の充足判定が高コスト。InnoDB はギャップロック(隙間への挿入だけを止める)とnext-keyロック(レコード+直前ギャップ)で、インデックスのキー範囲として述語ロックを近似する。
  • ギャップロックどうしは共存し、衝突するのは挿入側との間だけ。REPEATABLE READ + ロック読み取りでファントムを防ぎ、通常の SELECT は MVCC スナップショットで読む二段構え。
  • ロック範囲は実行時の走査方法で決まるため、適切なインデックスでロックを述語に近づけるのが、想定外のロック待ち・デッドロックを避ける王道。

データベース Article

ファントムと述語ロック・ギャップロック・next-keyロックを実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

データベース

比較で見る軸

難易度: advanced / カテゴリ: データベース / タグ数: 6

導入後に効く点

理想は述語ロックだが計算コストが高い。InnoDB はインデックス上のギャップロックとnext-keyロック(行ロック+直前ギャップ)で範囲を近似的にロックし、REPEATABLE READでファントムを防ぐ。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
データベース
タグ数
6

判断チェックリスト

  • 自社の用途が「データベース / トランザクション」に近いか確認する。
  • 強みである「ファントムは行ロックでは防げない。存在しない行の挿入を止めるには、述語が指す範囲そのものをロックする必要がある。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

データベーストランザクション同時実行制御InnoDBMySQLデータベーストランザクション同時実行制御
参考: 公式情報