テック技報最新号へ →
コード

Postgres: FOR UPDATE SKIP LOCKED でテーブルをジョブキューにする

ワーカーが SELECT ... FOR UPDATE SKIP LOCKED LIMIT n でジョブを取ると、ほかのワーカーがロック中の行は飛ばして次の行を取る。ワーカー同士が互いの行で待たされず、同じジョブを二重に取ることもない。

@amitsinha__07 · 投稿 2026-11-03
#postgres#job-queue#locking#infra

使う場面

Redis や専用のキューを足すほどではない規模のバックグラウンドジョブ(メール送信、画像変換、外部 API の呼び出しなど)。業務データと同じ DB に置けるので、ジョブの登録を業務データの更新と同じトランザクションで書ける。

やり方

sql
-- ワーカーごとに 1 トランザクション
BEGIN;

SELECT id, payload
FROM jobs
WHERE status = 'ready'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 10;

-- 取れた行を処理中にして、すぐコミットする
UPDATE jobs SET status = 'running', started_at = now()
WHERE id = ANY($1);

COMMIT;
-- 実際の処理はトランザクションの外で行い、終わったら status = 'done' に更新する

1 文で書くなら次の形もよく使われる。

sql
UPDATE jobs SET status = 'running', started_at = now()
WHERE id IN (
  SELECT id FROM jobs
  WHERE status = 'ready'
  ORDER BY id
  FOR UPDATE SKIP LOCKED
  LIMIT 10
)
RETURNING id, payload;

インデックスは「ready の行を id 順に探す」ために付ける。

sql
CREATE INDEX jobs_ready_idx ON jobs (id) WHERE status = 'ready';

注意点

  • ジョブを取るトランザクションは短くする。処理の間ずっと行ロックを持つと、接続を占有し、VACUUM も遅れる。上の例のように「取って running にしてコミット」まででトランザクションを閉じる。
  • その代わり、ワーカーが途中で落ちると running のまま残る。started_at が古い running の行を ready に戻す見回り処理を用意する。処理は二回実行されても壊れない(冪等な)形にしておく。
  • SKIP LOCKED を付けると、ロック中の行は結果から黙って消える。取り出しの順序は厳密ではなくなるので、順序が重要な処理には向かない。
  • 終わった行を残し続けるとテーブルが膨らむ。done の行は定期的に消すか別テーブルへ移す。
  • PgBouncer の transaction モードでも使える(行ロックはトランザクションの中で完結するため)。セッション単位の機能との違いは Postgres: PgBouncer の transaction モードではセッション状態が別の接続に漏れる。SET LOCAL と xact 系ロックを使う を参照。イベントを外部へ確実に送る用途は Transactional outbox: DB の更新とイベント送信を同じトランザクションに入れる が同じ仕組みを使う。
  • キューのテーブルを作るほどでもなく「夜間バッチは同時に 1 つだけ」としたいだけなら、アドバイザリーロックで足りる。SELECT pg_try_advisory_lock(hashtext('nightly-report')) が false なら、ほかのワーカーが実行中なのでそのまま終了する(@james_ailton)。PgBouncer の transaction モードではセッション単位のロックが使えないので、トランザクションの中で pg_try_advisory_xact_lock を使う。

出典

X の元の投稿を開く