PostgreSQL の 'ERROR: deadlock detected' をトランザクションで修正する

intermediate🐘 PostgreSQL2026-03-17| PostgreSQL 12以降 (Linux/macOS/Windows)、並行トランザクションを使用するすべてのアプリケーション (Django、Rails、Node.js、Java など)

Error Message

ERROR: deadlock detected
#postgresql#deadlock#transaction#concurrency

エラーの内容

ログやアプリケーションの出力に以下のような内容が表示されます:

ERROR: deadlock detected
DETAIL: プロセス 12345 はトランザクション 7890 の ShareLock を待機中; プロセス 67890 によりブロック。
        プロセス 67890 はトランザクション 12345 の ShareLock を待機中; プロセス 12345 によりブロック。
HINT: クエリの詳細についてはサーバーログを参照してください。

PostgreSQL は一方のトランザクション(「被害者」)を強制終了してサイクルを解消します。もう一方は正常にコミットされ、被害者はロールバックされます。アプリケーションは結果の代わりにこのエラーを受け取ります。

デッドロックの原因

典型的なシナリオ:2つのトランザクションが同じ行を逆の順序でロックする場合です。

-- トランザクション A             -- トランザクション B
BEGIN;                       BEGIN;
UPDATE accounts              UPDATE accounts
  SET balance = balance - 50   SET balance = balance - 30
  WHERE id = 1;                WHERE id = 2;  -- 行2をロック
                             -- 次に行1を試みる → Aによりブロック
UPDATE accounts
  SET balance = balance + 30
  WHERE id = 2;  -- 行2を試みる → Bによりブロック
-- デッドロック

どちらのトランザクションも前進できません。PostgreSQL は deadlock_timeout(デフォルト1秒)後にサイクルを検出し、一方を被害者として選択して終了させます。

応急処置:失敗したトランザクションをリトライする

PostgreSQL はすでに被害者をロールバックしています。あとは再実行するだけです。リトライループを追加してください——短いバックオフを挟んだ3回の試行で、ほぼすべての実際のケースに対応できます。

Python (psycopg2)

import psycopg2
from psycopg2 import errors
import time

def run_with_retry(conn, fn, max_retries=3):
    for attempt in range(max_retries):
        try:
            with conn.cursor() as cur:
                fn(cur)
            conn.commit()
            return
        except errors.DeadlockDetected:
            conn.rollback()
            if attempt  setTimeout(r, 100 * (attempt + 1)));
      } else {
        throw err;
      }
    }
  }
}

根本的な修正:一貫した順序で行をロックする

リトライは症状を修正します。デッドロックの発生そのものを防ぐには、常に同じ順序でロックを取得してください——行を操作する前に主キーでソートします。

-- 悪い例: トランザクション A が id=1 → id=2 の順でロック
--         トランザクション B が id=2 → id=1 の順でロック

-- 良い例: 昇順 id で事前に両方の行をロック
BEGIN;
SELECT * FROM accounts
  WHERE id IN (1, 2)
  ORDER BY id
  FOR UPDATE;  -- 行1、次に行2をロック — 常に同じ順序

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

SELECT ... FOR UPDATE ORDER BY id は必要なすべてのロックを予測可能な順序で一度に取得します。このパターンに従う2つのトランザクションは互いにデッドロックしません——一方がもう一方の完了を待つだけです。

トランザクションを短く保つ

長時間実行されるトランザクションはロックを長く保持し、衝突の可能性が高まります。いくつかの簡単なルールでデッドロックの頻度を大幅に削減できます:

  • すべての計算はトランザクションを開く前に実行してください——トランザクション内ではなく。
  • トランザクション内でネットワーク呼び出しやユーザー入力の待機を行わないでください。
  • ジョブキューには FOR UPDATE SKIP LOCKED を使用し、他のワーカーがすでに取得した行をスキップするようにしてください。
-- ジョブキュー: 他のワーカーをブロックせずに保留中の行を1件取得
SELECT id, payload FROM jobs
  WHERE status = 'pending'
  ORDER BY id
  LIMIT 1
  FOR UPDATE SKIP LOCKED;

デッドロックが発生しているクエリを特定する

何かを書き換える前に、デッドロックが実際にどこで発生しているか確認してください。2つのアプローチがあります:

方法1 — ロギングを有効にする(再起動後も持続、本番環境の監視に最適):

-- postgresql.conf または ALTER SYSTEM SET
deadlock_timeout = 200ms     -- デフォルトは1s; より多く検出するには低く設定
log_lock_waits = on
log_min_duration_statement = 1000  -- 1秒より遅いクエリをログ

方法2 — ライブクエリ(再起動不要、現在ブロックされているものを表示):

SELECT
  blocked.pid,
  blocked.query  AS blocked_query,
  blocking.pid   AS blocking_pid,
  blocking.query AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;

修正を確認する

  • まず再現する:古い順序で2つの並行トランザクションを実行し、ERROR: deadlock detected が発生することを確認します。
  • 修正を適用する——一貫したロック順序または事前の FOR UPDATE
  • テストを再実行する:今度はデッドロックエラーが発生しないことを確認します。
  • 本番ログを監視する:デプロイ後、deadlock detected を grep します。表示されなくなれば、根本原因に対処できています。
-- スモークテスト: 2つの psql タブを開いて同時に実行

-- タブ 1:
BEGIN;
SELECT * FROM accounts WHERE id IN (1,2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;

-- タブ 2 (同じロック順序 = デッドロックなし):
BEGIN;
SELECT * FROM accounts WHERE id IN (1,2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance + 30 WHERE id = 1;
UPDATE accounts SET balance = balance - 30 WHERE id = 2;
COMMIT;

タブ2はタブ1がロックを保持している間一時停止し、その後正常に続行します。エラーも被害者もなく——予想される待機して続行する動作だけです。

Related Error Notes