PostgreSQLのエラー『deadlock detected』の解決方法

PostgreSQLのエラー『deadlock detected』の解決方法

このエラーは、複数のトランザクションが互いにロックを取り合い、先に進めずにデッドロック状態に陥ったときにPostgreSQLが強制的にどちらか一方のトランザクションを終了させて発生する。特に同時書き込みや更新が多発するWebアプリやAPI環境では頻出する。エラーの内容とその発生条件、回避および解決のための実践的な対処法をまとめる。

目次

エラーメッセージの例と発生条件

ERROR:  deadlock detected
DETAIL:  Process 1234 waits for ShareLock on transaction 5678; blocked by process 4321.
Process 4321 waits for ShareLock on transaction 1234; blocked by process 1234.

このメッセージは、2つ以上のプロセスが相互にロックを要求しあい、待機状態から抜け出せなくなった場合に表示される。

原因1: 異なる順序でリソースをロックしている

-- トランザクションA
BEGIN;
UPDATE users SET name = 'Alice' WHERE id = 1;
UPDATE orders SET status = 'shipped' WHERE id = 100;

-- トランザクションB(順序が逆)
BEGIN;
UPDATE orders SET status = 'delivered' WHERE id = 100;
UPDATE users SET name = 'Bob' WHERE id = 1;

解決策1: ロックするテーブルや行の順序を統一する

常に同じ順番でリソースを取得するように設計することでデッドロックの可能性を下げられる。

原因2: 複数の行をまたいだ同時更新

大量のデータを処理するバッチ処理や一括更新が競合している場合に発生する。

解決策2: 更新対象を絞り、一度に処理する件数を減らす

-- 1000件を一括で更新するのではなく、100件ごとに分割する
UPDATE items SET price = price * 1.1 WHERE category = 'tools' LIMIT 100;

原因3: 明示的なロック(SELECT FOR UPDATE)が重なっている

-- 同じ行をSELECT FOR UPDATEでロックし、別トランザクションが同時にアクセス
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;

解決策3: SELECT FOR UPDATEを使用する際は排他設計を意識

同じ行をロックする可能性のあるコードをできるだけ直列化する。

原因4: ロック保持時間が長すぎる

処理が複雑でトランザクションを長時間保持してしまい、他のトランザクションと競合する。

解決策4: トランザクションの範囲を最小限に絞る

-- 悪い例(複数の操作を長時間実行)
BEGIN;
…複雑な処理…
UPDATE table SET …;
COMMIT;

-- 良い例(短時間でロック)
BEGIN;
UPDATE table SET …;
COMMIT;

原因5: アプリケーション層のリトライ処理が無限ループしている

デッドロック検知後に自動リトライして再び同じパターンで競合する。

解決策5: リトライは回数制限と待機時間(バックオフ)を設ける

-- 擬似コード
for i in 1..3
try
update_db()
break
catch DeadlockError
sleep(random(50..200))

原因6: 複数スレッド・プロセスでの並列実行が同一レコードに集中

ジョブやAPIが並列実行されて、偶然同じデータにアクセスすることで競合が起きる。

解決策6: シャーディングまたは排他制御(mutex)をアプリ側で導入

対象IDのハッシュ値などでアクセス先を分散させる。

原因7: アップサート(INSERT ON CONFLICT)でロック競合

INSERT INTO items (id, stock) VALUES (1, 10)
ON CONFLICT (id) DO UPDATE SET stock = EXCLUDED.stock;

この処理が同じidに対して同時に実行されるとデッドロックになる。

解決策7: UPSERT対象のIDを事前にロックまたは直列化する

-- INSERT前にロック
SELECT * FROM items WHERE id = 1 FOR UPDATE;

原因8: 外部キー制約による暗黙的なロック

子テーブルを先に更新して親テーブルを削除するなど、制約上順序が逆の場合に競合する。

解決策8: 外部キーのロック順も意識した処理順にする

親テーブル → 子テーブルの順で操作を統一する。

原因9: ANALYZEやVACUUMなどのメンテナンス処理との競合

PostgreSQLの内部処理とアプリケーションの更新処理が衝突する場合もある。

解決策9: 大量更新中はメンテナンス処理を避ける

cronなどで処理時間帯をずらす。

原因10: トランザクションの中で複数の関数が複雑な更新を行っている

1つのトランザクション内で多階層的に更新・参照が入り組むことでロックが競合する。

解決策10: トランザクションの中での関数呼び出しを整理し、処理を単純化

関数の中で再度BEGIN/COMMITを使わず、1トランザクションで完結させる。

まとめ

『deadlock detected』エラーは複数のトランザクションが互いにロックを要求し、待機状態から抜け出せなくなったときに発生する。リソースアクセスの順序、トランザクションの粒度、リトライ設計などを見直すことで多くのケースは回避できる。ログのDETAIL情報をよく確認し、再現性のある状況ではロック順の統一を徹底するのが重要。