- データベース
- 技術 💻
トランザクション分離レベルの違いをわかりやすく解説
トランザクション分離レベルとは、「同時に動いている他のトランザクションの変更を、どこまで見えないようにするか」を決める設定です。分離を強くするほどデータの整合性は高まりますが、同時に処理できる量(並行性)は下がります。その間のどこを選ぶかを、4つのレベルで指定します。
この記事では、4つの分離レベルの違いを具体例で説明し、MySQLとPostgreSQLでデフォルトが違うことの影響、そして分離レベルを上げても防げない「ロストアップデート」の対策まで解説します。
まず、同時実行で起きる3つの現象
分離レベルは、次の3つの現象を「防ぐかどうか」で定義されています。
ダーティリード(Dirty Read)
他のトランザクションが、まだコミットしていない変更が見えてしまう現象です。相手がロールバックすると、存在しなかったはずのデータを読んだことになります。
反復不能読み取り(Non-Repeatable Read)
同じトランザクションの中で、同じ行を2回読むと値が変わっている現象です。
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 1000
-- この間に、別のトランザクションが更新してコミット
SELECT balance FROM accounts WHERE id = 1; -- 800
COMMIT;
ファントムリード(Phantom Read)
同じ条件で2回検索すると、行が増えている(または減っている)現象です。別のトランザクションが条件に合う行をINSERTしてコミットしたために起きます。集計処理の途中で起きると、合計と明細が合わなくなります。
4つの分離レベルの違い
SQL標準では、4つの分離レベルが定義されています。
| 分離レベル | ダーティリード | 反復不能読み取り | ファントムリード |
|---|---|---|---|
| READ UNCOMMITTED | 起きうる | 起きうる | 起きうる |
| READ COMMITTED | 防ぐ | 起きうる | 起きうる |
| REPEATABLE READ | 防ぐ | 防ぐ | 起きうる(※) |
| SERIALIZABLE | 防ぐ | 防ぐ | 防ぐ |
※SQL標準の定義。実際のDBでは、REPEATABLE READでもファントムを防ぐ実装があります(後述)。
- READ UNCOMMITTED — 最も緩い。未コミットの変更も見える。実務で使うことはほぼありません
- READ COMMITTED — コミット済みの最新データを読む。多くのDBのデフォルト
- REPEATABLE READ — トランザクション中に読んだ行は、何度読んでも同じ値
- SERIALIZABLE — 同時に走ったトランザクションを、1つずつ順番に実行したのと同じ結果に保証する。最も厳格
DBごとのデフォルトの違い
ここが実務で最も効く知識です。主要なDBで、デフォルトの分離レベルが違います。
| DB | デフォルト |
|---|---|
| PostgreSQL | READ COMMITTED |
| Oracle Database | READ COMMITTED |
| SQL Server | READ COMMITTED |
| MySQL(InnoDB) | REPEATABLE READ |
つまり、冒頭の「同じSELECTの結果が変わる」現象は、PostgreSQLのデフォルトでは起きますが、MySQLのデフォルトでは起きません。同じアプリケーションコードでも、DBを移行しただけで挙動が変わることがあるわけです。
同じ名前でも、中身が同じとは限らない
さらに注意が必要なのは、同じ名前の分離レベルでも、DBごとに実装が違うことです。
- PostgreSQLのREPEATABLE READ — トランザクション開始時のスナップショットを使うため、ファントムも起きません。その代わり、同じ行を同時に更新しようとすると、後から来た側がエラー(シリアライゼーション失敗)になり、リトライが必要です
- MySQL(InnoDB)のREPEATABLE READ — 通常のSELECTはスナップショットで読み、ロックを伴う読み取り(SELECT ... FOR UPDATE等)ではギャップロックを使ってファントムを抑えます
- PostgreSQLのREAD UNCOMMITTED — 指定しても、READ COMMITTEDと同じ動作になります
- Oracle — READ COMMITTEDとSERIALIZABLEの2つ(と読み取り専用)のみ
分離レベルの名前は「最低限の保証のラベル」と捉え、実際の挙動は使っているDBのドキュメントで確認するのが確実です。
分離レベルを確認・変更する
-- 現在の分離レベルを確認
-- MySQL
SELECT @@transaction_isolation;
-- PostgreSQL
SHOW transaction_isolation;
-- このトランザクションだけ変更(PostgreSQL)
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
アプリケーションからは、フレームワークの設定(例:Springの @Transactional(isolation = ...))で指定することもできます。
分離レベルでは防げない「ロストアップデート」
実務で一番被害が大きいのが、この現象です。
2人が同時に「残高を読む→計算する→書き戻す」を行うと、後から書き込んだ方が、先の更新を上書きして消してしまいます。
Aさん: 残高1000を読む → 100引いて900を書き込む
Bさん: 残高1000を読む → 200引いて800を書き込む(Aさんの更新が消える)
結果: 800(正しくは700)
READ COMMITTEDでは防げず、DBや書き方によってはREPEATABLE READでも防げません。アプリケーション側で明示的に守る必要があります。
対策1:悲観ロック(SELECT ... FOR UPDATE)
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 行をロック
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;
読んだ時点で行をロックするので、他のトランザクションはコミットまで待たされます。確実ですが、待ちが発生します。
対策2:楽観ロック(バージョン列)
UPDATE accounts
SET balance = 900, version = version + 1
WHERE id = 1 AND version = 5; -- 読んだときのバージョン
更新件数が0件なら、誰かが先に更新したということなので、読み直してやり直します。競合が少ない処理では、待ちが発生しないぶん効率的です。多くのORMやフレームワークに仕組みが用意されています。
対策3:計算をSQLの中で完結させる
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
「読んでから書く」をやめ、1文で更新すれば、この形のロストアップデートは起きません。
どの分離レベルを選べばいいか
- 基本はDBのデフォルトのままにし、そのデフォルトが何かをチームで把握しておく
- 「読んでから書き戻す」処理は、分離レベルに頼らず、悲観ロック・楽観ロック・1文での更新のどれかで守る
- 複数のSELECTで帳票や集計を作るときは、読み取り専用トランザクション(REPEATABLE READ等)で囲み、同じ時点のデータを見るようにする
- SERIALIZABLEを使う場合は、失敗時のリトライ処理が必須。リトライなしで上げると、エラーが増えたように見えます
よくある質問
Q. 全部SERIALIZABLEにすれば安全ですか? A. 整合性は最も強くなりますが、両立できないトランザクションはエラーになるため、アプリ側にリトライ処理が必須です。並行性も下がるため、必要な処理に限って使うのが一般的です。
Q. MySQLからPostgreSQLに移行するときの注意点は? A. デフォルトがREPEATABLE READからREAD COMMITTEDに変わります。同じトランザクション内で同じデータを複数回読む処理は、結果が変わらないか確認してください。
Q. トランザクションを長く開いておくと何が問題ですか? A. ロックを持ち続けて他の処理を待たせるほか、DBが古いデータを掃除できなくなり、全体の性能が低下することがあります。トランザクション内で外部APIを呼んだり、ユーザーの入力を待ったりするのは避けましょう。
Q. 読み取りだけの処理にもトランザクションは必要ですか? A. 複数のSELECTの結果を組み合わせる場合は、同じ時点のデータを見るために有効です。
まとめ
- 分離レベルは、ダーティリード・反復不能読み取り・ファントムリードをどこまで防ぐかで決まる
- PostgreSQL・Oracle・SQL ServerはREAD COMMITTED、MySQLはREPEATABLE READがデフォルト
- 同じ名前でもDBごとに実装が違う。挙動はドキュメントで確認する
- ロストアップデートは分離レベルでは防げない。悲観ロック・楽観ロック・1文での更新で守る
株式会社RJCが携わる保険・金融・決済のシステムでは、「同時に2つの処理が走ったらどうなるか」が設計レビューの定番の論点です。こうした知識を、チームで共有しながら開発しています。
▶ RJC丸わかり https://www.r-jc.jp/guide/?mediacode=02-00001-003
▶ エントリーはこちら(1〜2分で完了します) https://www.r-jc.jp/recruitment/?mediacode=02-00001-003
株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。