トランザクション処理
トランザクション処理
🌍 実世界のアナロジー
銀行の振込を想像してください — 口座Aから口座Bに1000を振り込む場合:
- 口座Aから1000を差し引く
- 口座Bに1000を追加する
ステップ1が成功したがステップ2が失敗した場合(システムクラッシュなど)、Aのお金は減ったがBには届いていない — これは大きな問題です。
トランザクションは、この2つのステップを1つの「原子操作」に結び付けます。両方のステップが成功するか(COMMIT)、両方がロールバックされます(ROLLBACK)。「半分完了」の状態は存在しません。
🎯 コア概念
トランザクションとは
トランザクションは、すべて成功するかすべてロールバックされる、分割不可能なSQL操作のシーケンスです。データベースのデータ一貫性を保証するための核心的なメカニズムです。
ACID特性
| 特性 | 英語 | 意味 |
|---|---|---|
| 原子性 | Atomicity | トランザクション内のすべての操作が成功するか、すべてロールバックされる |
| 一貫性 | Consistency | データベースが一貫した状態から別の一貫した状態に移行する |
| 分離性 | Isolation | 同時実行トランザクションが互いに干渉しない(直列実行されているかのように) |
| 永続性 | Durability | コミット後、システムクラッシュが発生してもデータは永続的に保存される |
BEGIN / COMMIT / ROLLBACK
-- トランザクションを開始
BEGIN; -- または START TRANSACTION
-- 一連の操作を実行
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
-- すべて成功、コミット
COMMIT;
-- 問題が発生、ロールバック
ROLLBACK;
SAVEPOINT — セーブポイント
トランザクション内に「チェックポイント」を設定します。特定のセーブポイントまでロールバックでき、それ以前の操作には影響しません。
BEGIN;
INSERT INTO orders VALUES (1020, 101, 1, '2026-06-28', 500.00, 'pending');
SAVEPOINT sp1;
INSERT INTO order_items VALUES (1020, 1, 2, 100.00);
SAVEPOINT sp2;
-- ステップ2にエラーがある場合、sp1までのみロールバック
ROLLBACK TO sp1;
-- ステップ1の操作を保持
COMMIT;
| コマンド | 目的 |
|---|---|
SAVEPOINT name |
セーブポイントを設定 |
ROLLBACK TO name |
指定したセーブポイントまでロールバック(セーブポイント以前の操作は保持) |
RELEASE SAVEPOINT name |
セーブポイントを削除 |
トランザクション分離レベル
複数のトランザクションが同じデータを同時に操作する場合、以下の問題が発生する可能性があります:
| 問題 | 説明 | 例 |
|---|---|---|
| ダーティリード | 他のトランザクションがコミットしていないデータを読み取る | トランザクションAがデータを変更したがコミットしていない;トランザクションBが変更後の値を読み取る |
| 反復不能読み取り | 同じトランザクション内で同じ行を2回読み取ると異なる結果になる | トランザクションAが同じ行を2回読み取る;その間にトランザクションBがその行を変更してコミット |
| ファントム読み取り | 同じトランザクション内で同じクエリを2回実行すると行数が異なる | トランザクションAが2回クエリを実行;その間にトランザクションBが新しい行を挿入 |
SQLは4つの分離レベルを定義しています:
| 分離レベル | ダーティリード | 反復不能読み取り | ファントム読み取り | 説明 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ 発生する可能性 | ✅ 発生する可能性 | ✅ 発生する可能性 | 最低レベル、あまり使用されない |
| READ COMMITTED | ❌ 発生しない | ✅ 発生する可能性 | ✅ 発生する可能性 | Oracle/PostgreSQLのデフォルト |
| REPEATABLE READ | ❌ 発生しない | ❌ 発生しない | ✅ 発生する可能性 | MySQLのデフォルト |
| SERIALIZABLE | ❌ 発生しない | ❌ 発生しない | ❌ 発生しない | 最高レベル、パフォーマンスが最も低い |
-- 現在の分離レベルを確認
SELECT @@transaction_isolation;
-- 分離レベルを設定
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
デッドロック
デッドロックは、2つのトランザクションが互いにリソースの解放を待ち合う状態です:
トランザクションA:テーブル1をロック → テーブル2を待機
トランザクションB:テーブル2をロック → テーブル1を待機
→ 永遠に待機、完了できない
データベースの対処方法:デッドロックを自動的に検出し、一方のトランザクションを「犠牲者」として選んでロールバックし、他方を続行させます。
デッドロック回避戦略:
- テーブルと行に固定順序でアクセスする
- トランザクションがロックを保持する時間を短縮する
- より低い分離レベルを使用する
- よくあるクエリにインデックスを作成し、ロック範囲を縮小する
ベストプラクティス
| 原則 | 説明 |
|---|---|
| トランザクションを短く保つ | ロック保持時間を短縮し、同時実行性を向上 |
| トランザクション内でユーザー入力を待たない | トランザクションは迅速に完了すべき |
| 適切な分離レベルを使用する | デフォルトで最高レベルにしない |
| 一貫したアクセス順序を使用する | デッドロックの確率を低減 |
| エラーハンドリングを追加する | エラー発生時に速やかにロールバック |
📝 基本構文
-- トランザクションを開始
BEGIN; -- MySQL、PostgreSQL
START TRANSACTION; -- MySQLでもサポート
-- トランザクションをコミット(すべての操作が永続的に有効になる)
COMMIT;
-- トランザクションをロールバック(すべての操作を元に戻す)
ROLLBACK;
-- セーブポイント
SAVEPOINT savepoint_name;
ROLLBACK TO savepoint_name;
RELEASE SAVEPOINT savepoint_name;
-- 分離レベルを設定
SET TRANSACTION ISOLATION LEVEL isolation_level;
-- 分離レベル:READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE
-- グローバル分離レベルを設定(すべての新しい接続に影響)
SET GLOBAL TRANSACTION ISOLATION LEVEL isolation_level;
-- オートコミットスイッチ(MySQLはデフォルトでON)
SET autocommit = 0; -- オートコミットを無効、手動COMMITが必要
SET autocommit = 1; -- オートコミットを有効(デフォルト)
- MySQLはデフォルトで
autocommit = 1で、各SQL文が自動コミットされます。トランザクションを使用する前にBEGINまたはSET autocommit = 0を実行してください - DDL文(CREATE、ALTER、DROP)は現在のトランザクションを暗黙的にコミットします
- 接続が切断されると、コミットされていないトランザクションは自動的にロールバックされます
📌 例
例:注文作成トランザクション(注文 + 在庫減算)(難易度⭐⭐)
-- シナリオ:顧客が商品を注文する場合、注文の作成と在庫の減算を同時に行う必要がある
BEGIN;
-- ステップ1:注文を作成
INSERT INTO orders (order_id, customer_id, employee_id, order_date, total_amount, status)
VALUES (1020, 101, 1, '2026-06-28', 6999.00, 'pending');
-- ステップ2:在庫を減算
UPDATE products SET stock = stock - 1 WHERE product_id = 1;
-- ステップ3:在庫が十分かチェック(在庫 < 0 の場合はロールバック)
-- 実際には通常、アプリケーションロジックやトリガーで処理
-- すべて成功、コミット
COMMIT;
途中でエラーが発生した場合:
BEGIN;
INSERT INTO orders (order_id, customer_id, employee_id, order_date, total_amount, status)
VALUES (1021, 102, 3, '2026-06-28', 129.00, 'pending');
UPDATE products SET stock = stock - 1 WHERE product_id = 2;
-- 在庫不足を発見した場合、トランザクション全体をロールバック
ROLLBACK;
-- 注文も在庫もトランザクション前の状態に戻る
例:SAVEPOINTを使った部分ロールバック(難易度⭐⭐)
BEGIN;
-- 最初の注文:成功
INSERT INTO orders VALUES (1022, 103, 4, '2026-06-28', 3500.00, 'pending');
UPDATE products SET stock = stock - 1 WHERE product_id = 4;
SAVEPOINT after_first_order;
-- 2番目の注文:エラー
INSERT INTO orders VALUES (1023, 104, 4, '2026-06-28', 2200.00, 'pending');
UPDATE products SET stock = stock - 1 WHERE product_id = 5;
SAVEPOINT after_second_order;
-- 2番目の注文に問題を発見、最初の注文の後にロールバック
ROLLBACK TO after_first_order;
-- 最初の注文は有効なまま、コミット
COMMIT;
-- 結果:注文1022は保持、注文1023はロールバック
検証:
SELECT order_id, total_amount, status FROM orders WHERE order_id >= 1022;
order_id | total_amount | status
---------+-------------+--------
1022 | 3500.00 | pending
説明:ROLLBACK TO after_first_order はセーブポイント以降の操作のみを元に戻します。最初の注文と在庫減算は保持されます。
🎬 シナリオ演習
シナリオ1:従業員給与の一括調整(エラーハンドリング付き)
BEGIN;
-- 営業部門に10%の昇給
UPDATE employees
SET salary = salary * 1.10
WHERE department_id = (SELECT department_id FROM departments WHERE department_name = 'Sales');
SAVEPOINT after_sales_raise;
-- 技術部門に8%の昇給
UPDATE employees
SET salary = salary * 1.08
WHERE department_id = (SELECT department_id FROM departments WHERE department_name = 'Tech');
SAVEPOINT after_tech_raise;
-- 予算承認が通らなかったと仮定、技術部門の昇給のみ取り消し
ROLLBACK TO after_sales_raise;
-- 営業部門の昇給は保持、コミット
COMMIT;
ポイント:SAVEPOINT により「部分ロールバック」が可能で、柔軟な制御が必要な一括操作に適しています。
シナリオ2:振込のシミュレーションとデッドロック対処
-- セッションA:部門1から部門2に予算を振替
BEGIN;
UPDATE departments SET budget = budget - 50000 WHERE department_id = 1;
-- 少し待機...
UPDATE departments SET budget = budget + 50000 WHERE department_id = 2;
COMMIT;
-- セッションB:同時に部門2から部門1に予算を振替(別の接続)
BEGIN;
UPDATE departments SET budget = budget - 30000 WHERE department_id = 2;
UPDATE departments SET budget = budget + 30000 WHERE department_id = 1;
COMMIT;
デッドロック防止:
-- 両セッションがdepartment_idの昇順で部門にアクセス
-- セッションA:
BEGIN;
UPDATE departments SET budget = budget - 50000 WHERE department_id = 1; -- まず1をロック
UPDATE departments SET budget = budget + 50000 WHERE department_id = 2; -- 次に2をロック
COMMIT;
-- セッションB:
BEGIN;
UPDATE departments SET budget = budget + 30000 WHERE department_id = 1; -- 同じくまず1をロック
UPDATE departments SET budget = budget - 30000 WHERE department_id = 2; -- 次に2をロック
COMMIT;
ポイント:一貫したアクセス順序の使用は、デッドロックを回避する最も簡単で効果的な方法です。
❓ よくある質問
質問:MySQLはデフォルトでオートコミットですが、トランザクションはどう使うのですか? 回答: MySQLはデフォルトで
autocommit = 1で、各SQL文が自動コミットされます。トランザクションを使用するには、まずBEGIN(またはSTART TRANSACTION)を実行してください。その後のSQL文は、COMMITまたはROLLBACKを実行するまで自動コミットされません。
質問:COMMIT後にROLLBACKできますか? 回答: いいえ。
COMMITはトランザクションの終了です。コミット後、データは永続的に保存され、元に戻すことはできません。コミット済みのデータを「元に戻す」には、新しいトランザクションで逆操作を実行する必要があります(例:元の値にUPDATE)。
質問:どの分離レベルを選ぶべきですか? 回答: ほとんどのシナリオでは、データベースのデフォルト(MySQLは
REPEATABLE READ、Oracle/PostgreSQLはREAD COMMITTED)を使用してください。ファントム読み取りを明示的に防止する必要があり、パフォーマンスのトレードオフを受け入れられる場合のみSERIALIZABLEを使用してください。
質問:DDL文(CREATE TABLE、ALTER TABLE)はトランザクションで制御されますか? 回答: いいえ。DDL文は現在のトランザクションを暗黙的にコミットします。トランザクション内でDDLを実行した後、以前のSQL文はロールバックできなくなります。これはMySQLの動作です。PostgreSQLではDDLをロールバックできます。
📖 まとめ
| 概念 | 説明 |
|---|---|
| トランザクション | 分割不可能なデータベース操作のグループ |
| ACID | 原子性、一貫性、分離性、永続性 |
| BEGIN | トランザクションを開始 |
| COMMIT | トランザクションをコミットし、永続化 |
| ROLLBACK | トランザクションをロールバックし、すべて元に戻す |
| SAVEPOINT | トランザクション内のチェックポイント、部分ロールバックをサポート |
| 分離レベル | 同時実行トランザクション間の可視性を制御 |
| デッドロック | 2つのトランザクションが互いに待機;データベースが自動検出し一方をロールバック |
- トランザクションを短く保ち、ロック保持時間を短縮
- 固定順序でリソースにアクセスし、デッドロックを低減
- ビジネスニーズに基づいて適切な分離レベルを選択
📝 演習
ordersテーブルに新しい注文を挿入すると同時に、productsテーブルから対応する商品の在庫を減算するトランザクションを記述してください。在庫不足の場合はトランザクション全体をロールバックしてください。SAVEPOINTを使って、3つの注文レコードを挿入し、3番目をロールバックしつつ最初の2つを保持するトランザクションを実装してください。- データベースのデフォルトの分離レベルを確認し、
READ COMMITTEDに設定して同時読み書きへの影響を観察してください。 - 思考問題:トランザクションの実行時間が長い場合、他のトランザクションにどのような影響がありますか?どう最適化できますか?
次のレッスン
👉 22-stored-procedures - ストアドプロシージャ:ストアドプロシージャの作成と呼び出し、入出力パラメータ、フロー制御文、ストアドプロシージャの実際の応用を学びます。



