PostgreSQLの「insert or update violates foreign key constraint」エラーを修正する

intermediate🐘 PostgreSQL2026-03-26| PostgreSQL 12以降、Linux/macOS/Windows、任意のPostgreSQLクライアント(psql、pgAdmin、アプリケーションコード)

Error Message

ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"
#postgresql#foreign-key#constraint#referential-integrity#sql

TL;DR

挿入または更新しようとしている行の外部キー値が、参照先テーブルに存在しません。親レコードが存在しないか、間違ったIDを渡しているか、挿入の順序が誤っています。修正方法:まず親行が存在することを確認してから、再試行してください。

エラーの内容

ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"
DETAIL: Key (customer_id)=(999) is not present in table "customers".

DETAILの行をよく読んでください — デバッグの半分はそこで完結します。ここではcustomer_id = 999customersテーブルに対応する行を持たないと示されています。これがまさに問題の核心です。

根本原因

PostgreSQLは書き込み時に参照整合性を強制します。外部キー列に設定するすべての値は、親テーブルに既に存在していなければなりません。例外はなく、親行が存在しない場合は挿入が即座に拒否されます。

これは通常、以下のいずれかの原因で発生します:

  • 親レコードが作成されていない(注文が登録される前に顧客が登録されていない)
  • 誤ったIDが渡されている — オフバイワンエラー、古いキャッシュ値、またはアプリロジックのバグ
  • バッチ挿入の順序が誤っている — 子行が親行より先に挿入されている
  • マイグレーションまたはシードスクリプトが親テーブルをスキップした
  • ON DELETE CASCADEON DELETE SET NULLなしに親行が削除され、孤立した子行が残っている

ステップ1:欠落している親行を確認する

何かを変更する前に、参照しているIDが実際に存在するかを確認します:

-- 顧客999は存在するか?
SELECT * FROM customers WHERE id = 999;

行が返されない場合、それが問題です。親レコードが削除されているか、そもそも作成されていません。

制約がどのように定義されているかを正確に確認するには、直接調べます:

-- psqlでの簡単な方法
\d orders

-- またはシステムカタログを参照する
SELECT
  conname AS constraint_name,
  conrelid::regclass AS table_name,
  a.attname AS column_name,
  confrelid::regclass AS referenced_table,
  af.attname AS referenced_column
FROM pg_constraint c
JOIN pg_attribute a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
JOIN pg_attribute af ON af.attnum = ANY(c.confkey) AND af.attrelid = c.confrelid
WHERE c.contype = 'f'
  AND c.conrelid = 'orders'::regclass;

修正方法

修正1:先に親行を挿入する

ほとんどの場合、これが解決策です。欠落している親レコードを作成してから、子の挿入を再試行します:

-- まず顧客を作成する
INSERT INTO customers (id, name, email)
VALUES (999, 'Alice', 'alice@example.com');

-- これで注文の挿入が通る
INSERT INTO orders (id, customer_id, total)
VALUES (1, 999, 150.00);

修正2:正しい既存IDを使用する

親レコードが存在しているのに、誤ったIDを渡していることがあります。まず正しいIDを調べます:

-- AliceのIDを調べる
SELECT id FROM customers WHERE email = 'alice@example.com';
-- 結果: 42

-- 999ではなく42を使用する
INSERT INTO orders (customer_id, total)
VALUES (42, 150.00);

環境をまたいでIDをハードコードしないでください。本番環境で挿入された顧客のIDは、ステージング環境とほぼ確実に異なります。

修正3:トランザクションでラップする(バッチ挿入の場合)

関連する行をまとめて挿入する場合は、すべてをトランザクションでラップし、親を先に配置します:

BEGIN;

INSERT INTO customers (id, name) VALUES (100, 'Bob');
INSERT INTO orders (id, customer_id, total) VALUES (1, 100, 75.00);

COMMIT;

いずれかの挿入が失敗した場合、トランザクション全体がロールバックされます。孤立した行や中途半端な状態にはなりません。

修正4:制約を一時的に遅延させる(マイグレーション時のみ)

大規模なマイグレーションでは、挿入順序の管理が現実的でない場合があります。制約がDEFERRABLEとして作成されている場合、チェックをトランザクション終了時まで延期できます:

-- 制約がDEFERRABLEの場合のみ有効
BEGIN;
SET CONSTRAINTS orders_customer_id_fkey DEFERRED;

-- このトランザクション内では任意の順序で挿入可能
INSERT INTO orders (id, customer_id, total) VALUES (1, 100, 75.00);
INSERT INTO customers (id, name) VALUES (100, 'Bob');

COMMIT;  -- ここでFKチェックが実行される

制約がまだ遅延可能でない場合は、再作成します:

ALTER TABLE orders
  DROP CONSTRAINT orders_customer_id_fkey,
  ADD CONSTRAINT orders_customer_id_fkey
    FOREIGN KEY (customer_id) REFERENCES customers(id)
    DEFERRABLE INITIALLY IMMEDIATE;

修正5:制約を無効にする(最終手段、マイグレーション時のみ)

これは制御されたマイグレーション中のみ使用してください。本番アプリケーションのコードでは絶対に使用しないでください:

-- FK強制を無効化
ALTER TABLE orders DISABLE TRIGGER ALL;

-- ... バッチ挿入 ...

-- 再有効化
ALTER TABLE orders ENABLE TRIGGER ALL;

-- その後すぐに孤立行を確認する
SELECT o.id, o.customer_id
FROM orders o
LEFT JOIN customers c ON c.id = o.customer_id
WHERE c.id IS NULL;

**再有効化のたびに必ず孤立行チェックを実行してください。**これを省略すると、最悪のタイミングで表面化するサイレントなデータ整合性の問題が発生します。

確認

挿入を再試行し、データが正しく保存されたことを確認します:

-- 再試行
INSERT INTO orders (id, customer_id, total)
VALUES (1, 999, 150.00);

-- JOINで確認する
SELECT o.id, o.total, c.name AS customer_name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.id = 1;

クリーンなJOIN結果は、参照整合性が保たれていることを意味します。

予防策

このエラーを恒久的に防ぐためのいくつかの習慣:

  • シードスクリプトとマイグレーションでは依存関係の順序で挿入する — 親は常に子より先に。
  • 親行の削除が想定される場合はON DELETE CASCADEまたはON DELETE SET NULLを定義する。これにより子行が孤立せず自動的に処理されます。
  • 親行の挿入時にRETURNING idを使用する — 実際に生成されたIDをキャプチャし、子の挿入に直接渡します。
  • ORM(SQLAlchemy、Hibernate、ActiveRecord)では、IDを手動で管理するのではなく、モデルレベルでカスケードオプションを設定します。
-- RETURNINGで生成されたIDをキャプチャする
INSERT INTO customers (name, email)
VALUES ('Carol', 'carol@example.com')
RETURNING id;
-- 結果: id = 43

-- 直接使用する
INSERT INTO orders (customer_id, total)
VALUES (43, 200.00);

Related Error Notes