Claude Codeでデッドロックの原因を調べる手順 — PostgreSQLとMySQLのログから
PostgreSQLのdeadlock detectedログとMySQLのSHOW ENGINE INNODB STATUSをclaude -pに渡し、誰が誰を待っているかを表にして、ロック取得順の修正案まで出す手順です。
デッドロックのログは、そのまま読むと「プロセス番号とトランザクション番号の羅列」です。Claude Codeに読ませるときも、まず「誰が誰のロックを待っているか」の表に起こさせると、原因の見落としが減ります。ここでは、PostgreSQLのログとMySQL(InnoDB)のSHOW ENGINE INNODB STATUSをclaude -pに渡し、待ちグラフを作り、ロック取得順を直す修正案まで進める手順を扱います。
デッドロックの原因は「待ちグラフ」に起こすと見える
デッドロックとは、複数のトランザクションがそれぞれ相手の欲しいロックを握ったまま待ち合い、どれも先へ進めなくなる状態です。MySQLのマニュアルは、複数テーブルの行を逆の順序でロックしたときに起きると説明しています。範囲ロックやギャップのロックがタイミング次第で食い違ったときにも起きます。
どちらのDBもデッドロックを自動で検出し、片方を巻き戻します。PostgreSQLは「どのトランザクションが中断されるかは予測しづらく、当てにしてはいけない」と書いています。MySQLのInnoDBは、挿入・更新・削除した行数が少ない小さなトランザクションを巻き戻す側に選ぼうとします。つまり、ログでエラーを受けた側が「原因を作った側」とは限りません。
だから読み解きの出発点は、エラーになった文ではなく、次の3点の表です。
- 待っているトランザクション(またはプロセス)
- 待たれているトランザクション
- それぞれが握っているロックと、欲しいロック
この表の矢印が輪になっていれば、その輪の中でロックを取る順序が食い違っています。直し方はほぼ一つに絞れます。全員が同じ順序でロックを取るようにそろえるのです。
PostgreSQLで原因を追えるログ設定にする
PostgreSQLのデッドロックは、検出された時点でエラーとしてサーバーログに出ます。エラーコードは40P01(deadlock_detected)です。ただ、既定のログ設定だと「誰が」「どの接続から」が追いにくいので、先に次を確認します。
| 設定 | 役割 | 既定 |
|---|---|---|
log_line_prefix | 役割各行の先頭に時刻・プロセスID・ユーザー・DB・アプリ名を付ける | 既定'%m [%p] ' |
log_lock_waits | 役割deadlock_timeoutを超えてロック待ちした場合にもログを出す | 既定off |
deadlock_timeout | 役割ロック待ちしてから、デッドロックかを調べに行くまでの時間 | 既定1秒 |
log_line_prefixはpostgresql.confかサーバー起動時のコマンドラインでしか設定できません。公式のマニュアルには'%m [%p] %q%u@%d/%a 'という例があり、時刻・プロセスID・ユーザー・DB・アプリ名が並びます。%aで見えるアプリ名は、接続側でapplication_nameを設定しておかないと空になります。
# postgresql.conf
log_line_prefix = '%m [%p] %q%u@%d/%a '
log_lock_waits = on
deadlock_timeout = 1slog_lock_waitsは、デッドロックにならなかった長いロック待ちも記録します。マニュアルによれば、ロック待ちの調査ではdeadlock_timeoutを通常より短くするとよい、とあります。ただしdeadlock_timeoutを下げるとデッドロック検査の回数が増えます。検査は比較的重い処理なので、本番では短くしすぎないのが無難です。
ログには、たとえば次のような形で出ます。これは説明用に組んだ例で、行の並びは環境によって変わります。
2026-10-27 10:15:02.123 JST [8812] app@shop/orders ERROR: deadlock detected
2026-10-27 10:15:02.123 JST [8812] app@shop/orders DETAIL: Process 8812 waits for ShareLock on transaction 7451; blocked by process 8790.
Process 8790 waits for ShareLock on transaction 7452; blocked by process 8812.
Process 8812: UPDATE accounts SET balance = balance + 100 WHERE id = 2;
Process 8790: UPDATE accounts SET balance = balance + 100 WHERE id = 1;プロセス8812が8790を待ち、8790が8812を待つ輪ができています。しかもそれぞれの文はid = 2とid = 1という逆順の更新です。この例なら人間が一目で読めますが、実ログは同時刻に別の接続のエラーが混ざり、数十行になります。ここでClaude Codeに整理を任せます。
MySQLはSHOW ENGINE INNODB STATUSの該当節を取り出す
InnoDBで直近のデッドロックを見るには、SHOW ENGINE INNODB STATUSを実行します。出力のLATEST DETECTED DEADLOCK節に、関与したトランザクション、各トランザクションが実行しようとした文、握っているロックと欲しいロック、巻き戻された側が載ります。デッドロックが一度も起きていなければ、この節自体が出ません。
注意点が3つあります。
- この節に残るのは直近の1件だけです。過去の分を追うには
innodb_print_all_deadlocksを有効にして、すべてをエラーログへ出す必要があります - デッドロック検出が有効(既定)でも、
LOCK TABLESで取ったテーブルロックや、InnoDB以外のストレージエンジンのロックが絡む場合、InnoDBは検出できません。innodb_lock_wait_timeoutのタイムアウトに頼ることになります - 待ちリストが200トランザクションを超えるか、100万を超えるロックを調べる必要があると、
TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPHというメッセージが出て、調べた側が巻き戻されます。これは本物のデッドロックではなく、検出の打ち切りです
mysql -e 'SHOW ENGINE INNODB STATUS\G' > innodb-status.txt\Gで縦に出力したテキストには、LATEST DETECTED DEADLOCK以外の節(TRANSACTIONS、FILE I/Oなど)も含まれます。全文を渡す前に、LATEST DETECTED DEADLOCKから次の節までを切り出しておくと、不要な情報を渡さずに済み、読ませる量も減ります。
claude -pにログを渡して待ちグラフを起こす
ここからがClaude Codeの出番です。非対話モードのclaude -pは標準入力を読めるので、ログをパイプでそのまま渡せます。公式の例にも、ビルドエラーのログを渡して原因を説明させる使い方があります。
cat pg-deadlock.log | claude -p \
'待ちグラフを表にして。列は待つ側・待たれる側・ロック対象・実行中の文。\
ログに無い事柄は書かない' > graph.md効かせたい指示は2つです。
- 表の列を指定する。形が決まっていれば、人間が輪を見つけやすく、別のログと見比べやすくなる
- 「ログに無い事柄は書かない」と明示する。ロックの対象テーブルやインデックスはログから読める範囲だけにさせる
結果を後続のスクリプトで扱いたいなら、--output-format jsonに--json-schemaを組み合わせて、辺のリストを取り出せます。応答のstructured_outputフィールドに入ります。
cat pg-deadlock.log | claude -p 'ログから待ち関係を抽出して' \
--output-format json \
--json-schema '{"type":"object","properties":{"edges":{"type":"array","items":{"type":"object","properties":{"waiter":{"type":"string"},"holder":{"type":"string"},"statement":{"type":"string"}},"required":["waiter","holder"]}}},"required":["edges"]}' \
| jq '.structured_output.edges'スキーマの検証に通らず構造化出力が得られなかったときは、structured_outputが応答に含まれません。スクリプトで使う前に、フィールドの有無を確かめてください。
標準入力の上限と、CIでの起動
パイプで渡せるのは10MBまでで、超えるとエラーで終了します。長期間のログは、デッドロックの前後数分だけをgrepやawkで切り出してから渡すか、ファイルに書き出してパスをプロンプトに書く方法をとります。
CIなど無人の環境では、--bareを付けるとホスト側のフック・プラグイン・自動メモリ・CLAUDE.mdを読み込まずに起動します。ログ解析のように毎回同じ条件で動いてほしい処理には向いています。
ロックの取得順を直す修正案に進む
待ちグラフに輪が見つかったら、次は修正です。PostgreSQLの公式マニュアルは、デッドロックへの最善の防御として「複数のオブジェクトに対するロックを、すべてのアプリケーションで一貫した順序で取ること」を挙げています。先ほどの例でも、両方のトランザクションが同じ順序で行を更新すれば、デッドロックは起きなかったと説明しています。
同じ節には、あと2つの考え方があります。
- 一つのオブジェクトに最初に取るロックは、トランザクション内で最終的に必要になる最も強いモードにしておく
- 事前に確かめきれないときは、デッドロックで中断されたトランザクションを再試行する
MySQLのマニュアルも方向は同じです。小さなトランザクションにして長く開きっぱなしにしない、複数テーブルや広い範囲を更新するときはどのトランザクションも同じ順序で操作する、SELECT ... FOR UPDATEやUPDATE ... WHEREの条件列にインデックスを張る、の3点を挙げています。デッドロックの起こりやすさは分離レベルでは変わらない、という記述もあります。分離レベルは読み取りの挙動を変え、デッドロックは書き込みから起きるためです。
Claude Codeに修正案を出させるときは、ログだけでなく、問題の文を発行しているコードも読ませます。たとえば次のように頼みます。
claude -p 'graph.mdの輪を作っている2つの更新処理をsrc/から探し、\
ロックを取る順序を固定する修正案をdiffで出して。\
並べ替えの基準になるキーも説明して' \
--allowedTools "Read,Grep,Glob"--allowedToolsに読み取り系だけを列挙しているのは、この段階では修正案を見たいだけで、ファイルを書き換えさせたくないためです。許可の考え方はallowedTools/disallowedToolsの記事、モードの選び方は--permission-modeの記事にまとめています。
ロックを取る順序を固定する例
修正前
呼び出し元が渡した順に更新するため、transfer(1, 2)とtransfer(2, 1)が同時に走ると、片方がid=1、もう片方がid=2から先に更新して輪ができる。
修正後
更新対象のidを昇順に並べ替えてから、その順にロックを取る。どの呼び出しもid=1の次にid=2を取るので、待ち合いの輪はできない。
コード側の形は、たとえば次のようになります。これはClaude Codeに出させる修正案のイメージで、実際の出力はコードベースによって変わります。
// 修正前: 引数の順でロックを取る
await tx.query("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [amt, from]);
await tx.query("UPDATE accounts SET balance = balance + $1 WHERE id = $2", [amt, to]);
// 修正後: id の昇順で取る
const [first, second] = from < to ? [from, to] : [to, from];
await tx.query("SELECT 1 FROM accounts WHERE id IN ($1, $2) ORDER BY id FOR UPDATE", [first, second]);
await tx.query("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [amt, from]);
await tx.query("UPDATE accounts SET balance = balance + $1 WHERE id = $2", [amt, to]);ただし、並べ替えただけで安心はできません。ORDER BY付きのFOR UPDATEでロックが本当にその順で取られるかは、実行計画や条件で変わりえます。修正後は、同じ2つのトランザクションを並行に流して再現させ、デッドロックが消えたことを確かめてください。
ログだけでは決められないこと
Claude Codeは、渡されたログの範囲でしか判断できません。読み解きが外れやすい場面を挙げます。
落とし穴
ログに出た文が、ロックを取った文とは限らない
デッドロックのログに載る文は、待ちが発生した場面の文です。そのトランザクションが先に取ったロックは、もっと前の別の文で取られているかもしれません。ログの文だけで「この文が悪い」と決めず、トランザクションの前半の文もアプリのコードか、log_statementなどの文ログで確かめます。
次の表は、ログの種類ごとに分かることと、追加で渡すべき材料の対応です。
| 渡すもの | 分かること | 分からないこと |
|---|---|---|
PostgreSQLのdeadlock detectedログ | 分かること輪を作ったプロセスと、その時点の文 | 分からないこと各トランザクションが先に取ったロック |
log_lock_waitsのログ | 分かることデッドロックに至らない長い待ち | 分からないこと待たせている側の全体像 |
LATEST DETECTED DEADLOCK | 分かること関与した文、握っているロックと待つロック、巻き戻された側 | 分からないこと直近1件以外の過去の事例 |
| アプリのコード | 分かることロックを取る順序の設計 | 分からないこと実際に何が同時に走ったか |
ログと実装の両方を渡して初めて、「どの経路が逆順なのか」まで言えます。逆に言えば、ログだけを渡して修正案まで出させる使い方は、推測が混ざりやすくなります。待ちグラフを作る段階と、修正案を出す段階を分けて実行するのはそのためです。
ログを渡す前に確認しておくこと
文のログには、バインドされた値が入ることがあります。PostgreSQLのlog_parameter_max_lengthは、文ログに出るバインド値を指定バイト数で切る設定で、0にすると、エラーを伴わない文ログにバインド値を記録しません。顧客のメールアドレスや氏名がWHERE句に入る検索があるなら、claude -pに渡す前にマスクするか、値を切った設定でログを取り直すのが確実です。
DB自体の接続情報はClaude Codeに渡さず、ログのテキストとコードだけを読ませる構成にすると、解析中にDBへ書き込む経路が最初からありません。DBへ直接つないで調べさせたい場合は、読み取り専用のユーザーを用意するなど、別の統制が要ります。PostgreSQLのスキーマ側から整えたい場合は、pg-aiguideの導入記事も参考になります。
また、-pの連続実行は、対話より利用枠を多く使う報告もあります。大量のログを何度も回す場合は、claude -pの5時間枠の記事の切り分けを見てから回すと、想定外の消費を避けられます。
まとめ
デッドロックの調査は、ログを待ちグラフの表に起こし、輪を作っているロックの順序を見つけ、その順序をそろえる、という流れです。Claude Codeには、表への整形と、コードのどこが逆順かの探索を任せます。ログに載っていない部分は推測になるため、表を作る段階と修正案の段階を分け、最後は必ず並行実行で再現して確かめます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- code.claude.com/docs/en/headless
- dev.mysql.com/doc/refman/8.4/en/innodb-deadlocks.html
- dev.mysql.com/doc/refman/8.4/en/innodb-deadlock-detection.html
- dev.mysql.com/doc/refman/8.4/en/innodb-standard-monitor.html
- postgresql.org/docs/current/runtime-config-logging.html
- postgresql.org/docs/current/runtime-config-locks.html
- postgresql.org/docs/current/explicit-locking.html