MySQLのアイソレーションレベル入門:REPEATABLE READで「見えない行」が起きる理由

MySQL分離レベル超入門と書かれた黒板キャッチアップ画像 技術解説

MySQLでトランザクションを使っていると、「別の接続では登録済みなのに、この接続のSELECTでは見えない」「ロック待ちが起きる条件が分からない」といった疑問に出会うことがあります。

その背景にあるのが、トランザクションのアイソレーションレベルです。アイソレーションレベルは、複数のトランザクションが同時に動くときに、結果の一貫性、再現性、ロックの負荷をどの程度重視するかを調整する設定です。

InnoDBのデフォルトはREPEATABLE READ

MySQL公式ドキュメントでは、InnoDBがSQL:1992で定義された4つのアイソレーションレベル、READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLEを提供していると説明しています。

このうち、InnoDBのデフォルトはREPEATABLE READです。何も設定していないMySQL環境では、まずこの挙動を前提に考える必要があります。

アイソレーションレベル 大まかな特徴 実務での見方
READ UNCOMMITTED 未コミットの変更を読み得る 通常の業務処理では慎重に扱う
READ COMMITTED 各SELECTで新しいコミット済みスナップショットを読む 最新に近い読み取りを重視するときに候補になる
REPEATABLE READ 同じトランザクション内の一貫した読み取りを重視する InnoDBのデフォルトとして、まず理解したい設定
SERIALIZABLE より厳格な分離を行う 特殊な要件や調査用途で検討する

アイソレーションレベルは、次のようにトランザクション単位で指定できます。サーバー全体のデフォルトを変える方法もありますが、実運用では影響範囲を把握してから変更する必要があります。

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- SQL statements
COMMIT;

REPEATABLE READで「見えない行」が起きる理由

REPEATABLE READでは、同じトランザクション内の通常のSELECTは、最初の一貫読み取りで作られたスナップショットを読みます。別のセッションがその後に行を追加してコミットしても、現在のトランザクションが同じスナップショットを見続けるため、その行がすぐには見えないことがあります。

接続Bが追加してコミットした行が、接続Aの2回目SELECTでは最初のスナップショットにより見えないことを示した図
REPEATABLE READの通常のSELECTでは、同じトランザクション内で最初に作られたスナップショットを読み続けます。

たとえば、接続Aがトランザクションを開始してSELECTしたあと、接続Bが新しい行をINSERTしてCOMMITしたとします。このとき接続Aがもう一度通常のSELECTを実行しても、接続Aのスナップショットが進んでいなければ、新しい行は見えません。

これは、MySQLが壊れているわけではありません。同じトランザクション内で読み取り結果を安定させるための挙動です。接続Aで最新の状態を見たい場合は、いったんCOMMITして新しいトランザクションで読み直す、または要件に応じてREAD COMMITTEDやロック読みを検討します。

READ COMMITTEDではSELECTごとに見え方が変わる

READ COMMITTEDでは、同じトランザクション内であっても、一貫読み取りのたびに新しいスナップショットが作られます。そのため、別のセッションがコミットした変更は、次の通常のSELECTで見える可能性があります。

READ COMMITTEDでは2回目のSELECTで追加行が見え、REPEATABLE READでは同じスナップショットによりまだ見えないことを日本語列名で比較した図
READ COMMITTEDではSELECTごとに新しいスナップショットを読みます。一方、REPEATABLE READでは同じトランザクション内の通常SELECTが一貫したスナップショットを読み続けます。

この違いは、レポート処理や管理画面の表示では便利に見えることがあります。一方で、同じトランザクション内で同じ条件のSELECTを繰り返したときに結果が変わる可能性があるため、処理の前提を明確にする必要があります。

また、InnoDBのREAD COMMITTEDでは、ロック読み、UPDATEDELETEにおいて、通常はギャップではなくインデックスレコードをロックします。ただし、外部キー制約や重複キーの確認ではギャップロックが使われる場合があります。ロック待ちやデッドロックを調査するときは、この差も確認点になります。

普通のSELECTとロック読みは分けて考える

ここで注意したいのは、通常のSELECTと、SELECT ... FOR UPDATESELECT ... FOR SHAREのようなロック読みを同じものとして扱わないことです。

通常のSELECTは一貫読み取りであり、InnoDBのMVCCにより、ある時点のスナップショットを読みます。これ自体はテーブルにロックを設定しないため、他のセッションは同じテーブルを変更できます。

普通のSELECTはスナップショットを見て書き込みを妨げず、FOR UPDATEやUPDATEでは行とすき間のロックにより追加が待ちになることを示した図
通常のSELECTはスナップショットを読みます。SELECT ... FOR UPDATEUPDATEなどは現在の状態を使い、条件によって行や範囲をロックします。

一方、SELECT ... FOR UPDATESELECT ... FOR SHAREUPDATEDELETEは、最新の状態を使ってロックを取ります。REPEATABLE READでは、ユニークインデックスをユニーク条件で検索する場合は見つかったインデックスレコードだけをロックしますが、範囲検索などではギャップロックやネクストキーロックを使い、他のセッションによる範囲内への挿入を防ぐことがあります。

そのため、同じREPEATABLE READトランザクションの中で、通常のSELECTとロック読みや更新系SQLを混ぜると、「通常のSELECTで見えていた状態」と「更新系SQLが扱う現在の状態」がずれて見える場合があります。公式ドキュメントも、この組み合わせは解釈が難しくなると説明しています。

図解例題1:コミットされた行が見えない理由を説明する

例題:接続AでSTART TRANSACTION後にSELECT * FROM orders;を実行しました。その後、接続Bがordersに新しい行を追加してCOMMITしました。接続Aでもう一度同じSELECTを実行しましたが、新しい行が表示されません。まず疑うべきことは何でしょうか。

例題1で接続Bが追加してコミットした行が接続Aの2回目SELECTではまだ見えず、AのCOMMIT後に見える流れを示した図
接続Aの最初のSELECTでスナップショットが固定されると、接続Bが追加してCOMMITした行は、接続Aの同じトランザクション内ではまだ見えません。

考え方:接続AのアイソレーションレベルがREPEATABLE READで、通常のSELECTを使っている可能性があります。この場合、接続Aは最初の読み取りで作られたスナップショットを読み続けるため、接続Bのコミット後の行がすぐには見えないことがあります。接続AでCOMMITして読み直すと、スナップショットが進み、新しい行が見えるようになります。

コミット済み行が見えないときに分離レベル、読み方、トランザクション状態、COMMIT後の再読込を確認する順番を示した図
「見えない」現象は、分離レベル、通常のSELECTかどうか、トランザクションが続いているか、COMMIT後に見えるかの順に確認すると整理しやすくなります。

図解例題2:ロック待ちを減らしたいときの確認点

例題:あるバッチ処理で、範囲条件を使ったUPDATEが多く、同時に別セッションから同じ範囲へINSERTする処理がロック待ちになっています。アイソレーションレベルを変える前に、何を確認すべきでしょうか。

範囲UPDATEが年齢20から29の行とすき間をロックし、年齢25のINSERTが待ちになることを示した図
範囲条件のUPDATEでは、対象行だけでなく、条件やインデックスによっては範囲内のすき間もロック対象になり、同じ範囲へのINSERTが待つことがあります。

考え方:まず、どのSQLがどのインデックスを使い、どの範囲をロックしているかを確認します。REPEATABLE READでは、範囲検索でギャップロックやネクストキーロックが関係する場合があります。READ COMMITTEDではギャップロックの扱いが変わりますが、外部キー制約や重複キー確認など例外もあります。設定変更だけでなく、インデックス設計、検索条件、更新順序、トランザクション時間も合わせて見直す必要があります。

ロック待ち対策でSQL、インデックス、範囲、時間、外部キーや重複キーを確認してからREAD COMMITTEDを検証する図
アイソレーションレベルを変える前に、SQL、実際に使ったインデックス、ロックされた範囲、トランザクション時間、外部キーや重複キー確認の影響を切り分けます。

どのレベルを選ぶべきか

結論として、アイソレーションレベルは「これが常に正解」と決めるものではありません。まずはInnoDBのデフォルトであるREPEATABLE READを理解し、そのうえでアプリケーションの要件に合わせて検討します。

MySQLの分離レベルを読み取りの安定、最新に近い表示、ロック待ちの観点で選ぶ考え方を示した図
アイソレーションレベルは、読み取りの新しさ、一貫性、ロックの負荷、業務上避けたい不整合を整理して選びます。

実務では、次のような観点で切り分けると判断しやすくなります。

  • 同じトランザクション内で読み取り結果を安定させたいなら、REPEATABLE READの挙動を前提に設計する
  • 各SELECTでできるだけ新しいコミット済みデータを読みたいなら、READ COMMITTEDを検討する
  • 範囲更新やロック待ちが問題になっているなら、アイソレーションレベルだけでなく、インデックス、検索条件、トランザクション時間を確認する
  • 厳格な直列化が必要な処理では、SERIALIZABLEの影響を検証する

本番環境で変更する場合は、アプリケーション全体の前提が変わる可能性があります。特に、同じトランザクション内で複数回読み取る処理、範囲更新、在庫や残高のような整合性が重要な処理では、テスト環境で再現ケースを作って確認することが重要です。

参考情報

まとめ

MySQLのアイソレーションレベルを理解するときは、まずInnoDBのデフォルトがREPEATABLE READであることを押さえます。そのうえで、通常のSELECTが読むスナップショット、READ COMMITTEDでの読み取りの新しさ、ロック読みや更新系SQLのロックの取り方を分けて考えることが大切です。

「コミットされたはずの行が見えない」「範囲更新でロック待ちになる」といった現象は、アイソレーションレベルと読み取り方法を確認すると整理しやすくなります。設定を変える前に、どのSQLが、どのトランザクションで、どの時点のデータを見ているのかを小さな再現例で確かめると、原因を追いやすくなります。

タイトルとURLをコピーしました