同じクエリーでさまざまな時間にさまざまな行のセットが生成されると、いわゆるファントムの問題がトランザクション内で発生します。 たとえば、SELECT
が 2 回実行されたが、1 回目には返されなかった行が 2 回目には返された場合、その行が「ファントム」行です。
child
テーブルの id
カラム上にインデックスがあり、識別子の値が 100 よりも大きいすべての行をテーブルから読み取り、選択された行の一部のカラムをあとで更新するという意図でロックすると仮定します。
SELECT * FROM child WHERE id > 100 FOR UPDATE;
クエリーでは、id
が 100 よりも大きい最初のレコードからインデックスがスキャンされます。 このテーブルには id
の値が 90 と 102 の行が格納されているものとします。 スキャン範囲内のインデックスレコード上に設定されたロックによって、ギャップ (この場合のギャップは 90 から 102 まで) への挿入がロックアウトされていない場合は、別のセッションが id
が 101 の新しい行をそのテーブルに挿入できます。 同じトランザクション内で同じ SELECT
を実行すると、クエリーから返された結果セット内に、id
が 101 の新しい行 (「ファントム」) が含まれています。 一連の行をデータ項目とみなせば、この新しいファントムの子は、「トランザクション中は読み取られるデータが変更されないようにトランザクションを実行できるべきである」というトランザクションの分離原則に違反しています。
ファントムの発生を回避できるように、InnoDB
では通常、インデックス行ロックとギャップロックを組み合わせたネクストキーロックと呼ばれるアルゴリズムが使用されます。 InnoDB
は、テーブルインデックスを検索またはスキャンするときに、生成されたインデックスレコード上に共有ロックまたは排他ロックを設定するという方法で、行レベルロックを実行します。 したがって、行レベルロックは、実際にはインデックスレコードロックです。 さらに、あるインデックスレコードに対するネクストキーロックによって、そのインデックスレコードの前の「ギャップ」も影響を受けます。 つまり、ネクストキーロックは、インデックスレコードロックと、そのインデックスレコードの前のギャップに対するギャップロックとを組み合わせたものです。 あるセッションがインデックス内のレコード R
上に共有ロックまたは排他ロックを持っている場合は、別のセッションがインデックスの順番で R
の直前にあるギャップに新しいインデックスレコードを挿入できません。
InnoDB
はインデックスをスキャンするときに、インデックス内の最後のレコードのあとのギャップをロックすることもできます。 前述の例では、まさにそれが行われています。id
が 100 よりも大きいテーブルへの挿入が回避されるように、InnoDB
で設定されたロックには、id
値 102 のあとのギャップに対するロックが含まれています。
ネクストキーロックを使用すると、アプリケーションに一意性チェックを実装できます。共有モードでデータを読み取るときに、挿入される行の重複が見られなければ、行を安全に挿入でき、読み取り中に後続の行に設定されたネクストキーロックによって、任意のユーザーによる重複行の挿入が回避されることを確認できます。 したがって、ネクストキーロックを使用すれば、テーブル内に存在しないものも「ロック」できます。
ギャップロックは、セクション15.7.1「InnoDB ロック」で説明した方法で無効にすることができます。 ギャップロックが無効になっていると、ほかのセッションが新しい行をギャップに挿入できるため、ファントムの問題が発生する可能性があります。