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 = 999がcustomersテーブルに対応する行を持たないと示されています。これがまさに問題の核心です。
根本原因
PostgreSQLは書き込み時に参照整合性を強制します。外部キー列に設定するすべての値は、親テーブルに既に存在していなければなりません。例外はなく、親行が存在しない場合は挿入が即座に拒否されます。
これは通常、以下のいずれかの原因で発生します:
- 親レコードが作成されていない(注文が登録される前に顧客が登録されていない)
- 誤ったIDが渡されている — オフバイワンエラー、古いキャッシュ値、またはアプリロジックのバグ
- バッチ挿入の順序が誤っている — 子行が親行より先に挿入されている
- マイグレーションまたはシードスクリプトが親テーブルをスキップした
ON DELETE CASCADEやON 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);

