【SQL Server】デッドロックの原因と確認方法|エラー1205・デッドロックグラフ・回避策

【SQL Server】デッドロックの原因と確認方法|エラー1205・デッドロックグラフ・回避策 SQL

SQL Serverで更新処理やバッチ処理を実行していると、Msg 1205 のデッドロックエラーが発生することがあります。デッドロックは単なるロック待ちではなく、複数のトランザクションが互いに相手のロック解放を待ってしまい、どちらも進めなくなる状態です。

この記事では、SQL Serverのデッドロックの原因、エラー1205の意味、デッドロックグラフの確認方法、system_health や Extended Events での調査、アプリケーション側のリトライ実装、再発防止策まで実務向けに整理します。

先に結論
SQL Serverのデッドロックは、まずエラー1205が出た時点で「被害側のトランザクションはロールバックされた」と考えます。原因調査では xml_deadlock_report のデッドロックグラフを確認し、どのSQLがどの順番でロックを取り合ったかを見ます。対策は、処理順序をそろえる、トランザクションを短くする、適切なインデックスを用意する、エラー1205を捕捉して安全に再試行する、の4つが中心です。
スポンサーリンク

SQL Serverのデッドロックとは

デッドロックは、2つ以上のセッションが互いに必要なリソースをロックしたまま、相手のロック解放を待ち続ける状態です。通常のロック待ちは、片方の処理が終われば進みます。しかしデッドロックでは待ち関係が循環しているため、SQL Serverが片方を強制的に終了して解消します。

状態 意味 代表的な見え方
ロック待ち 一方が相手の処理完了を待っている blocking_session_id に待たせているSPIDが出る
デッドロック 複数セッションが互いに待ち合っている Msg 1205 が返り、片方のトランザクションがロールバックされる
タイムアウト 一定時間待ってアプリやクライアントが諦める アプリ側のタイムアウト例外やコマンドタイムアウトになる

ロック待ちの確認方法は SQL Serverでテーブルロックを確認するSQL にまとめています。この記事では、ロック待ちが循環してエラー1205になるケースに絞って説明します。

エラー1205の意味

SQL Serverでデッドロックが検出されると、片方のトランザクションがデッドロックの被害者として選ばれ、現在のバッチが終了し、そのトランザクションはロールバックされます。アプリケーションには 1205 のエラーが返ります。

error-1205-message.txt
Msg 1205, Level 13, State 51, Line 1
Transaction (Process ID 57) was deadlocked on lock resources with another process
and has been chosen as the deadlock victim. Rerun the transaction.

重要なのは、被害側の処理は途中まで成功しているのではなく、トランザクション単位でロールバックされる点です。そのため、アプリケーション側ではエラー1205を捕捉し、必要に応じて再試行できる設計にしておきます。

よくある原因

デッドロックの多くは、複数の処理が同じテーブルや行を違う順番で更新することで発生します。特に、複数テーブルをまたぐ更新、明細とヘッダの更新、在庫引当、ステータス更新、集計テーブル更新で起きやすいです。

原因 起きやすい例 見直すポイント
更新順序が逆 処理Aは注文→在庫、処理Bは在庫→注文の順に更新する 全処理でテーブル・行のアクセス順をそろえる
トランザクションが長い 画面入力待ちや外部API待ちをトランザクション中に行う DB更新部分だけを短いトランザクションにする
インデックス不足 対象行の検索に時間がかかり、広い範囲をロックする WHERE句・JOIN条件に合うインデックスを確認する
大量更新 一括UPDATEやDELETEが長時間ロックを保持する 分割更新、実行時間帯、バッチサイズを見直す
分離レベルが強い SERIALIZABLE などで範囲ロックが増える 必要な分離レベルかを確認する

更新SQL自体の書き方を確認したい場合は、SQLのUPDATE文でデータを更新する方法 も参考になります。WHERE句の絞り込みやJOIN更新の書き方が曖昧だと、不要なロック範囲が広がりやすくなります。

症状別の切り分け

エラー1205が出ている場合はデッドロックですが、現場ではロック待ち、タイムアウト、長時間SQLと混同されがちです。まずは症状から切り分けると、見るべき場所を間違えにくくなります。

症状 可能性が高いもの 次に見るもの
Msg 1205 が返る デッドロック xml_deadlock_report とデッドロックグラフ
SQLが待ち続ける 通常のブロッキング sys.dm_exec_requests.blocking_session_id
アプリ側でタイムアウトする コマンドタイムアウトまたは長時間待機 アプリログ、SQL実行時間、ブロッキング状況
夜間バッチだけ失敗する 大量更新同士の競合 バッチの実行順序、対象テーブル、更新件数
特定画面でだけ発生する 画面操作と裏側バッチの競合 アプリ名、ホスト名、実行SQL、トランザクション範囲

通常のブロッキングまで含めて広く調べたい場合は、SQLのロック調査・解除完全ガイド も合わせて確認すると、SQL Server以外のDBとの違いも整理できます。

デッドロックを再現する簡単な例

次のように、2つのセッションが異なる順番で同じ2つのテーブルを更新すると、デッドロックが発生しやすくなります。実際に試す場合は検証用DBで行ってください。

setup-deadlock-sample.sql
CREATE TABLE dbo.DeadlockA (
    id int NOT NULL PRIMARY KEY,
    value int NOT NULL
);

CREATE TABLE dbo.DeadlockB (
    id int NOT NULL PRIMARY KEY,
    value int NOT NULL
);

INSERT INTO dbo.DeadlockA (id, value) VALUES (1, 10);
INSERT INTO dbo.DeadlockB (id, value) VALUES (1, 20);
session-a.sql
-- セッションA
BEGIN TRAN;

UPDATE dbo.DeadlockA
SET value = value + 1
WHERE id = 1;

-- ここでセッションBを実行してから続ける
UPDATE dbo.DeadlockB
SET value = value + 1
WHERE id = 1;

COMMIT;
session-b.sql
-- セッションB
BEGIN TRAN;

UPDATE dbo.DeadlockB
SET value = value + 1
WHERE id = 1;

-- セッションAの2つ目のUPDATEとぶつかる
UPDATE dbo.DeadlockA
SET value = value + 1
WHERE id = 1;

COMMIT;

この例では、セッションAが DeadlockA をロックし、セッションBが DeadlockB をロックします。その後、互いに相手が持っているロックを取りに行くため、待ち関係が循環します。

修正例: 更新順序をそろえる

上の例では、セッションAとセッションBで更新順序が逆になっています。デッドロックを避ける基本は、同じリソースを扱う処理ではアクセス順序を統一することです。

same-order-update.sql
-- どの処理でも DeadlockA -> DeadlockB の順に更新する
BEGIN TRAN;

UPDATE dbo.DeadlockA
SET value = value + 1
WHERE id = 1;

UPDATE dbo.DeadlockB
SET value = value + 1
WHERE id = 1;

COMMIT;

アクセス順序をそろえると、片方が待つことはあっても、互いに待ち合う循環を作りにくくなります。ヘッダと明細、注文と在庫、親テーブルと子テーブルのように、複数テーブルを更新する処理では特に効果があります。

まず確認すること

エラー1205が出た直後に確認したいのは、通常のロック待ちではなくデッドロックが記録されているかです。リアルタイムに待ちを見たい場合は sys.dm_exec_requestsblocking_session_id を見ますが、デッドロックは発生後に片方がロールバックされるため、後からデッドロックグラフを見るのが基本です。

確認したいこと 使うもの
発生したSQLとSPID xml_deadlock_report のデッドロックグラフ
どのリソースを取り合ったか デッドロックグラフの resource-list
どちらが被害者になったか デッドロックグラフの victim-list
通常のブロッキングが残っているか sys.dm_exec_requests / sys.dm_tran_locks

system_healthからデッドロックグラフを確認する

SQL Serverには既定で system_health という Extended Events セッションがあります。環境によって保持件数や権限の制約はありますが、まずはここから xml_deadlock_report を確認すると早いです。

system-health-deadlock-ring-buffer.sql
WITH deadlock_events AS (
    SELECT CAST(t.target_data AS xml) AS target_data
    FROM sys.dm_xe_session_targets AS t
    JOIN sys.dm_xe_sessions AS s
        ON s.address = t.event_session_address
    WHERE s.name = N'system_health'
      AND t.target_name = N'ring_buffer'
)
SELECT
    x.event_data.value('@timestamp', 'datetime2') AS event_time,
    x.event_data.query('(data/value/deadlock)[1]') AS deadlock_graph
FROM deadlock_events
CROSS APPLY target_data.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS x(event_data)
ORDER BY event_time DESC;

結果の deadlock_graph に、デッドロックに関わったプロセス、SQL、ロック対象がXMLで出ます。GUIで見たい場合は、XMLを .xdl として保存してSSMSで開くと、グラフとして確認できます。

デッドロックグラフで見るポイント

デッドロックグラフは情報量が多いですが、最初に見る場所は限られています。victim-listprocess-listresource-list の3つを順に見ます。

見る場所 意味 確認すること
victim-list 被害者になったプロセス どのSPIDが1205でロールバックされたか
process-list 関係したセッション 実行SQL、アプリ名、ホスト名、トランザクション開始時刻
resource-list 取り合ったリソース KEY、PAGE、OBJECT、インデックス名、テーブル名
owner-list ロックを持っていた側 どちらが何を保持していたか
waiter-list 待っていた側 どちらが何を待っていたか

SQL本文だけで判断せず、どのインデックスやキーで競合したかまで見ます。同じUPDATE文でも、インデックス不足で広い範囲を走査している場合はデッドロックが起きやすくなります。

デッドロックグラフからSQL本文を抜き出す

XMLをそのまま読むのがつらい場合は、まず関係したプロセスのSQL本文だけ抜き出します。次のSQLは、system_health のリングバッファから直近のデッドロックを取り出し、プロセスID、ホスト名、アプリ名、SQL本文を一覧化する例です。

extract-deadlock-sql.sql
WITH deadlock_events AS (
    SELECT CAST(t.target_data AS xml) AS target_data
    FROM sys.dm_xe_session_targets AS t
    JOIN sys.dm_xe_sessions AS s
        ON s.address = t.event_session_address
    WHERE s.name = N'system_health'
      AND t.target_name = N'ring_buffer'
), deadlock_xml AS (
    SELECT TOP (1)
        x.event_data.query('(data/value/deadlock)[1]') AS deadlock_graph
    FROM deadlock_events
    CROSS APPLY target_data.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS x(event_data)
    ORDER BY x.event_data.value('@timestamp', 'datetime2') DESC
)
SELECT
    p.value('@id', 'nvarchar(100)') AS process_id,
    p.value('@spid', 'int') AS spid,
    p.value('@hostname', 'nvarchar(128)') AS host_name,
    p.value('@clientapp', 'nvarchar(256)') AS client_app,
    p.value('(inputbuf/text())[1]', 'nvarchar(max)') AS input_sql
FROM deadlock_xml
CROSS APPLY deadlock_graph.nodes('/deadlock/process-list/process') AS n(p);

このSQLで関係したSQL本文を確認し、どの処理が逆順にロックを取りに行っているかを探します。グラフ全体を見る前の入口として使うと、調査がかなり楽になります。

専用のExtended Eventsを作る方法

本番で継続的に調査するなら、デッドロック専用のExtended Eventsセッションを作ると扱いやすくなります。ファイル出力にしておくと、リングバッファより後から追いやすいです。

create-deadlock-xevent.sql
CREATE EVENT SESSION [capture_deadlock] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file(
    SET filename = N'C:\Temp\capture_deadlock.xel',
        max_file_size = 20,
        max_rollover_files = 5
)
WITH (STARTUP_STATE = ON);

ALTER EVENT SESSION [capture_deadlock] ON SERVER STATE = START;

出力先のパスはSQL Serverサービスアカウントが書き込める場所にします。調査が終わったら、必要に応じてセッション停止や削除も検討します。

read-deadlock-xevent.sql
SELECT
    CAST(event_data AS xml) AS deadlock_event
FROM sys.fn_xe_file_target_read_file(
    N'C:\Temp\capture_deadlock*.xel',
    NULL,
    NULL,
    NULL
)
WHERE object_name = N'xml_deadlock_report';

権限不足で確認できない場合

Extended EventsやDMVを参照するには、環境によってサーバー状態を参照する権限が必要です。SQL Server 2019以前では VIEW SERVER STATE、SQL Server 2022以降では一部の情報に VIEW SERVER PERFORMANCE STATE が必要になる場合があります。本番環境で権限不足になる場合は、DBAや管理者に読み取り権限の付与を相談してください。

deadlock-permission.sql
-- 権限付与の例。実行は管理者が判断する
GRANT VIEW SERVER STATE TO [login_name];

通常のロック待ちも合わせて確認する

デッドロックが発生する前後には、通常のブロッキングも起きていることがあります。現在進行中のロック待ちは、次のように確認します。

current-blocking.sql
SELECT
    r.session_id,
    r.blocking_session_id,
    r.wait_type,
    r.wait_time,
    r.wait_resource,
    DB_NAME(r.database_id) AS database_name,
    t.text AS running_sql
FROM sys.dm_exec_requests AS r
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE r.blocking_session_id <> 0
ORDER BY r.wait_time DESC;

より詳しいテーブルロックやセッション情報は、SQL Serverのテーブルロック確認SQL で整理しています。デッドロックが頻発する環境では、発生時のデッドロックグラフと通常時のブロッキング状況をセットで見ると原因を絞りやすくなります。

TRY…CATCHでエラー1205を捕捉する

ストアドプロシージャ側でデッドロックを扱う場合は、TRY...CATCHERROR_NUMBER() を確認します。ただし、DB側だけで安易に無限リトライするのは避け、アプリケーション側の再試行方針と合わせます。

try-catch-error-1205.sql
BEGIN TRY
    BEGIN TRAN;

    UPDATE dbo.Orders
    SET status = N'PROCESSING'
    WHERE order_id = @order_id;

    UPDATE dbo.Stocks
    SET quantity = quantity - @quantity
    WHERE item_id = @item_id;

    COMMIT;
END TRY
BEGIN CATCH
    IF XACT_STATE() <> 0
        ROLLBACK;

    IF ERROR_NUMBER() = 1205
    BEGIN
        -- アプリ側で再試行できるように、デッドロックとして扱う
        THROW;
    END;

    THROW;
END CATCH;

ポイントは、例外時にトランザクションを残さないことです。中途半端な状態で接続がプールに戻ると、別の処理にも影響します。

アプリケーション側で再試行する

デッドロックは完全にゼロにできないため、アプリケーション側でエラー1205を安全に再試行できるようにしておくのが実務的です。ただし、何度実行しても問題ない処理、つまり冪等性を確保できる処理だけを自動再試行の対象にします。

再試行で確認すること 理由
最大回数を決める 無限ループを避ける
少し待ってから再試行する 相手トランザクションが完了する時間を作る
待ち時間に揺らぎを入れる 同時再試行でまた衝突するのを避ける
重複登録を防ぐ INSERT系は一意制約や業務キーで二重実行を防ぐ
ログを残す 頻発しているSQLや条件を後から調査する

Microsoftの案内でも、エラー1205を捕捉し、少し待ってから再送する考え方が示されています。ただし、根本原因を直さず再試行だけに頼ると、負荷が高い時間帯に同じ問題が繰り返されます。

再試行処理の考え方

アプリケーション側で再試行する場合は、短い待機を挟み、回数上限を設けます。また、同じタイミングで複数処理が再実行されると再び衝突するため、待機時間に揺らぎを入れるのが無難です。

retry-pseudocode.txt
max_retry = 3

for attempt in 1..max_retry:
    try:
        execute_transaction()
        break
    except SqlError as e:
        if e.number != 1205:
            raise

        if attempt == max_retry:
            raise

        wait_random_milliseconds(500, 2000)

INSERTや外部API連携を含む処理では、再試行による二重実行に注意します。業務キー、一意制約、処理済みフラグなどで、同じ処理が複数回実行されても破綻しない設計にしておきます。

再発防止策

デッドロック対策では、発生したSQLを直すだけでなく、処理設計を見直します。特に効果が高いのは、アクセス順序の統一とトランザクション短縮です。

対策 具体例
アクセス順序を統一する どの処理でも注文→明細→在庫の順に更新する
トランザクションを短くする 画面入力、API通信、ファイル処理をトランザクション外に出す
必要な行だけ更新する WHERE句を明確にし、対象行を絞る
インデックスを見直す 検索条件・JOIN条件に合うインデックスを用意する
一括更新を分割する 長時間ロックを避けるため、バッチサイズを制御する
分離レベルを確認する 不要に強い分離レベルや範囲ロックを避ける

NULL更新や条件付き更新が絡む場合は、SQLのUPDATE文でNULLに更新する方法 のような基本パターンも確認しておくと、不要な全件更新を避けやすくなります。

デッドロック優先度を使う場合

どうしても片方の処理を優先したい場合は、SET DEADLOCK_PRIORITY を使えます。優先度が低いセッションは、デッドロック発生時に被害者として選ばれやすくなります。

deadlock-priority.sql
-- このセッションはデッドロック時に被害者になりやすくする
SET DEADLOCK_PRIORITY LOW;

BEGIN TRAN;
-- 重要度の低い集計・メンテナンス処理など
COMMIT;

ただし、優先度は根本解決ではありません。重要度の低いバッチを犠牲にして業務処理を守る、といった設計上の判断がある場合に限定して使います。

Oracleのデッドロックとの違い

Oracleでは ORA-00060 としてデッドロックが通知されます。SQL Serverでは 1205 が代表的なエラーです。どちらも「相互待ちをDBが検出し、片方を終了する」という考え方は同じですが、調査に使うビューやログ、グラフの見方は異なります。Oracle側の切り分けは Oracleのデッドロック(ORA-00060)の原因と解決方法 にまとめています。

よくある質問

SQL Serverのエラー1205は再実行してよいですか?

処理が冪等で、二重登録や二重更新を防げる設計なら再試行できます。ただし、最大回数、待ち時間、ログ出力を決めておきます。根本原因の調査をせず再試行だけで隠すのは避けます。

デッドロックとブロッキングの違いは?

ブロッキングは一方が待っている状態で、待たせている処理が終われば進みます。デッドロックは待ち関係が循環しており、SQL Serverが片方を被害者として終了します。

デッドロックグラフが見つからない場合は?

system_health の保持範囲から流れてしまった可能性があります。頻発する場合は、専用のExtended Eventsセッションを作ってファイル出力にします。また、参照権限が不足していないかも確認してください。

インデックス不足でデッドロックは起きますか?

起きやすくなります。対象行を探すために広い範囲を読み取ったり更新したりすると、ロック保持時間や競合範囲が広がります。実行計画とインデックスを確認します。

まとめ

SQL Serverのデッドロックは、複数のトランザクションが互いに相手のロック解放を待つことで発生します。エラー1205が返った側は被害者としてロールバックされるため、アプリケーション側ではエラー処理と再試行設計が必要です。

調査では xml_deadlock_report のデッドロックグラフを確認し、被害者、関係したSQL、ロック対象、アクセス順序を見ます。再発防止では、アクセス順序をそろえる、トランザクションを短くする、インデックスを整える、大量更新を分割する、のような設計面の見直しが重要です。