ERROR: duplicate key value violates unique constraint "customer_pkey" は、すでにある値と同じ値を、重複が許されない列(主キー・UNIQUE)に入れようとしたときのエラーです。 制約の名前が _pkey(主キー)で、id を指定せずに INSERT しているのに出るなら、ほぼ確実にシーケンス(自動採番)が、テーブルの中の最大の id より後ろに戻っているのが原因です。

エラーの制約名 INSERT のしかた 原因 直し方
xxx_pkey id を指定していない シーケンスのずれ(移行・id 指定の INSERT のあと) setval でシーケンスを最大値に合わせる
xxx_pkey id を指定している 同じ id のデータが本当にある 入れるデータを見直す
xxx_email_key など — メールアドレスなどが本当に重複 ON CONFLICT で扱う、入力チェック

確認環境:PostgreSQL 16.15(確認日 2026年10月11日)。serial 列と IDENTITY 列(ALWAYS・BY DEFAULT)、\copy での取り込みで再現しました。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PostgreSQL duplicate keyの直し方|シーケンスのずれ」

再現:id を指定して入れたあと、通常の INSERT が失敗する

CREATE TABLE customer (id serial PRIMARY KEY, email text UNIQUE, name text);

INSERT INTO customer (name, email) VALUES ('佐藤', 'sato@example.com'), ('鈴木', 'suzuki@example.com');   -- id 1, 2
-- データ移行:id を指定して入れる
INSERT INTO customer (id, name, email) VALUES (3, '高橋', '...'), (4, '田中', '...'), (5, '伊藤', '...');

INSERT INTO customer (name, email) VALUES ('渡辺', 'watanabe@example.com');   -- 通常の INSERT
ERROR:  duplicate key value violates unique constraint "customer_pkey"
DETAIL:  Key (id)=(3) already exists.
確認したもの 値
テーブルの最大の id(SELECT max(id)) 5
シーケンスの値(last_value) 2(id を指定した INSERT では進まない)
次に採番される id 3 → すでにある → エラー

id を指定した INSERT では、シーケンスは進みません。 シーケンスは「次は 3」のままなので、3・4・5 とぶつかり続けます。

失敗しても番号は進む(何回かやると通ってしまう)

もう一度 INSERT すると、今度は Key (id)=(4) already exists. になりました。失敗した INSERT でも、シーケンスの番号は使われて進みます(PostgreSQL 公式ドキュメントでも、nextval で取った値は取り消されないとされています)。3 回失敗すると 6 に届いて通るため、「何度か実行したら直った」と見えますが、原因は残っています。

直し方:setval でシーケンスを最大値に合わせる

SELECT setval(pg_get_serial_sequence('customer', 'id'),
              coalesce(max(id), 1),
              max(id) IS NOT NULL)
FROM customer;
 setval
--------
      5

INSERT INTO customer (name, email) VALUES ('渡辺', 'watanabe@example.com') RETURNING id;
 id
----
  6                ← 最大値の次から採番された
書き方の部分 意味
pg_get_serial_sequence('customer', 'id') 列に紐づくシーケンスの名前を調べる(public.customer_id_seq)。名前を手で書かなくてよい
coalesce(max(id), 1) テーブルが空でもエラーにしない
max(id) IS NOT NULL 空なら false(次は 1)、データがあれば true(次は最大値+1)

確認環境でも、空のテーブルにこの SQL を実行すると、次の id は 1、データがあれば最大値の次になりました。PostgreSQL 公式ドキュメントのとおり、3つ目の引数が true なら「次は指定値+1」、false なら「次は指定値そのもの」です。

setval はトランザクションを ROLLBACK しても元に戻りません(公式ドキュメント)。本番で実行する前に、max(id) を確かめてから実行します。

データベースの全テーブルをまとめて直す

移行のあとは、ほかのテーブルもずれています。public スキーマの、シーケンスを持つ列をすべて直す例です。

DO $$
DECLARE r record; m bigint;
BEGIN
  FOR r IN
    SELECT c.relname AS t, a.attname AS col,
           pg_get_serial_sequence(quote_ident(c.relname), a.attname) AS seq
    FROM pg_class c JOIN pg_attribute a ON a.attrelid = c.oid
    WHERE c.relkind = 'r' AND c.relnamespace = 'public'::regnamespace AND a.attnum > 0
      AND pg_get_serial_sequence(quote_ident(c.relname), a.attname) IS NOT NULL
  LOOP
    EXECUTE format('SELECT max(%I) FROM %I', r.col, r.t) INTO m;
    PERFORM setval(r.seq, coalesce(m, 1), m IS NOT NULL);
    RAISE NOTICE '% . % → %', r.t, r.col, coalesce(m, 0);
  END LOOP;
END $$;
NOTICE:  customer . id → 6
NOTICE:  item . id → 2
NOTICE:  item2 . id → 2
NOTICE:  empty_t . id → 1
NOTICE:  c2 . id → 2

確認環境では、serial 列・IDENTITY 列・\copy で取り込んだテーブルのすべてが直り、次の INSERT が成功しました。ほかのスキーマもあるなら、'public'::regnamespace の部分を変えます。

ずれが起きる操作

操作 シーケンス
INSERT ... (id, ...) VALUES (3, ...)(id を指定) 進まない
\copy や COPY で id 付きの CSV を取り込む 進まない(確認環境でも、取り込み後の INSERT が Key (id)=(1) already exists.)
別のデータベースから、データだけを移す 移した先のシーケンスは進まない
pg_dump でダンプして戻す ダンプに SELECT pg_catalog.setval(...) が含まれていた(確認環境)ため、通常はずれない

データを移したら、最後に setval を実行する手順にしておくと防げます。

IDENTITY 列と serial の違い

PostgreSQL 10 以降は、serial の代わりに IDENTITY 列も使えます。

列の定義 id を指定した INSERT ずれ
serial 入る 起きる
GENERATED BY DEFAULT AS IDENTITY 入る 起きる
GENERATED ALWAYS AS IDENTITY エラーで止まる 起きにくい
INSERT INTO item (id, name) VALUES (1, 'a');      -- ALWAYS の列に id を指定
ERROR:  cannot insert a non-DEFAULT value into column "id"
DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
HINT:  Use OVERRIDING SYSTEM VALUE to override.

GENERATED ALWAYS なら、うっかり id を指定した INSERT を止められます。 ただし、ヒントにある OVERRIDING SYSTEM VALUE を付けて入れた場合は、同じようにずれました(確認環境でも、そのあとの INSERT が Key (id)=(1) already exists.)。直し方は serial と同じ setval です。

本当に重複しているとき

制約の名前が _pkey でなければ、シーケンスとは関係ありません。

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

DETAIL に、重複した列と値が表示されます。 同じメールアドレスが入ってくる可能性があるなら、ON CONFLICT で扱いを決めます。

INSERT INTO customer (name, email) VALUES ('別人', 'sato@example.com')
ON CONFLICT (email) DO NOTHING;              -- 重複なら何もしない(INSERT 0 0)
書き方 重複したとき
ON CONFLICT (email) DO NOTHING 入れない(エラーにしない)
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name 既存の行を更新する

アプリでは、このエラーの SQLSTATE は 23505 です。Java なら SQLException.getSQLState()、Python(psycopg)なら errors.UniqueViolation で判定できます。

確認の手順(チェックリスト)

順番 確認すること 方法
1 制約の名前 _pkey ならシーケンスのずれを疑う。それ以外は本当の重複
2 DETAIL の値 Key (id)=(3) のように、ぶつかった値
3 最大値とシーケンス SELECT max(id) FROM t;、SELECT last_value FROM t_id_seq;
4 直し方 setval(pg_get_serial_sequence(...), max(id))
5 移行の手順 データを入れたあとに、全テーブルの setval を実行
6 再発を防ぐ GENERATED ALWAYS AS IDENTITY で id の指定を止める

よくある質問

Q. シーケンスの番号が飛んでいます(1, 2, 5 など)。問題ですか。 A. 問題ありません。失敗した INSERT やロールバックでも番号は使われるため、PostgreSQL のシーケンスは「すき間のない連番」にはなりません(公式ドキュメント)。請求書番号のように連番が必要なら、別の採番の仕組みを使います。

Q. TRUNCATE したあと、id を 1 から始めたいです。 A. TRUNCATE TABLE customer RESTART IDENTITY; で、テーブルを空にすると同時にシーケンスも最初に戻せます。データを残したまま戻すと、今回のエラーの原因になります。

まとめ

  • _pkey の重複で、id を指定していないなら、シーケンスのずれ
  • id を指定した INSERT・COPY では、シーケンスは進まない
  • 直し方は setval(pg_get_serial_sequence('t', 'id'), max(id))。移行後は全テーブルをまとめて直す
  • 失敗した INSERT でも番号は進む。何回かやると通るのは、直ったのではない
  • GENERATED ALWAYS AS IDENTITY なら、id を指定した INSERT を止められる
  • _pkey 以外の制約なら本当の重複。ON CONFLICT で扱いを決める

参考・出典

確認日はいずれも 2026年10月11日です。

  • PostgreSQL 16 Documentation「Sequence Manipulation Functions」:https://www.postgresql.org/docs/16/functions-sequence.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PostgreSQL duplicate keyの直し方|シーケンスのずれ」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?