複数のユーザーが同時にデータを更新するシステムでは、「コミット前のデータを別のトランザクションが読んでしまう」「2回読んだら値が変わっていた」といった不整合が起きます。これを制御するのがトランザクションの「分離レベル」です。この記事では、トランザクションが持つACID特性と、SQLで定義された4つの分離レベルの挙動を整理します。ダーティリード・ノンリピータブルリード・ファントムリードの違いと、MySQLのデフォルト(REPEATABLE READ)で何が防げるかを表で確認できます。
このシナリオで考える
口座Aから口座Bへ1万円を振り込む処理を例として使います。残高を減らすUPDATEと残高を増やすUPDATEの2つで1件の振込が完結します。この2つが「必ず両方成功、または両方失敗」で終わるよう保証する仕組みがトランザクションです。
CREATE TABLE accounts (
id INT PRIMARY KEY,
name VARCHAR(50),
balance INT NOT NULL
);
INSERT INTO accounts VALUES
(1, '山田', 50000),
(2, '鈴木', 30000);
振込処理の骨格は次のようになります。
START TRANSACTION;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
COMMIT;
-- エラーが起きた場合
ROLLBACK;
ACIDとは:トランザクションの4つの特性
ACIDは、信頼できるトランザクション処理に必要な4つの特性の頭文字です。
| 特性 | 読み | 意味 |
|---|---|---|
| Atomicity | 原子性 | すべて成功、またはすべて失敗。途中で止まっても中間状態を残さない |
| Consistency | 一貫性 | トランザクション前後でNOT NULL・外部キーなどのDB制約を満たした状態を保つ |
| Isolation | 分離性 | 並列実行中のトランザクションが互いに干渉しない(どの程度かは分離レベルで決まる) |
| Durability | 永続性 | COMMITしたデータはシステム障害後も失われない |
4つのうち「分離性をどの程度保証するか」を調整するのが分離レベルです。分離性を高めるほど安全になりますが、ロックの競合が増えて同時実行性が下がります。
3種類の異常現象:何が問題になるのか
分離レベルを理解するには、まず3つの異常現象を把握しておく必要があります。
ダーティリード(dirty read)
別のトランザクションがUPDATEしたが、まだCOMMITしていないデータを読んでしまう現象です。相手がROLLBACKすると、読んだ値は「存在しなかったことになった」データになります。
ノンリピータブルリード(non-repeatable read)
同一トランザクション内で同じ行を2回SELECTしたとき、間に別トランザクションがCOMMITしたため値が変わってしまう現象です。「同じ行を読んだのに値が違う」という状況がこれにあたります。
ファントムリード(phantom read)
同一トランザクション内で同じ条件のSELECTを2回実行したとき、間に別トランザクションがINSERT/DELETEしてCOMMITしたため、返ってくる行数が変わってしまう現象です。幽霊(phantom)のように行が現れたり消えたりする点から名付けられています。
4つの分離レベルと防げる異常現象
SQLには4段階の分離レベルが定義されています。○は発生する可能性あり、✕は防げることを示します。
| 分離レベル | ダーティリード | ノンリピータブルリード | ファントムリード |
|---|---|---|---|
| READ UNCOMMITTED | ○ | ○ | ○ |
| READ COMMITTED | ✕ | ○ | ○ |
| REPEATABLE READ(MySQL既定) | ✕ | ✕ | △ |
| SERIALIZABLE | ✕ | ✕ | ✕ |
REPEATABLE READのファントムリードが「△」なのは、MySQLのInnoDBエンジンが「ギャップロック」という仕組みで通常のケースでは防いでいるためです。ロッキングリード(SELECT ... FOR UPDATE)を使う場面では挙動が変わる場合があります。
READ UNCOMMITTED
最も緩い設定です。COMMITされていないデータも読めるため、ダーティリードが発生します。実務ではほぼ使われません。
READ COMMITTED
COMMITされたデータのみを読みます。PostgreSQLのデフォルトでもあります。ダーティリードは防げますが、SELECTのたびに最新スナップショットを取得するため、ノンリピータブルリードが起きます。
REPEATABLE READ(MySQLのデフォルト)
トランザクション開始時点のスナップショットを読み続けます。同じ行を何度SELECTしても同じ値が返ってくるため、ノンリピータブルリードを防ぎます。InnoDBのギャップロックによりファントムリードも通常は防ぎます。
SERIALIZABLE
最も強い設定です。すべてのSELECTに共有ロックをかけ、トランザクションを直列実行したのと同等の結果を保証します。デッドロックのリスクが上がり、スループットが低下するため、特定の要件がある場合にのみ使います。
MySQLでの確認と変更方法
-- 現在の分離レベルを確認
SHOW VARIABLES LIKE 'transaction_isolation';
+------------------------+-----------------+
| Variable_name | Value |
+------------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+------------------------+-----------------+
-- セッション単位で変更(次のトランザクションから適用)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 次の1トランザクションだけに適用
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
-- ...
COMMIT;
グローバルに変更する場合は SET GLOBAL TRANSACTION ISOLATION LEVEL ... を使います。my.cnf に transaction-isolation = READ-COMMITTED と書けば再起動後も維持されます。
よくある落とし穴と注意点
- 「REPEATABLE READなのに別セッションのINSERT結果が見えた」:
SELECT ... FOR UPDATEやSELECT ... LOCK IN SHARE MODEはスナップショットではなく最新行を読みます。ロッキングリードを使うと通常のREPEATABLE READとは挙動が変わります。 - 「READ COMMITTEDで集計値が毎回変わった」: バッチ処理で集計クエリを複数回実行する場合、READ COMMITTEDでは実行のたびに最新スナップショットを取得します。途中で他のトランザクションがCOMMITすると集計値がずれます。集計の一貫性が必要な場合はREPEATABLE READを選びましょう。
- 「分離レベルを上げれば安全」とは限らない: SERIALIZABLEは安全ですが、ロック競合が増えデッドロックが発生しやすくなります。多くのWebアプリケーションではMySQLのデフォルト(REPEATABLE READ)で十分です。
まとめ
ACIDのうち「分離性(Isolation)」をどこまで保証するかを制御するのが分離レベルです。MySQLのデフォルトはREPEATABLE READで、ダーティリードとノンリピータブルリードを防ぎ、ファントムリードもInnoDBのギャップロックでほぼ防ぎます。変更する際は性能とのトレードオフを意識しましょう。
参考リンク
- MySQL 8.0 リファレンス: トランザクション分離レベル(日本語)
- MySQL 8.0 Reference Manual: InnoDB Transaction Isolation Levels
アイキャッチ画像: Photo by Shubham Dhage on Unsplash

