PostgreSQL『ERROR: cannot execute SELECT FOR UPDATE』の原因と対処
- 作成日 2025.09.10
- 更新日 2025.10.06
- その他
SELECT … FOR UPDATE/NO KEY UPDATE/SHARE/KEY SHARE で行ロックを取得しようとした際、トランザクション状態や接続先、クエリ形状の制約により実行できないときに発生する。代表メッセージ別に切り分け、最短復旧手順、正しいクエリ形、代替手段(UPDATE … RETURNING / SKIP LOCKED / アドバイザリロック)までを網羅した。
- 1. 代表メッセージと意味(まずどれが出ているかを確認)
- 2. 発生条件の早見表
- 3. まず実行:接続とTxの状態を即チェック
- 4. 最短復旧:READ ONLY/レプリカが原因のとき
- 5. 正しいクエリ形:FOR UPDATEは最外側で、対象テーブルを明示
- 6. 集約/集合演算・複雑ビューでの制約と対処
- 7. 業務実装の定番:ワークキュー(SKIP LOCKED/NOWAIT)
- 8. FOR UPDATE OF の正しい指定(別名/存在チェック)
- 9. 並列実行との相性が悪い場合のワークアラウンド
- 10. READ ONLYが避けられない構成での代替設計
- 11. 再現→解消の通し例(READ ONLY / レプリカ / サブクエリ)
- 12. チェックリスト(上から順に実施)
- 13. まとめ:最短復旧から恒久対策まで
代表メッセージと意味(まずどれが出ているかを確認)
ERROR: cannot execute SELECT FOR UPDATE in a read-only transaction
ERROR: cannot execute SELECT FOR UPDATE during recovery
ERROR: FOR UPDATE is not allowed in subqueries
ERROR: FOR UPDATE cannot be applied to a query with aggregate/grouping
ERROR: relation "t" in FOR UPDATE clause not found in FROM clause・read-only:現在のTx/セッションが読取専用
・during recovery:接続先がリカバリ中(レプリカ/ホットスタンバイ)
・subqueries/aggregate:クエリ形状が不正(最外側でない、元行が一意に特定不能)
・relation not found:FOR UPDATE OF の対象がFROMに存在しない(別名不一致等)
発生条件の早見表
□ BEGIN READ ONLY / SET TRANSACTION READ ONLY / default_transaction_read_only=on
□ リードレプリカへ接続(pg_is_in_recovery()=true)
□ サブクエリやビュー内部でFOR UPDATEを記述(最外側SELECTでない)
□ GROUP BY/集約/UNION/INTERSECT/EXCEPT等で基底行が特定不能
□ JOIN時に FOR UPDATE OF の対象/別名の指定ミス
□ FDW/複雑ビュー/並列計画との相性問題(実行環境依存)まず実行:接続とTxの状態を即チェック
-- 読取専用か
SHOW transaction_read_only; -- on なら READ ONLY
SHOW default_transaction_read_only; -- 既定値
-- レプリカか
SELECT pg_is_in_recovery(); -- true ならレプリカ(FOR UPDATE不可)
-- psqlで接続情報
\conninfo最短復旧:READ ONLY/レプリカが原因のとき
-- READ ONLYで開始してしまったケース
ROLLBACK;
BEGIN; -- または BEGIN READ WRITE;
SET TRANSACTION READ WRITE;
-- ここで SELECT ... FOR UPDATE を再実行
-- セッション既定を当面だけ変更
SET default_transaction_read_only = off;
-- レプリカ接続だった場合:writerへ接続先を切替
-- libpq 例:書込可能ノードを自動選別
-- postgresql://user@host1,host2/db?target_session_attrs=read-write正しいクエリ形:FOR UPDATEは最外側で、対象テーブルを明示
-- OK:最外側SELECTでFOR UPDATE、行の出所がt1に対応
SELECT t1.*
FROM t1
WHERE t1.status = 'ready'
FOR UPDATE;
-- OK:JOIN時は対象を限定(t1だけロック)
SELECT t1.*, t2.*
FROM t1 JOIN t2 ON t1.id = t2.t1_id
WHERE t1.status = 'ready'
FOR UPDATE OF t1;
-- NG:サブクエリ内のFOR UPDATE
SELECT * FROM (
SELECT * FROM t1 WHERE status='ready' FOR UPDATE
) s; -- ERROR: FOR UPDATE is not allowed in subqueries集約/集合演算・複雑ビューでの制約と対処
-- NG:GROUP BY 直上
SELECT user_id, count(*) FROM logs
GROUP BY user_id
FOR UPDATE; -- エラー
-- 対処:代表行を取りたいならウィンドウ関数やDISTINCT ONへ
WITH ranked AS (
SELECT l.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY ts DESC) rn
FROM logs l
)
SELECT * FROM ranked WHERE rn=1 FOR UPDATE; -- 最外側で・非更新可能ビュー/FDWはFOR UPDATE非対応のことがある→基底テーブルに当てるか、UPDATE … RETURNINGへ寄せる
業務実装の定番:ワークキュー(SKIP LOCKED/NOWAIT)
-- 競合を待たずに取り出す(ジョブピック)
BEGIN;
WITH picked AS (
SELECT id
FROM jobs
WHERE status='ready'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 10
)
UPDATE jobs j
SET status='running'
FROM picked p
WHERE j.id = p.id
RETURNING j.*;
COMMIT;
-- 即時エラーにしたいとき
SELECT * FROM accounts WHERE id=1 FOR UPDATE NOWAIT;・「SELECT … FOR UPDATE … LIMIT … SKIP LOCKED」は可
・最終更新は UPDATE … RETURNING に寄せると一貫性が高い
FOR UPDATE OF の正しい指定(別名/存在チェック)
-- FROMで使っている別名を指定
SELECT *
FROM orders o
JOIN customers c ON c.id = o.customer_id
FOR UPDATE OF o; -- OK
-- NG:FROMにない名前
FOR UPDATE OF payments; -- ERROR: relation ... not found in FROM clause並列実行との相性が悪い場合のワークアラウンド
-- セッション単位で並列を避ける
SET max_parallel_workers_per_gather = 0;
-- またはクエリを書き換えて並列計画にならないよう抑制・環境/バージョン依存のため、ロック取得系は非並列にする方が無難
READ ONLYが避けられない構成での代替設計
-- 代替1:UPDATE ... RETURNING で直接更新と取得を一体化(writerで実行)
WITH cte AS (
SELECT id FROM t WHERE picked=false
ORDER BY id
LIMIT 10
FOR UPDATE SKIP LOCKED
)
UPDATE t
SET picked=true
FROM cte
WHERE t.id = cte.id
RETURNING t.*;
-- 代替2:アドバイザリロック(プロセス間の調停)
SELECT pg_try_advisory_lock(42); -- 取得できたら続行
-- ・・・処理・・・
SELECT pg_advisory_unlock(42);・レプリカでは行ロック不可→ロックを伴う処理はwriter側へルーティング
再現→解消の通し例(READ ONLY / レプリカ / サブクエリ)
-- 1) READ ONLYでの再現と解消
BEGIN READ ONLY;
SELECT * FROM t FOR UPDATE;
-- ERROR: cannot execute SELECT FOR UPDATE in a read-only transaction
ROLLBACK;
BEGIN;
SET TRANSACTION READ WRITE;
SELECT * FROM t FOR UPDATE; -- OK
COMMIT;
-- 2) レプリカでの再現
SELECT pg_is_in_recovery(); -- true ならレプリカ
SELECT * FROM t FOR UPDATE; -- ERROR: ... during recovery
-- 3) サブクエリ内でのNG
SELECT * FROM (SELECT * FROM t WHERE status='ready' FOR UPDATE) s;
-- ERROR: FOR UPDATE is not allowed in subqueriesチェックリスト(上から順に実施)
□ SHOW transaction_read_only / default_transaction_read_only を確認し READ WRITE に
□ SELECT pg_is_in_recovery() でレプリカ接続を排除(writerへ / target_session_attrs=read-write)
□ FOR UPDATE が最外側SELECTであるかを確認
□ JOIN時は FOR UPDATE OF <別名> で対象テーブルを限定
□ GROUP BY/UNION等の直上にFOR UPDATEを置いていないか
□ ビュー/FDW経由なら基底テーブルへ当てる or UPDATE … RETURNINGへ寄せる
□ SKIP LOCKED / NOWAIT で競合ポリシーを明示
□ 必要なら並列計画を抑止(max_parallel_workers_per_gather=0)
まとめ:最短復旧から恒久対策まで
・原因特定:read-only/レプリカ/クエリ形状のどれか
・即応:ROLLBACK→READ WRITEでやり直し/writerへ接続切替
・設計:FOR UPDATEは最外側+OFで対象限定、更新はUPDATE … RETURNINGへ
・運用:接続先制御(target_session_attrs=read-write)、競合戦略(SKIP LOCKED/NOWAIT)
・拡張:ビュー/FDW/並列の制約は回避設計で吸収
-
前の記事
PostgreSQL『ERROR: operator does not exist』の原因と対処 2025.09.09
-
次の記事
PostgreSQLでのエラー『 table referenced in 』の原因と対処 2025.09.10
コメントを書く