みなさんこんにちは!ヒロポンです!
夜のバッチで複数テーブルを更新したら、途中でエラーになって片方だけ反映されてた。気付いたのは翌朝。
在庫は減ってるのに受注は確定してない。そういうズレ。
これ、俺もやりました。流通系の基幹システムの保守をやってた頃、夜中に呼び出されてデータを手で復旧したことがあります。
整合性を戻すのに3時間。正直、心が折れかけました。
この事故を防ぐのがSQL Server トランザクションです。「複数の更新をひとまとまりにして、途中で失敗したら全部なかったことにする」しくみ。
この記事は、BEGIN TRANSACTION / COMMIT / ROLLBACK の基本から、TRY...CATCH で確実に巻き戻すテンプレ、最後に分離レベルの選び方まで、業務SEが本番でやらかさない順に積み上げていきます。
トランザクションが解決する問題 — なぜ「途中で失敗」が怖いのか
まず、なんでトランザクションが要るのか。
たとえば「在庫を1減らす」「受注を確定にする」の2つの UPDATE。これ、別々に実行すると、1つ目が通って2つ目がコケた瞬間にデータが宙ぶらりんになります。
-- ❌ バラバラに実行: 2つ目が失敗すると整合性が壊れる
UPDATE dbo.Stock SET Qty = Qty - 1 WHERE ProductId = 100;
UPDATE dbo.Orders SET Status = 'Fixed' WHERE OrderId = 5001; -- ここで落ちたら?
業務システムって「在庫と受注は必ずセットで動く」みたいな前提が山ほどある。片方だけ動くと、その前提が崩れる。
トランザクションは、この「全部やるか、全部やらないか(All or Nothing)」を保証するための機能です。COMMIT で確定、ROLLBACK で全取り消し。これだけ。
トランザクションの基本: BEGIN TRANSACTION / COMMIT / ROLLBACK
最小の雛形がこれです。コピペして、テーブル名と条件だけ差し替えてください。
BEGIN TRANSACTION;
UPDATE dbo.Stock SET Qty = Qty - 1 WHERE ProductId = 100;
UPDATE dbo.Orders SET Status = 'Fixed' WHERE OrderId = 5001;
COMMIT TRANSACTION; -- 両方成功したら、ここでまとめて確定
BEGIN TRANSACTION から COMMIT までが「ひとまとまり」。
この間の更新は、COMMIT するまで他のセッションから見ればまだ起きてないことになってます。
ん?コミットするまで他からは見えへんの?? そう、見えないんです。だから途中経過を他人に踏ませずに済む。
BEGIN TRAN と略してもOKです。手で叩く時は短いほうが楽。
途中でおかしいと気付いたら、COMMIT の代わりに ROLLBACK TRANSACTION; を打つ。すると BEGIN 以降の更新が全部なかったことになります。これがいい感じに効くんですよね。
ただ、本番の手作業ならともかく、ストアドやバッチに組み込むなら「エラーが出たら自動で巻き戻す」までセットにしないと意味がない。次にいきます。
エラーで確実に巻き戻す: TRY…CATCH と XACT_ABORT
COMMIT に辿り着く前に例外が飛んだら、誰が ROLLBACK するのか?? これを TRY...CATCH で自動化します。
SET XACT_ABORT ON; -- 実行時エラーで自動的に ROLLBACK させる保険
BEGIN TRY
BEGIN TRANSACTION;
UPDATE dbo.Stock SET Qty = Qty - 1 WHERE ProductId = 100;
UPDATE dbo.Orders SET Status = 'Fixed' WHERE OrderId = 5001;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION; -- 途中で落ちたら全部巻き戻す
THROW; -- エラーは握りつぶさず、呼び出し元へ再送出する
END CATCH;
実行結果(CATCH 内でわざとエラーを起こすと、更新が元の値に巻き戻る・SQL Server 2019 互換エンジンで確認):

処理の流れを図にするとこんな感じ。

ポイントは2つあります。
ひとつ目、SET XACT_ABORT ON。これは「実行時エラーが出たら、その時点でトランザクションを強制的に中断扱いにする」スイッチです。
入れておかないと、エラーの種類によってはトランザクションが中途半端に生き残ることがある。バッチ系では原則 ON にしとくのが安全です。
ふたつ目、CATCH の中の IF @@TRANCOUNT > 0。@@TRANCOUNT は今開いているトランザクションの数。
これを見てから ROLLBACK しないと、「もう巻き戻し済みなのに、さらに ROLLBACK しようとしてエラー」みたいな二次災害が起きます。
THROW で再送出してるのは、エラーをアプリ側のログにちゃんと届けるため。ここを握りつぶすと、「失敗したのに成功したように見える」一番こわいパターンになります。
ハマりポイント: ROLLBACK 漏れで接続が開きっぱなしになる
ここ、俺がいっちばん痛い目を見たやつです。
TRY...CATCH を使わずに BEGIN TRANSACTION だけ書いて、途中で例外が飛ぶ。すると COMMIT も ROLLBACK もされないまま処理が抜けます。
-- ❌ ROLLBACK 漏れの典型
BEGIN TRANSACTION;
UPDATE dbo.Stock SET Qty = Qty - 1 WHERE ProductId = 100;
-- ここで例外 → COMMIT も ROLLBACK もされず、抜けてしまう
COMMIT TRANSACTION;
何が起きるか。開きっぱなしのトランザクションが、対象の行にロックを掛けたまま居座るんです。
別のセッションが同じ行を触ろうとすると、ロック待ちでひたすら止まる。
最悪、接続プールが詰まって画面全体が固まる。で、原因が「どこかで開いたまま放置されたトランザクション」だと気付くまでに、また時間が溶ける。
対策はシンプルで、BEGIN TRANSACTION を書いたら、必ず TRY...CATCH とセットにする。さっきのテンプレを雛形として持っておけば、これは踏みません!!
開きっぱなしを疑うときは、DBCC OPENTRAN で「一番古い未コミットのトランザクション」を確認できます。障害対応中はこれが効きます。
分離レベルの選び方 — 4種を業務SE視点で
ここまでが「書き込み側」の話。最後に「読み取り側」、分離レベルの話をします。
分離レベルは、「コミットされてないデータをどこまで見せるか / 読んでる間どこまでロックするか」 のさじ加減です。SQL Server の既定は READ COMMITTED。
業務でよく出る4種を、業務SEの判断軸で並べました。

ざっくり指針はこんな感じです。
- READ COMMITTED: 既定。大半の業務処理はこれでいい。迷ったらここから動かさない
- REPEATABLE READ: 同じトランザクション内で同じ行を2回読んだら必ず同じ値、を保証したい集計系
- SERIALIZABLE: 一番厳密。ただしロックを大量に持つのでデッドロックが起きやすい。安易に上げない
- SNAPSHOT: 読み取りがロックを待たない(行のバージョンを別に持つ方式)。参照の多い画面で書き込みとぶつかりたくない時に効く
設定はこう書きます。
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 既定
-- SET TRANSACTION ISOLATION LEVEL SNAPSHOT; -- 読み取りがロックを待たない方式に
注意したいのは、分離レベルを上げるほどロックが強くなって、デッドロックや待ちが増えること。「一貫性が欲しいから SERIALIZABLE」と安易に上げると、別の場所で待ちが詰まります。
SNAPSHOT は読み取り側がロックを待たない代わりに、tempdb に行のバージョンを溜めます。そっちの容量と負荷を見ておく必要がある。タダではない、ってことですね。
それと、SNAPSHOT は DB 側の準備が要ります。ALTER DATABASE [DB名] SET ALLOW_SNAPSHOT_ISOLATION ON; をしておかないと、SET TRANSACTION ISOLATION LEVEL SNAPSHOT の時点で Msg 3952 で弾かれます。
上のコメント行をそのまま外して動かないのは、たいていこれ。

俺の現場ではこう使ってる
実務での落とし込みは、だいたい3つに固定してます。
- 更新が2本以上ある処理は、問答無用で
TRY...CATCH+XACT_ABORT ONの雛形に乗せる。1本でも、後で増える前提なら最初から乗せておく - 分離レベルは原則
READ COMMITTEDのまま触らない。上げるのは「この集計だけ一貫性が要る」と理由を言える時だけ - トランザクションの中に、外部APIコールや人の確認待ちを挟まない。ロックを握ったまま待つと、その間ずっと他をブロックする
最後に1個だけ、強めに言っておきます。トランザクション周りは、本番でいきなり試さない。
ロックやデッドロックは、データ量と同時実行があって初めて出る現象です。1人で開発機を触ってる時には絶対に再現しません。
検証機で、ある程度のデータと並行アクセスを作って試す。これをサボると、夜中に呼び出されるのは未来の自分です。
まとめ
要点はこれだけ。
- 複数更新は
BEGIN TRANSACTION〜COMMITでひとまとまりにする。失敗時はROLLBACK - 自動で巻き戻すなら
TRY...CATCH+SET XACT_ABORT ON+IF @@TRANCOUNT > 0の雛形 BEGIN TRANSACTIONを書いたら必ずTRY...CATCHとセット。でないと ROLLBACK 漏れでロックが残る- 分離レベルは原則
READ COMMITTED。上げる時は理由を言えるときだけ
SQL Server の内部動作(ロックや分離レベルがどう実装されてるか)まで体系的に押さえたいなら、内部実装系の解説書を1冊持っておくといい。障害対応のときの解像度が一段上がります。
このテンプレ、ストアドの雛形にそのまま貼って使ってください。
よくある質問
Q1. COMMIT を書き忘れたらどうなりますか?
セッションが生きている間、トランザクションは開いたままになり、対象の行にロックが残ります。他のセッションがそこを触ると待たされます。バッチなら TRY...CATCH で必ず COMMIT か ROLLBACK に到達するように書くのが安全です。手作業中に開いたままを疑うときは DBCC OPENTRAN で確認できます。
Q2. XACT_ABORT ON は常に付けるべきですか?
バッチやストアドでは原則 ON を推奨します。OFF のままだと、エラーの種類によってはトランザクションが中断されずに残り、@@TRANCOUNT の扱いが複雑になります。ON にしておけば「実行時エラー=即中断」で挙動が読みやすくなります。
Q3. ROLLBACK と COMMIT、どちらも @@TRANCOUNT のチェックは要りますか?
ROLLBACK 側で必要です。CATCH に入った時点で、すでにトランザクションが中断・巻き戻し済みのケースがあるためです。IF @@TRANCOUNT > 0 を挟まずに ROLLBACK すると「対応するトランザクションがない」エラーになることがあります。COMMIT は正常系で1回到達するだけなので通常チェック不要です。
Q4. 分離レベルを SERIALIZABLE にすれば一番安全ですか?
データの一貫性という意味では最も厳密ですが、その分ロックを広く長く持つので、デッドロックやロック待ちが増えます。「安全」と引き換えに「同時実行性」を捨てる選択です。大半の業務は READ COMMITTED で足り、必要な処理だけ上げるのが定石です。
Q5. SNAPSHOT 分離レベルはどんな時に使いますか?
参照(SELECT)が多くて、書き込みとロックでぶつかりたくない画面に向きます。読み取りがロックを待たない代わりに、tempdb に行のバージョンを保持するので、tempdb の容量と I/O に余裕があるか確認してから有効化してください。
次に読むべき記事





以上!
同じ事故で夜中に呼び出された経験ある人いたら、どんどんシェア待ってるぜ!!
執筆者
バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る



