PostgreSQL『ERROR: update or delete on table violates foreign key constraint』の原因と対処

PostgreSQL『ERROR: update or delete on table violates foreign key constraint』の原因と対処

親テーブルの行をUPDATE/DELETEしたとき、子テーブルの外部キー(FK)が参照整合性を守れないと発生する。代表メッセージの読み方、発生条件、最短復旧、堅牢な削除/再割当て手順、CASCADE/SET NULL/SET DEFAULT/RESTRICTの使い分け、DEFERRABLEやインデックス設計、実運用のチェックリストまでをまとめた。

エラーの読み方と代表メッセージ

ERROR:  update or delete on table "parents" violates foreign key constraint "children_parent_id_fkey" on table "children"
DETAIL:  Key (id)=(42) is still referenced from table "children".

・どの親テーブル行(Key (id)=(42))を、どの子テーブル/制約名(children / children_parent_id_fkey)が参照しているかが示される
・UPDATEで主キー/参照キーを変更した場合も同様に発生

発生条件の早見表

□ 親行を削除しようとしたが、子テーブルに参照行が残っている
□ 親の主キー/候補キーを更新したが、FKが ON UPDATE CASCADE でない
□ 子のFK列がNOT NULLなのに、ON DELETE SET NULLを期待
□ 複合FKで一部列だけを変更(MATCHと整合せずNG)
□ 参照側にFK用インデックスがなく、ロック競合や遅延で失敗/タイムアウト
□ トリガ/ビュー/FDW経由の間接更新で一貫性が崩れる

まず実行:原因の子行と制約を特定(最短復旧の入口)

-- 1) 直近エラーの制約名を控える(例: children_parent_id_fkey)

-- 2) 参照している子行を特定
SELECT * FROM children WHERE parent_id = 42;

-- 3) 制約の定義を確認
SELECT conname, confdeltype, confupdtype, condeferrable, convalidated
FROM pg_constraint
WHERE conname = 'children_parent_id_fkey';

-- 4) psqlでテーブル定義
\d children

方針A:親の削除に追随させる(ON DELETE CASCADE)

-- 既存FKをCASCADEへ付け替え(安全確認後)
ALTER TABLE children
  DROP CONSTRAINT children_parent_id_fkey,
  ADD  CONSTRAINT children_parent_id_fkey
    FOREIGN KEY (parent_id)
    REFERENCES parents(id)
    ON DELETE CASCADE;

-- 実行前の影響把握(消える行数を試算)
SELECT COUNT(*) FROM children WHERE parent_id = 42;

-- 削除
DELETE FROM parents WHERE id = 42;  -- 子も連鎖削除

・大量連鎖や多段連鎖がある場合は必ず件数/インデックス/トランザクション境界を検証
・子側のFK列にインデックスがあるとロック時間とコストが大幅に改善

CREATE INDEX IF NOT EXISTS idx_children_parent_id ON children(parent_id);

方針B:親の削除時に子の参照を切る(ON DELETE SET NULL/SET DEFAULT)

-- NULLにできるなら
ALTER TABLE children
  DROP CONSTRAINT children_parent_id_fkey,
  ADD  CONSTRAINT children_parent_id_fkey
    FOREIGN KEY (parent_id)
    REFERENCES parents(id)
    ON DELETE SET NULL;  -- parent_id は NULL 許容が必須

-- 既定値に戻すなら
ALTER TABLE children
  ALTER COLUMN parent_id SET DEFAULT 1; -- 例: ダミー親ID
ALTER TABLE children
  DROP CONSTRAINT children_parent_id_fkey,
  ADD  CONSTRAINT children_parent_id_fkey
    FOREIGN KEY (parent_id) REFERENCES parents(id)
    ON DELETE SET DEFAULT;

・SET NULLはFK列がNULLABLEであること
・SET DEFAULTは既定値が有効な親を指すことが前提

方針C:親を消さず「子を別の親へ再割当て」する(再親化)

-- 子の参照先を先に変更(親42→親7へ集約)
UPDATE children SET parent_id = 7 WHERE parent_id = 42;

-- 参照がなくなったら親42を削除
DELETE FROM parents WHERE id = 42;

・大量更新時はインデックス必須、ロック順序の設計にも注意

方針D:トランザクション内で順序を組み立てる(DEFERRABLE活用)

-- FKを遅延可能に(コミット時に整合性を判定)
ALTER TABLE children
  DROP CONSTRAINT children_parent_id_fkey,
  ADD  CONSTRAINT children_parent_id_fkey
    FOREIGN KEY (parent_id) REFERENCES parents(id)
    DEFERRABLE INITIALLY DEFERRED;

BEGIN;
SET CONSTRAINTS ALL DEFERRED;  -- 以後、このTx内はコミット時に検査
UPDATE children SET parent_id = 7 WHERE parent_id = 42;
DELETE FROM parents WHERE id = 42;  -- コミット時点で整合していればOK
COMMIT;

・DEFERRABLEでないFKには効果がない
・順序問題(先に削除→後で再割当)のようなケースに有効

UPDATE時の主キー変更:ON UPDATE CASCADEが必要

-- 親の主キー(id)変更に子を追随
ALTER TABLE children
  DROP CONSTRAINT children_parent_id_fkey,
  ADD  CONSTRAINT children_parent_id_fkey
    FOREIGN KEY (parent_id) REFERENCES parents(id)
    ON UPDATE CASCADE;

-- 親の主キーを変更
UPDATE parents SET id = 100 WHERE id = 42;  -- 子のparent_idも100に更新

・主キー更新はコストが高いので慎重に。可能なら不変な代理キーを採用

RESTRICT / NO ACTION の違いと注意

-- どちらも参照があれば削除失敗
-- RESTRICT: その場で即時拒否
-- NO ACTION: 文の終わり(またはトリガ後)に検査(PostgreSQLの既定)

・多段カスケードやトリガ併用時に挙動差が出る場合がある

安全な削除/再割当の手順テンプレート(運用用)

BEGIN;

-- 1) 実影響件数を把握
SELECT COUNT(*) FROM children WHERE parent_id = 42;

-- 2) 必要なら一時テーブルに退避
CREATE TEMP TABLE backup_children AS
SELECT * FROM children WHERE parent_id = 42;

-- 3) 方針を適用(例:再割当)
UPDATE children SET parent_id = 7 WHERE parent_id = 42;

-- 4) 親の削除
DELETE FROM parents WHERE id = 42;

COMMIT;

・ROLLBACK即応ができるように必ずトランザクションで囲む

FK性能/ロック健全性:インデックスと実行順序

-- 参照側FK列にインデックスは必須級
CREATE INDEX IF NOT EXISTS idx_children_parent_id ON children(parent_id);

-- 複合FKなら複合インデックス
CREATE INDEX IF NOT EXISTS idx_child_fk ON child(a_id, b_id);

・インデックスなしだと全件探索→排他ロック延伸→タイムアウト/デッドロックの温床

見落としやすいパターン(チェックリスト)

□ 子テーブルがさらに孫テーブルに参照される(多段連鎖の確認)
□ ビュー/トリガ/ルールによる間接更新が実体を触っている
□ パーティションテーブル間で参照移動(バージョン差異の仕様に注意)
□ NOT VALID なFKが残っており、VALIDATE時に大量違反が露呈
□ 複合FKの一部だけ更新して整合が崩れる

診断SQL:どのテーブルが参照しているかを一気に可視化

-- 親テーブル 'public.parents' を参照する外部キー一覧
SELECT
  con.conname,
  con.confdeltype, con.confupdtype,
  rel_t.relname AS child_table,
  array_agg(att2.attname ORDER BY att2.attnum) AS child_columns,
  rel_p.relname AS parent_table,
  array_agg(att.attname ORDER BY att.attnum)  AS parent_columns
FROM pg_constraint con
JOIN pg_class rel_p ON rel_p.oid = con.confrelid
JOIN pg_class rel_t ON rel_t.oid = con.conrelid
JOIN pg_attribute att  ON att.attrelid  = con.confrelid AND att.attnum = ANY (con.confkey)
JOIN pg_attribute att2 ON att2.attrelid = con.conrelid  AND att2.attnum = ANY (con.conkey)
WHERE con.contype='f' AND rel_p.relname='parents' AND rel_p.relnamespace = 'public'::regnamespace
GROUP BY con.conname, con.confdeltype, con.confupdtype, rel_t.relname, rel_p.relname
ORDER BY child_table, con.conname;

最小再現と解消(NG→OK)

-- 準備
DROP TABLE IF EXISTS children, parents;
CREATE TABLE parents(id int PRIMARY KEY, name text);
CREATE TABLE children(
  id serial PRIMARY KEY,
  parent_id int NOT NULL REFERENCES parents(id)  -- 既定は ON DELETE NO ACTION
);

INSERT INTO parents VALUES (1,'p1'), (2,'p2');
INSERT INTO children(parent_id) VALUES (1), (1), (2);

-- NG:参照が残っている親の削除
DELETE FROM parents WHERE id=1;
-- ERROR: update or delete on table "parents" violates foreign key constraint "children_parent_id_fkey" on table "children"

-- 解消パターン1:CASCADEに付け替えて削除
ALTER TABLE children
  DROP CONSTRAINT children_parent_id_fkey,
  ADD  CONSTRAINT children_parent_id_fkey
    FOREIGN KEY (parent_id) REFERENCES parents(id) ON DELETE CASCADE;
DELETE FROM parents WHERE id=1;  -- OK(子が連鎖削除)

-- 解消パターン2:再割当(親2に寄せる)
-- (CASCADEにせず)
UPDATE children SET parent_id = 2 WHERE parent_id = 1;
DELETE FROM parents WHERE id=1;  -- OK

テストとロールバック戦略(本番前の必須手順)

□ 影響件数の見積り(SELECT COUNT(*))
□ トランザクション+一時退避(backup_children等)
□ EXPLAIN/EXPLAIN ANALYZEでコスト把握
□ 子→孫までの連鎖とインデックスの有無
□ タイムアウト/ロック待ち設定(lock_timeout, statement_timeout)の確認

まとめ:選択肢の使い分け

・親を消す → 子も消してよい:ON DELETE CASCADE
・親を消す → 子を残したい:ON DELETE SET NULL/SET DEFAULT(列定義に注意)
・親は消さない → 子を移す:UPDATEで再割当(必要ならDEFERRABLE)
・親キーを更新する:ON UPDATE CASCADE、または不変キーを採用
・性能/安定性:子のFK列にインデックス、順序/境界はトランザクションで管理