MySQL のトランザクション分離レベル
トランザクション分離レベルは、複数のトランザクションが同時に実行される際に、互いの変更がどの程度見えるかを制御する仕組みだ。分離レベルが高いほどデータの一貫性は保たれるが、並行性やパフォーマンスとのトレードオフが生じる。MySQL の InnoDB では 4 つの分離レベルが用意されており、要件に応じた使い分けが求められる。
4 つの分離レベル
SQL 標準では 4 段階の分離レベルが定義されている。InnoDB はこれらすべてをサポートしており、デフォルトは REPEATABLE READ だ。
| 分離レベル | 特徴 |
|---|---|
| READ UNCOMMITTED | 他のトランザクションの未コミットデータが見える |
| READ COMMITTED | 他のトランザクションのコミット済みデータのみ見える |
| REPEATABLE READ | トランザクション開始時点のスナップショットを参照する |
| SERIALIZABLE | すべての読み取りに暗黙の共有ロックがかかる |
分離レベルが低いほど他のトランザクションの影響を受けやすく、高いほど安全だが制約も強くなる。この 4 つの違いを理解するには、それぞれで防げる「読み取り異常」を把握しておく必要がある。
読み取り異常の種類
トランザクション間で発生する読み取り異常には 3 つの代表的なパターンがある。
他のトランザクションがまだコミットしていない変更を読み取ってしまう現象。その変更がロールバックされた場合、存在しないデータを読んだことになる。
同一トランザクション内で同じ行を 2 回読み取った際に、別のトランザクションによる更新の影響で異なる値が返される現象。
同一トランザクション内で同じ条件の範囲検索を 2 回実行した際に、別のトランザクションによる行の挿入・削除の影響で結果行数が変わる現象。
各分離レベルがどの読み取り異常を防ぐかを整理すると、選択の判断材料になる。
| 分離レベル | ダーティリード | ノンリピータブルリード | ファントムリード |
|---|---|---|---|
| READ UNCOMMITTED | 発生する | 発生する | 発生する |
| READ COMMITTED | 防止 | 発生する | 発生する |
| REPEATABLE READ | 防止 | 防止 | InnoDB では防止 |
| SERIALIZABLE | 防止 | 防止 | 防止 |
InnoDB の REPEATABLE READ はネクストキーロックによってファントムリードも防止できるため、SQL 標準の定義よりも強い分離を提供している点が特筆すべきポイントだ。
READ UNCOMMITTED
最も低い分離レベルで、他のトランザクションの未コミットデータまで読み取ってしまう。
-- セッションの分離レベルを変更
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- トランザクション A
BEGIN;
UPDATE products SET price = 500 WHERE id = 1;
-- まだ COMMIT していない
-- トランザクション B(別セッション)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT price FROM products WHERE id = 1;
-- → 500 が見える(未コミットのデータ)
-- トランザクション A がロールバックすると、この 500 は幻のデータになる実用上、このレベルを選択する場面はほとんどない。厳密さが不要な統計集計や、おおよその件数確認といったごく限定的な用途で使われる程度だ。
READ COMMITTED
コミットされたデータのみを読み取る分離レベルで、Oracle や PostgreSQL のデフォルトでもある。ダーティリードは防止されるが、同一トランザクション内で同じクエリを実行しても異なる結果が返る可能性がある。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- トランザクション A
BEGIN;
SELECT price FROM products WHERE id = 1; -- → 100
-- トランザクション B(別セッション)で更新してコミット
UPDATE products SET price = 200 WHERE id = 1;
COMMIT;
-- トランザクション A の続き
SELECT price FROM products WHERE id = 1; -- → 200(変わっている)
COMMIT;READ COMMITTED では、SELECT を実行するたびに最新のスナップショットが生成される。
REPEATABLE READ がトランザクション開始時の 1 つのスナップショットを使い続けるのに対し、READ COMMITTED は文ごとにスナップショットを更新するため、他のトランザクションのコミット済み変更が即座に反映される。
この分離レベルは、InnoDB のギャップロックが無効になるという特性を持つ。ギャップロックによるロック競合がパフォーマンスのボトルネックになっている場合に、READ COMMITTED に変更することで改善できるケースがある。ただし、ファントムリードが発生する可能性がある点は受け入れる必要がある。
REPEATABLE READ(デフォルト)
MySQL InnoDB のデフォルト分離レベルであり、トランザクション開始時点のスナップショットを一貫して参照する。同じクエリを何度実行しても、そのトランザクション内では同じ結果が返る。
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- トランザクション A
BEGIN;
SELECT price FROM products WHERE id = 1; -- → 100
-- トランザクション B(別セッション)で更新してコミット
UPDATE products SET price = 200 WHERE id = 1;
COMMIT;
-- トランザクション A の続き
SELECT price FROM products WHERE id = 1; -- → 100(変わらない)
COMMIT;この一貫性は MVCC(Multi-Version Concurrency Control)によって実現されている。InnoDB は各行の過去のバージョンを UNDO ログに保持しており、トランザクションの開始時点より後に変更されたデータについては、過去のバージョンを参照する仕組みになっている。
読み取りにロックが不要なため、書き込みと読み取りが互いをブロックしない。高い並行性を実現する InnoDB の中核技術といえる。
長時間実行されるトランザクションがあると、UNDO ログが肥大化してパフォーマンスに影響を及ぼす可能性がある。トランザクションは適切な長さに保つべきだ。
SERIALIZABLE
最も厳格な分離レベルで、すべての SELECT 文が暗黙的に SELECT ... FOR SHARE として実行される。読み取りにも共有ロックがかかるため、読み取り中の行に対する他のトランザクションからの更新はブロックされる。
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- トランザクション A
BEGIN;
SELECT * FROM products WHERE id = 1;
-- この行に共有ロックがかかる
-- トランザクション B(別セッション)
BEGIN;
UPDATE products SET price = 200 WHERE id = 1;
-- → トランザクション A がコミットまたはロールバックするまで待機データの完全な一貫性が保証されるが、ロック競合の増加によるパフォーマンス低下が避けられない。金融取引のように整合性が最優先される限定的な場面を除けば、通常は REPEATABLE READ で十分だ。
分離レベルの確認と変更
現在の分離レベルは以下のコマンドで確認できる。
-- 現在のセッションの分離レベル
SELECT @@transaction_isolation;
-- グローバル設定の確認
SELECT @@global.transaction_isolation;
-- セッション単位で変更
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- グローバルで変更(新規セッションに適用)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- my.cnf で恒久的に設定
-- [mysqld]
-- transaction-isolation = READ-COMMITTEDセッション単位での変更は現在の接続にのみ影響し、グローバルでの変更は新しく接続したセッションから適用される。既存のセッションには影響しない点に注意が必要だ。
実務での選び方
多くのアプリケーションでは、デフォルトの REPEATABLE READ のまま運用して問題ない。InnoDB の MVCC とネクストキーロックの組み合わせにより、高い並行性と強い一貫性を両立できるためだ。
まずは REPEATABLE READ(デフォルト)で運用を開始する
ギャップロックによるロック競合が頻発する場合は READ COMMITTED への変更を検討する
変更時はアプリケーションの動作に影響がないかテスト環境で十分に検証する
分離レベルの変更はアプリケーション全体の振る舞いに影響を及ぼすため、安易に変更すべきではない。特に READ COMMITTED に変更する場合は、ステートメントベースのレプリケーションが使えなくなる制約があるほか、ファントムリードへの対策をアプリケーション側で講じる必要が出てくる。トレードオフを十分に理解したうえで判断することが重要だ。












