PostgreSQLの「duplicate key value violates unique constraint」エラーを修正する

intermediate🐘 PostgreSQL2026-03-29| PostgreSQL 12+、Linux/macOS/Windows、任意のクライアント(psql、pgAdmin、アプリコード)

Error Message

ERROR: duplicate key value violates unique constraint
#postgresql#unique#constraint#primary-key

何が起きているか

行を挿入または更新しようとすると、PostgreSQLが以下のエラーをスローします:

ERROR: duplicate key value violates unique constraint "users_pkey"
DETAIL: Key (id)=(42) already exists.

まずDETAIL行を確認してください — どの列(id)のどの値(42)が競合を引き起こしたかを正確に教えてくれます。これだけでデバッグの90%は完了です。残りの疑問は、なぜその値がすでに存在しているかということです。

よくある原因

  • SERIALまたはBIGSERIALのシーケンスが実際のテーブルデータと同期していない — 一括インポートやpg_restore後によく発生します
  • テーブルにすでに存在するIDを手動で指定している
  • ユニーク列(email、username、slug)に重複した値が渡された
  • 2つの同時リクエストが同じ行を挿入しようとして競合した

原因1:シーケンスのズレ(最も多い)

こんな状況を想像してください:50,000人のユーザーが入ったデータベースにpg_restoreを実行したとします。それらの行は明示的なIDで挿入されたため、PostgreSQLのシーケンスはインクリメントされていません。次のIDは1だと思っているのに、テーブルにはすでに50000までの行が存在しています。新しい挿入は毎回即座に失敗します。

シーケンスの値と実際の最大IDを確認する

-- シーケンスが次に生成する値は?
SELECT last_value FROM users_id_seq;

-- テーブルに現在存在する最大のIDは?
SELECT MAX(id) FROM users;

last_valueMAX(id)より小さければ、それが問題の原因です。

修正:シーケンスをリセットする

-- max(id)にリセットして、次の挿入がmax(id) + 1を取得するようにする
SELECT setval('users_id_seq', (SELECT MAX(id) FROM users));

シーケンス名をハードコードしたくない場合は、pg_get_serial_sequenceを使います:

SELECT setval(
  pg_get_serial_sequence('users', 'id'),
  (SELECT MAX(id) FROM users)
);

次にidを省略してINSERTすると、MAX(id) + 1が付与されます — クリーンで安全です。

ズレているシーケンスをすべて一括修正する

データベース全体をリストアした後は、複数のテーブルが同時に影響を受けている可能性があります。このスクリプトを一度実行するだけで、すべての主キーシーケンスを一括でリセットできます:

DO $$
DECLARE
  r RECORD;
BEGIN
  FOR r IN
    SELECT
      tc.table_name,
      kc.column_name,
      pg_get_serial_sequence(tc.table_name, kc.column_name) AS seq
    FROM information_schema.table_constraints tc
    JOIN information_schema.key_column_usage kc
      ON tc.constraint_name = kc.constraint_name
    WHERE tc.constraint_type = 'PRIMARY KEY'
      AND pg_get_serial_sequence(tc.table_name, kc.column_name) IS NOT NULL
  LOOP
    EXECUTE format(
      'SELECT setval(%L, COALESCE(MAX(%I), 1)) FROM %I',
      r.seq, r.column_name, r.table_name
    );
  END LOOP;
END;
$$;

原因2:ユニーク列の重複値

制約は主キーだけに限りません。こちらはemail列に対するエラーです:

ERROR: duplicate key value violates unique constraint "users_email_key"
DETAIL: Key (email)=(user@example.com) already exists.

競合している行を見つける

SELECT * FROM users WHERE email = 'user@example.com';

オプションA:重複をサイレントにスキップする

INSERT INTO users (email, name)
VALUES ('user@example.com', 'Alice')
ON CONFLICT (email) DO NOTHING;

オプションB:アップサート — 既存の行を更新する

INSERT INTO users (email, name, updated_at)
VALUES ('user@example.com', 'Alice Updated', NOW())
ON CONFLICT (email)
DO UPDATE SET
  name = EXCLUDED.name,
  updated_at = EXCLUDED.updated_at;

EXCLUDEDは、挿入に失敗した行を保持する仮想テーブルです。書き込もうとした新しい値を参照するために使用します。

原因3:一括挿入時の明示的なIDの競合

別のシステムからデータを移行する場合、元のデータが独自のIDを持っており、それらのIDがターゲットテーブルにすでに存在していると、競合するすべての行が失敗します。

挿入前に競合を確認する

-- すでに存在する受け入れ側のIDを確認する
SELECT s.id
FROM staging_users s
INNER JOIN users u ON s.id = u.id;

競合しない行のみ挿入する

INSERT INTO users (id, email, name)
SELECT id, email, name FROM staging_users
ON CONFLICT (id) DO NOTHING;

原因4:同時挿入による競合状態

2つのAPIリクエストが数ミリ秒以内に到着します。両方がデータベースを確認し、既存の行がないことを確認してから、どちらも挿入を実行します。一方が成功し、もう一方が制約違反になります。

アプリケーションレベルのチェックではこれを防げません — チェックと挿入の間のわずかな時間に2つ目のリクエストが割り込める余地があります。データベースレベルで対処してください:

-- Python (psycopg2) — エラーをキャッチして「すでに存在する」として扱う
try:
    cursor.execute(
        "INSERT INTO users (email) VALUES (%s)",
        (email,)
    )
    conn.commit()
except psycopg2.errors.UniqueViolation:
    conn.rollback()
    cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

さらに良い方法は、競合処理をSQL自体に押し込んで、挿入を最初から冪等にすることです:

INSERT INTO users (email)
VALUES ('user@example.com')
ON CONFLICT (email) DO NOTHING
RETURNING id;

修正が機能したか確認する

-- シーケンスが現在の最大値より先に進んでいることを確認する
SELECT
  last_value AS sequence_next,
  (SELECT MAX(id) FROM users) AS table_max
FROM users_id_seq;

-- 通常の挿入をテストする — IDを指定しなくても成功するはず
INSERT INTO users (email, name) VALUES ('test@example.com', 'Test');
SELECT * FROM users WHERE email = 'test@example.com';

覚えておくべきこと

  • pg_restoreや明示的なIDを持つ一括インポートの後は — アプリケーションを稼働させる前にシーケンスをリセットしてください。リストアの手順書に追加して、忘れないようにしましょう。
  • **ON CONFLICTはアプリケーションレベルの重複チェックより常に優れています。**データベース内でアトミックに実行されるため、アプリのコードでは保証できません。
  • **DETAIL行が最速のデバッグツールです。**制約名と競合した値が記載されています — スタックトレースより先にここから確認しましょう。
  • SERIALの代わりにGENERATED ALWAYS AS IDENTITYへの切り替えを検討してください(PostgreSQL 10以降)。PostgreSQLはIDENTITY列をより厳密に管理するため、シーケンスのズレが意図せず発生しにくくなります。

Related Error Notes