MySQLにおけるファントムリード
作成日時:2025-01-26
更新日時:2025-11-01
※あくまで自分の解釈。正しいとは限らない。
なぜREPEATABLE READでファントムリードが発生しないか
MVCCとギャップロックのおかげ。
- MVCC:スナップショット(Undoログから自身のトランザクションIDより古いデータを取得)を参照するため
- ギャップロックとネクストキーロックにより、データを挿入/削除させないため
パターン1:
1. トランザクションA:SELECT * FROM tbl;
2. トランザクションB:行挿入してコミット。
3. トランザクションA:再度SELECT * FROM tbl;
2で挿入したデータは見えない
ファントムリードは発生しない。
3のSELECTは1の時のスナップショットを参照しているから。
パターン2:
1. トランザクションA:SELECT * FROM tbl WHERE (インデックスを使用した範囲検索) FOR UPDATE;
2. トランザクションB:行挿入はギャップロックで失敗
登録できないのでファントムリードは発生しない。FOR UPDATE
15.7.2.4 読取りのロックに下記の記載がある。
一貫性読み取りでは、読み取られたビュー内に存在するレコードに設定されたロックはすべて無視されます。 (古いバージョンのレコードはロックできません。レコードのインメモリーコピー上の Undo ログに適用することで、再構築されます。)
以下、推察。
- REPEATABLE READはスナップショットを見ます
- トランザクション開始時点のデータを一貫して読み取ります
- 対象データがロックされていても、スナップショットは関係ないので(スナップショットの)データを読むことは出来ます
- 古いバージョンのレコードはロックできないので、ロックするには最新データを読まねばならない
非ユニークインデックスのロック
WHERE id = xxx FOR UPDATEにおいて、
idがユニークインデックスなら行ロック。
idが非ユニークインデックスなら同じキー値を持つレコードが複数存在する可能性があるため、ギャップロックorネクストキーロック。
非インデックスをキーにUPDATEすると、実質テーブルロックになる可能性がある。
検索にせよ更新にせよ、何らかの条件にしている項目は、パフォーマンスが許す限りインデックスを貼るべきか。
検索パフォーマンスの向上や、ロック範囲の縮小につながる。
比較
- PostgreSQL: 追記型アーキテクチャ。FOR UPDATEをつけてもスナップショットを参照し続けるから、ファントムリードは発生しない。
- MySQL: FOR UPDATEをつければ最新の情報を取得するが、ギャップ/ネクストキーロックによって登録できないからファントムリードは発生しない。
差し込み
-- A: トランザクション開始
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM table WHERE id BETWEEN 1 AND 10;
-- 結果: 5は存在しない
-- B: (別のセッションで)
BEGIN;
INSERT INTO table (id, ...) VALUES (5, ...);
COMMIT;
-- A: 更新を実行
UPDATE table SET column = value WHERE id BETWEEN 1 AND 10;
-- REPEATABLE READにおいて5は更新されないメモ
PostgreSQLの範囲ロックはMySQLと違って、実際にレコードが無くともその範囲でロックされる。
なんだか、PostgreSQLは論理、MySQLは物理で管理している感じがする。
ロックの考え方や、FOR UPDATE、クラスター/非クラスターなど。追記型とか。
参考
- MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.1 InnoDB ロック
- MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.2.1 トランザクション分離レベル
- MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.2.4 読取りのロック
- MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.4 ファントム行
- MySQL インデックス、MVCCの仕組み #MySQL - Qiita
- 漢(オトコ)のコンピュータ道: InnoDBのREPEATABLE READにおけるLocking Readについての注意点