こんにちは、かつコーチです。
前回はDELETE文で、WHERE句を書き忘れると取り返しのつかない事故につながるという話をしました。
「もし途中で間違いに気づいたら、実行前の状態に戻したい」——そう思ったことはないでしょうか。
その願いを叶えてくれるのが、今回解説するトランザクションです。
複数のSQL文をひとまとまりの処理として扱い、失敗したときにまとめて取り消せる仕組みを、COMMITとROLLBACKという2つのキーワードを軸に解説します。
トランザクションとは?
複数のSQL文を1つの処理としてまとめる仕組み
トランザクションとは、複数のSQL文を「すべて成功する」か「すべて失敗する」かのどちらかにまとめて扱う仕組みです。
たとえば銀行口座の送金処理を考えてみましょう。
送金元の残高を減らすUPDATE文と、送金先の残高を増やすUPDATE文の、合計2つのSQL文が必要になります。
-- 送金元の残高を減らす
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
-- 送金先の残高を増やす
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
もし1つ目のUPDATE文だけ成功して2つ目が何らかの理由で失敗したら、送金元からお金が消えたのに送金先には届かない、という深刻な不整合が起きてしまいます。
トランザクションを使えば、この2つのUPDATE文を1つの処理としてまとめ、「両方成功したら確定」「どちらかが失敗したら両方なかったことにする」という制御ができます。
なぜトランザクションが必要なのか
Webアプリでは、注文確定と在庫減算、ポイント付与と履歴登録など、複数のテーブルにまたがるデータ更新が同時に発生する場面が数多くあります。
これらの処理を個別のSQL文としてバラバラに実行してしまうと、途中でエラーやサーバーダウンが起きた際に、一部だけが反映された中途半端な状態でデータが残ってしまいます。
トランザクションは、こうした「中途半端な状態」を防ぎ、データの整合性を保つための土台となる仕組みです。
基本の書き方
この記事では、以下のテーブルを使います。
CREATE TABLE accounts (
id INT PRIMARY KEY AUTO_INCREMENT,
owner_name VARCHAR(50) NOT NULL,
balance INT NOT NULL DEFAULT 0
);
手順1:START TRANSACTIONでトランザクションを開始する
START TRANSACTION(またはBEGIN)を実行すると、そこからトランザクションが始まります。
START TRANSACTION;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
この時点では、変更はまだデータベースに正式に確定していません。
他のユーザーやアプリケーションからは、変更前の状態が見えています。
手順2:COMMITで変更を確定する
処理内容に問題がなければ、COMMITを実行して変更を確定します。
COMMIT;
COMMITを実行した瞬間に、トランザクション内で行ったすべての変更がデータベースに正式に反映されます。
一度COMMITした変更は、通常の操作では取り消せません。
手順3:ROLLBACKで変更を取り消す
処理の途中で問題が見つかった場合は、COMMITの代わりにROLLBACKを実行します。
START TRANSACTION;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
-- ここでエラーや想定外の値に気づいた場合
ROLLBACK;
ROLLBACKを実行すると、トランザクション開始後に行ったすべての変更が取り消され、START TRANSACTION以前の状態に戻ります。
「COMMITするまでは確定していない」「ROLLBACKすればいつでもやり直せる」という2段構えの安心感が、トランザクションの最大のメリットです。
よくあるつまずきポイント・エラー対処
COMMITし忘れてロックが解放されない
❌ Before:トランザクションを開始したまま、COMMITもROLLBACKもせず放置する
START TRANSACTION;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
-- ここで作業を中断し、他の作業に移ってしまう
私が実際に経験したのは、開発中にトランザクションを開始したまま別のSQL文の動作確認に気を取られ、COMMITもROLLBACKもせずに放置してしまったケースです。
該当の行に対して別のクライアントから同じUPDATEやSELECT ... FOR UPDATEを実行しようとすると処理がフリーズしたように固まり、Lock wait timeout exceeded; try restarting transactionというエラーで初めて原因に気づきました。
トランザクションが開いている間、変更対象の行はロックされたままになり、他の処理を待たせてしまいます。
✅ After:トランザクションは開いたらすぐ、COMMITかROLLBACKで完結させる
START TRANSACTION;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
COMMIT;
「トランザクションを開いたら、間を空けずにCOMMITかROLLBACKまで一気に実行する」というルールを徹底することで、意図しないロック待ちを防げます。
長時間かかる処理をトランザクションの中に含めないことも、同じ理由で重要です。
オートコミットモードで挙動が想定と違う
❌ Before:オートコミットの挙動を知らずに「ロールバックできるはず」と思い込む
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
-- START TRANSACTIONを書いていないのに、ROLLBACKすれば戻せると思っていた
ROLLBACK;
MySQLは初期設定でオートコミットモードが有効になっており、明示的にSTART TRANSACTIONを書かずにSQL文を実行すると、1文ごとに自動でCOMMITされます。
そのため上記のようにSTART TRANSACTIONを書かずにUPDATE文を実行してしまうと、その時点ですでに変更は確定しており、後からROLLBACKを実行してもすでに手遅れです。
✅ After:複数文をまとめて扱いたい場合は必ずSTART TRANSACTIONを明示する
START TRANSACTION;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
COMMIT;
「取り消せるようにしたい処理は、必ずSTART TRANSACTIONから書き始める」と覚えておくと、この勘違いを防げます。
アプリケーションのフレームワークによっては、DBトランザクションの開始・終了を自動で管理してくれる機能もあるので、使っている言語やORMの挙動もあわせて確認しておくとよいでしょう。
応用・一歩先の使い方
SAVEPOINTで部分的に取り消す
トランザクションの途中にSAVEPOINT(セーブポイント)を設定しておくと、トランザクション全体ではなく、一部だけをロールバックすることもできます。
START TRANSACTION;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
SAVEPOINT before_bonus;
UPDATE accounts SET balance = balance + 500 WHERE id = 1;
-- ボーナス付与だけを取り消したい場合
ROLLBACK TO SAVEPOINT before_bonus;
COMMIT;
ROLLBACK TO SAVEPOINTは、指定したセーブポイント以降の変更だけを取り消し、それ以前の変更は残したままにできます。
複数のステップからなる複雑な処理で、一部のステップだけをやり直したい場合に便利な機能です。
アプリケーションコードとの連携
実務では、トランザクションの開始・COMMIT・ROLLBACKは、SQLクライアントで手動実行するよりも、PHPやPythonなどのアプリケーションコードから制御することがほとんどです。
// PHP(PDO)でのトランザクション制御の例
try {
$pdo->beginTransaction();
$pdo->exec("UPDATE accounts SET balance = balance - 10000 WHERE id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 10000 WHERE id = 2");
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
例外が発生した場合に自動でROLLBACKする、という構造をあらかじめ用意しておくことで、エラー処理の漏れによる不整合を防ぎやすくなります。
「複数テーブルを更新する処理は、必ずtry-catchとトランザクションをセットで書く」という習慣を、コードレビューの基準に組み込んでおくのもおすすめです。
まとめ
この記事のポイント
- トランザクションは複数のSQL文を「すべて成功」か「すべて失敗」かにまとめる仕組み
- START TRANSACTIONで開始し、問題なければCOMMITで確定、問題があればROLLBACKで取り消す
- トランザクションを開いたまま放置すると、ロックが解放されず他の処理を待たせてしまう
- MySQLはオートコミットが初期設定のため、複数文をまとめたい場合は明示的にSTART TRANSACTIONが必要
- SAVEPOINTを使えば、トランザクションの一部だけを取り消すこともできる
次に読むべき記事
トランザクションが「なぜ」安全性を保証できるのかを理解するには、その背後にある4つの性質を知る必要があります。
次回は「ACID特性とは?トランザクションが守る4つの性質」を解説します。
タグ: SQL, 中級者向け, トランザクション