データベースシステムにおけるトランザクションは、複数の操作を論理的な単一の単位として扱い、データの整合性と信頼性を保証するために不可欠です。この信頼性を支える中心的な概念の一つが「トランザクション分離レベル」です。複数のトランザクションが同時に動作する環境下で、どのように相互作用を制限し、データの一貫性を保つか、その制御メカニズムを本稿では解説します。
トランザクションのACID特性
トランザクションは、以下の4つの主要な特性(ACID特性)によってその信頼性が保証されます。
- 原子性 (Atomicity): トランザクション内のすべての操作は、完全に実行されるか、全く実行されないかのいずれかです。一部だけ実行されることはありません。何らかの理由でトランザクションが途中で失敗した場合、システムはすべての変更を元に戻し(ロールバック)、トランザクション開始前の状態に復元します。
- 一貫性 (Consistency): トランザクションが開始される前と完了した後で、データベースは常に一貫した状態にあります。つまり、定義されたすべての整合性制約(外部キー、ユニーク制約など)が満たされている状態が維持されます。
- 独立性 (Isolation): 複数のトランザクションが同時に実行される場合でも、それぞれのトランザクションは他のトランザクションの影響を受けていないかのように独立して実行されます。これにより、ユーザーはトランザクションの並行処理による複雑さを意識する必要がなくなります。
- 永続性 (Durability): 一度コミットされたトランザクションによる変更は、システム障害が発生しても失われることなく、永続的にデータベースに保存されます。
並行処理における潜在的な問題
独立性の保証が不十分な場合、データベースの並行処理において以下のような問題が発生する可能性があります。
- ダーティリード (Dirty Read): あるトランザクションAが、まだコミットされていない別のトランザクションBによる変更データ(ダーティデータ)を読み取ってしまう現象です。もしトランザクションBがその後ロールバックした場合、トランザクションAは存在しないデータを基に処理を進めてしまうことになります。
- ノンリピータブルリード (Non-repeatable Read): あるトランザクションAが同じデータを複数回読み取った際に、その間に別のトランザクションBがそのデータを更新(または削除)しコミットしたため、読み取るたびに結果が異なる現象です。トランザクションAは一貫性のない結果を受け取ることになります。
- ファントムリード (Phantom Read): あるトランザクションAが特定の条件を満たす行の集合を複数回検索した際に、その間に別のトランザクションBがその条件を満たす新しい行を挿入(または既存の行を削除)しコミットしたため、2回目の検索で行の数が変わってしまう現象です。これはノンリピータブルリードが行の内容の変更に焦点を当てるのに対し、ファントムリードは行の集合自体の変化に焦点を当てます。
ノンリピータブルリードとファントムリードは混同されがちですが、以下のように区別できます。
- ノンリピータブルリードの焦点: 既存データの更新。同じ条件で
SELECT文を複数回実行した際に、すでに読み取った行の値が異なってしまう。 - ファントムリードの焦点: データの挿入または削除。同じ条件で
SELECT文を複数回実行した際に、結果として返される行の数が異なってしまう。
標準SQLのトランザクション分離レベル
SQL標準では、これらの並行性問題をどの程度許容するかによって、以下の4つの分離レベルを定義しています。
| 分離レベル | ダーティリード | ノンリピータブルリード | ファントムリード |
|---|---|---|---|
| Read Uncommitted (読み取り未コミット) | 発生する | 発生する | 発生する |
| Read Committed (読み取りコミット済み) | 発生しない | 発生する | 発生する |
| Repeatable Read (反復可能読み取り) | 発生しない | 発生しない | 発生する(標準SQLでは) |
| Serializable (直列化可能) | 発生しない | 発生しない | 発生しない |
MySQLにおけるトランザクション分離レベルとMVCC
多くのデータベースシステムでは、デフォルトの分離レベルとしてRead Committedを採用していますが、MySQL (InnoDBストレージエンジン) はデフォルトでRepeatable Readを採用しています。
SQL標準のRepeatable Readはファントムリードを許容するとされていますが、MySQLのInnoDBでは、ギャップロック (Gap Lock) というメカニズムを導入することで、Repeatable Readレベルでもファントムリードを防いでいます。ギャップロックは、インデックスレコード間の「ギャップ(間隙)」をロックすることで、その範囲内への新しい行の挿入を防ぎます。
また、MySQLのInnoDBは多版型同時実行制御 (Multi-Version Concurrency Control, MVCC) を利用して、ダーティリードやノンリピータブルリードの問題に対処しています。
- Read CommittedレベルでのMVCC: 各SELECT文は、その文が開始された時点までにコミットされたデータの最新バージョンを参照します。これによりダーティリードは防がれますが、同じトランザクション内でSELECT文を繰り返し実行した場合、その間に他のトランザクションがコミットした変更があれば、異なる結果が返される可能性があります(ノンリピータブルリードが発生)。
- Repeatable ReadレベルでのMVCC: トランザクション内の最初のSELECT文が実行された時点のデータスナップショットが維持されます。以降のSELECT文はすべて、このスナップショットを参照するため、トランザクションが終了するまで同じ結果が保証されます(ノンリピータブルリードが発生しない)。前述のギャップロックと組み合わせることでファントムリードも防がれます。
この特性から、Read CommittedはRepeatable Readと比較して一般的に並行性が高いとされます。なぜなら、Repeatable Readはデータのスナップショットを維持したり、ギャップロックを保持したりするため、ロックの粒度が粗くなり、ロック競合(ロック待ち)が発生しやすくなるためです。本番環境では、アプリケーションの要件とパフォーマンスのバランスを考慮し、Read Committedを選択することも珍しくありません。
その他の主要データベース
Oracle DatabaseやMicrosoft SQL Serverでは、デフォルトの分離レベルとしてRead Committedが採用されています。これらのデータベースのRead Committed実装もMVCCを活用しており、ダーティリードは防止しますが、ノンリピータブルリードやファントムリードは発生する可能性があります。
分離レベルの確認と設定
MySQLでは、以下のSQLコマンドを使用して現在の分離レベルを確認したり、設定したりできます。
現在のセッションまたはグローバルの隔離レベルを確認する:
SELECT @@TRANSACTION_ISOLATION;
-- または、古いバージョンや互換性のために
SELECT @@tx_isolation;
グローバルな分離レベルを設定する(データベースの再起動後も持続):
SET GLOBAL TRANSACTION ISOLATION LEVEL = 'READ COMMITTED';
セッションの分離レベルを設定する(現在の接続にのみ適用):
SET SESSION TRANSACTION ISOLATION LEVEL = 'REPEATABLE READ';
システム変数を一覧表示し、分離レベル関連の情報を確認する:
SHOW GLOBAL VARIABLES LIKE '%isolation%';
WAL (Write-Ahead Logging) の役割
データベースの永続性(Durability)とパフォーマンスを向上させるために、多くのデータベースシステムではWrite-Ahead Logging (WAL) という仕組みを採用しています。
トランザクションがコミットされた際、データファイルに直接変更を書き込むのではなく、まずRedoログ(トランザクションログ)にその変更操作を記録します。Redoログへの書き込みは通常、ディスクの末尾への追記(シーケンシャルI/O)であるため、データファイルへのランダムI/Oよりもはるかに高速です。これにより、ユーザーはコミットの完了を速やかに確認でき、データベースの応答性が向上します。
実際にデータファイルが更新されるのは、データベースのバックグラウンドプロセスがRedoログを読み取り、メモリ上のダーティページをディスクに書き出す(フラッシュする)際です。この非同期的な書き込みは、I/Oの統合や最適化を可能にし、システム全体のパフォーマンスの安定に貢献します。