- データベース
PostgreSQL duplicate keyの直し方|シーケンスのずれ
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期連続の黒字決算。