Skip to main content

トランザクション — ACID とコミット・ロールバック

「振込処理で、出金には成功したが入金で失敗した」。このとき最悪なのは処理が止まることではなく、お金が消えた状態でデータが残ることです。

トランザクションは、複数の操作をまとめて全部成功か、全部無かったことにするかの 2 択にする仕組みです。中途半端な状態を残さないための道具です。

この章で学ぶこと

  • トランザクションの 3 つの基本操作
  • ACID の 4 特性がそれぞれ何を保証するか
  • ロールバックが取り消す範囲
  • コミットしたあとに取り消せるか
  • SQL の誤りがいつ検出されるか
前提知識

SQL で INSERTUPDATE を書いた経験があれば読めます。

この章で扱わないこと

観点参照先
ACID の入門的な解説と演習データベース設計ガイド — CRUD と ACID
フレームワークでの実装、トランザクションを置く層の判断Laravel × DDD ガイド — トランザクション管理
再送の判断と、回数・間隔の決め方リトライと冪等性
実務での並行制御とリトライの実装Laravel API 開発ガイド — トランザクションと並行性
複数台に分散したときに原子性がどう崩れるかデータの分散
複数の表から 1 つの答えを作るクエリの書き方結合と集計

3 つの基本操作

トランザクションで使う操作は 3 つだけです。

BEGIN; -- 開始

UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;

COMMIT; -- 確定
-- または ROLLBACK; -- 取り消し
操作意味
開始 (BEGIN / START TRANSACTION)ここから 1 つのまとまりとして扱う
コミット (COMMIT)ここまでの変更を確定させる
ロールバック (ROLLBACK)開始以降の変更を取り消す

この 3 つが基本です。加えて、途中に印を打って部分的に戻すセーブポイントという仕組みもありますが、まずは 3 操作を押さえます。

ACID

トランザクションが保証する 4 つの性質です。頭文字を取って ACID と呼びます。

特性保証すること破れるとどうなるか
Atomicity (原子性)すべて成功か、すべて無かったことになる出金だけ成立してお金が消える
Consistency (一貫性)整合性の制約を満たした状態から状態へ移る存在しないユーザーの注文が生まれる
Isolation (独立性)並行する他のトランザクションの影響を受けない途中の中途半端な値を他から読まれる
Durability (永続性)コミットした結果は障害があっても失われない完了通知を出したのに再起動で消える

原子性 — 「全部か、無しか」

冒頭の振込の例がこれです。2 つの UPDATE のうち片方が失敗したら、もう片方も無かったことにします。「片方だけ成立」という状態は存在しません。

原子性は失敗時の後始末を自動化する仕組みでもあります。これが無いと、失敗を検知するたびに「ここまでに何を変更したか」を自分で記録して手作業で戻すコードが要ります。

永続性 — コミットしたら消えない

COMMIT が返った時点で、その変更は障害があっても残ります。データベースはこれを、変更をディスク上のログに書き切ってから完了を返すことで実現しています。

**この保証があるから「コミット後は取り消せない」という制約が生まれます。**取り消せるなら、確定したという約束が意味を失います。

ロールバックが取り消す範囲

ここが最も誤解されやすい点です。

**ロールバックが取り消すのは、そのトランザクションが行った変更だけです。**他のユーザーや他のセッションが同時に行った操作は一切影響を受けません。

セッション A のロールバックは、セッション B が確定した変更に触れません。「トランザクション開始からその時点までにデータベースに対して行われた全ユーザーの全操作を取り消す」ものではありません。

もしそうなら、誰か 1 人がロールバックするたびに全員の作業が消えることになり、同時に使えるシステムが成り立ちません。トランザクションはむしろ、各セッションの作業を互いから隔離するための仕組みです。

コミットしたあとは取り消せない

コミットしたトランザクションは、あとから ID を指定してもロールバックできません。

理由は永続性です。コミットは「この結果は確定した」という宣言で、他のトランザクションはその結果を前提に動き始めます。あとから取り消せてしまうと、確定した値を読んで別の判断をした処理まで巻き戻す必要が出て、収拾がつきません。

確定後に元へ戻したいなら、打ち消す操作を新しいトランザクションとして実行します

やりたいこと方法
直前の更新を戻す元の値へ更新する新しいトランザクションを実行する
会計上の取り消し逆仕訳を追加する (元のレコードは消さない)
ある時点の状態に戻すバックアップからのリストア、または PITR

会計や在庫のように履歴が重要な領域では、そもそも削除や上書きをせず、打ち消しのレコードを積む設計を選びます。「取り消した」という事実自体が記録として必要だからです。

構文エラーはいつ検出されるか

SQL の誤りが見つかるタイミングには段階があります。

段階検出されるもの
SQL 発行時構文の誤り、存在しないテーブルや列、型の不一致
文の実行時一意制約違反、外部キー違反、NOT NULL 違反
コミット時遅延制約 (DEFERRABLE) の違反

構文エラーは通常、その文を発行した時点で返ります。「コミットするまで構文エラーが分からない」という状態にはなりません。

ただし**制約チェックはコミットまで遅らせられます。**PostgreSQL などは制約に DEFERRABLE INITIALLY DEFERRED を指定でき、その場合はコミット時にまとめて検証されます。

-- 制約を DEFERRABLE で宣言しておく (これが無いと遅延できない)
ALTER TABLE a ADD CONSTRAINT a_b_fk FOREIGN KEY (b_id) REFERENCES b(id)
DEFERRABLE INITIALLY IMMEDIATE;
ALTER TABLE b ADD CONSTRAINT b_a_fk FOREIGN KEY (a_id) REFERENCES a(id)
DEFERRABLE INITIALLY IMMEDIATE;

-- 相互参照する 2 つのテーブルに、順番に挿入する
BEGIN;
SET CONSTRAINTS a_b_fk, b_a_fk DEFERRED;
INSERT INTO a (id, b_id) VALUES (1, 1); -- この時点では b がまだ無い
INSERT INTO b (id, a_id) VALUES (1, 1);
COMMIT; -- ここで両方の外部キーを検証

SET CONSTRAINTS が効くのは DEFERRABLE と宣言した制約だけです。宣言していない制約は、この指定を書いても文の実行時に検証されます。

したがって「SQL を発行した時点でエラーが出なくても、コミット時にエラーが発生しうる」という理解は成り立ちます。ただしそれは構文エラーではなく、遅延させた制約違反です。この区別を押さえておくと、エラーメッセージを読むときに原因の当たりを付けやすくなります。

使いどころ

トランザクションは万能ではありません。長く開いたままにすると、その間ロックが保持されて他の処理を待たせます。

すべきこと避けるべきこと
関連する複数の更新を 1 つにまとめるトランザクションの中で外部 API を呼ぶ
短く保つユーザーの入力を待つあいだ開いたままにする
失敗したら確実にロールバックする大量の行を 1 トランザクションで更新する

外部 API の呼び出しをトランザクションの中に入れないのは重要な原則です。外部が遅いとその間ずっとロックが残りますし、そもそも外部への呼び出しはロールバックできません。「メールを送ってからロールバック」しても、メールは戻ってきません。ロックが保持される期間についてはロックと分離レベルで扱います。

よくある誤解

「トランザクションは長く開いていても問題ない」 — ロックが保持され、他の処理を待たせます。短く保つのが原則です。

「コミット時に構文エラーが出ることはない」 — 構文エラーは発行時に出ますが、遅延制約の違反はコミット時に出ます。「発行時に通ってもコミット時にエラーになりうる」は成り立ちます。

「ロールバックは、開始から現在までに行われた全ユーザーの全操作を取り消す」 — 取り消すのは自分のトランザクションの変更だけです。他のセッションが確定した変更には影響しません。

「トランザクション ID が分かればコミット後もロールバックできる」 — できません。コミットは確定の宣言で、永続性の保証と矛盾します。打ち消す操作を新しく実行します。

「オートコミットならトランザクションを使っていない」 — 1 文ごとに暗黙のトランザクションが張られています。明示的に囲んでいないだけで、単一の文は原子的に実行されます。

確認問題

問 1. 運用担当者から「昨日の 15 時に誤って一括更新を流してしまった。そのトランザクションを取り消してほしい」と依頼されました。どう答えますか。

答え: コミット済みなら取り消せません。打ち消す操作を新しく実行するか、バックアップから復旧します。

説明の組み立て方です。

  1. **コミットは確定の宣言です。**永続性 (Durability) の保証があるので、あとから「無かったこと」にはできません。取り消せてしまうと、その結果を読んで別の処理を進めた分まで巻き戻す必要が出ます
  2. 代わりに取れる手段 — 変更前の値が分かるなら、元へ戻す更新を新しいトランザクションとして流します。分からないなら、バックアップやある時点への復旧 (PITR) を検討します
  3. 記録の観点 — 会計や在庫のように履歴が重要な領域では、そもそも上書きせず打ち消しのレコードを積む設計にしておくと、この依頼にそのまま応えられます

なお、まだコミットしていないトランザクションなら ROLLBACK で取り消せます。ただしその場合も、取り消されるのはそのトランザクション自身の変更だけで、同時刻に他のユーザーが確定させた操作には影響しません。

問 2. 「トランザクションを使えば、SQL の誤りはすべて発行した瞬間に分かる」と言われました。妥当ですか。

答え: 妥当ではありません。コミット時に初めてエラーになる場合があります。

構文の誤りや存在しないテーブルの参照は、たしかにその文を発行した時点で返ります。しかし制約のチェックはコミットまで遅らせられます。

BEGIN;
SET CONSTRAINTS a_b_fk, b_a_fk DEFERRED; -- DEFERRABLE 宣言済みの制約が対象
INSERT INTO a (id, b_id) VALUES (1, 1); -- b がまだ無いが通る
INSERT INTO b (id, a_id) VALUES (1, 1);
COMMIT; -- ここで両方の外部キーを検証する

相互に参照し合うテーブルへ順番に挿入したい場合など、途中の状態が一時的に制約を破ることがあります。遅延制約 (DEFERRABLE INITIALLY DEFERRED) はそれを許し、コミット時にまとめて検証します。

したがって「発行時に通ってもコミット時にエラーになりうる」は成り立ちます。ただしそのエラーは構文の誤りではなく、遅延させた制約違反です。エラーが出たタイミングから原因の種類を絞れるので、この区別は覚えておく価値があります。

問 3. 注文を確定したあと、外部の決済 API を呼びます。この呼び出しはトランザクションの中に入れるべきですか。

答え: 入れるべきではありません。

理由が 2 つあります。

  1. **外部呼び出しはロールバックできない。**トランザクションをロールバックしても、決済 API に送ったリクエストは取り消されません。データベース上は注文が無いのに決済だけ成立する、という食い違いが残ります
  2. **ロックを長く保持する。**外部 API の応答が遅ければ、その間ずっと行ロックが残ります。同じ行を触る他の処理が待たされます

現実的な設計は、トランザクションを閉じてから外部を呼び、その結果を別のトランザクションで反映することです。

[Tx1] 注文を「決済待ち」で保存 → COMMIT
決済 API を呼ぶ
[Tx2] 結果に応じて「確定」または「失敗」へ更新 → COMMIT

途中で落ちても「決済待ち」の注文が残るので、あとから照合して復旧できます。中間状態を明示的に持たせるのが、外部システムとの整合を取る定石です。

問 4. セッション A が在庫を 10 から 5 に更新中 (未コミット)、セッション B が同じ商品の商品名を更新してコミットしました。その後 A がロールバックしました。商品名はどうなりますか。

答え: B が更新した商品名のまま残ります。

A のロールバックが取り消すのは、A 自身が行った在庫の更新だけです。在庫は 10 に戻りますが、B が確定させた商品名には触れません。

この隔離があるから、複数のユーザーが同時に同じテーブルを扱えます。もしロールバックが「その時点までの全操作」を巻き戻すなら、誰か 1 人の失敗で全員の作業が消えることになります。

なお、A と B が同じ行の同じ列を更新しようとした場合は話が変わり、ロックによって片方が待たされます。その挙動は次の章で扱います。

まとめ

  • トランザクションの基本操作は開始・コミット・ロールバックの 3 つです
  • ACID は原子性・一貫性・独立性・永続性の 4 特性です
  • ロールバックが取り消すのは自分のトランザクションの変更だけで、他ユーザーの操作には影響しません
  • **コミット後はロールバックできません。**取り消したいなら打ち消す操作を新しく実行します
  • 構文エラーは発行時に出ますが、遅延制約の違反はコミット時に出ます
  • トランザクションは短く保ちます。中で外部 API を呼ばないのが原則です
関連リファレンス

次に読む