はじめに
PostgreSQLで bigserial を主キーに設定したテーブルに対し、
- INSERT文で主キーを指定していない
- にもかかわらず
- 一意性制約違反(SQLSTATE 23505)が発生する
という事象に遭遇しました。
調査の結果、原因は
「過去に主キーを手動でINSERTしてしまったことによるシーケンスと実データの不整合」
でした。
この記事では、
- なぜ
bigserialでも重複が起きるのか - 手動INSERTがどのように影響するのか
- 実際に行った対処方法
を整理してまとめます。
前提条件
- DB:PostgreSQL
- 主キー:
bigserial - 通常のINSERT文では主キーを指定していない
テーブル例
CREATE TABLE sample_table (
id bigserial PRIMARY KEY,
file_name text,
status integer,
updated_by text,
updated_at timestamp
);
問題のINSERT文
アプリケーション側で実行していたINSERT文は以下です。
INSERT INTO sample_table (
interchange_time,
file_name,
status,
updated_by,
updated_at,
input_id
)
VALUES (
:interchange_time,
:file_name,
:status,
:updated_by,
:updated_at,
:input_id
)
RETURNING id;
- 主キー
idは 明示的に指定していない - 採番されたIDを取得するために
RETURNING idを使用
このSQL自体に問題はありません。
それでも以下のエラーが発生しました。
ERROR: duplicate key value violates unique constraint
SQLSTATE: 23505
原因は「過去の手動INSERT」
起きていたこと
過去に、次のような 主キーを明示指定したINSERT が行われていました。
INSERT INTO sample_table (id, file_name)
VALUES (100, 'sample.csv');
この操作自体はPostgreSQLでは可能ですが、ここに落とし穴があります。
なぜそれが問題になるのか?
bigserialの正体
bigserial は以下の組み合わせです。
-
bigint型のカラム - 専用の シーケンス
-
nextval(シーケンス名)が DEFAULT に設定されている
🔴 重要なポイント
PostgreSQLのシーケンスは
「テーブルの実データ」を自動では見てくれない
具体例
-
過去に手動でこうINSERT
INSERT INTO sample_table (id, file_name) VALUES (100, 'manual.csv'); -
その時点でのシーケンス値は仮に
50 -
次に通常のINSERTを実行
- シーケンスから
51が採番される
- シーケンスから
-
すでに同じIDが存在 → 23505エラー
つまり、
手動INSERTによって
テーブルの最大IDとシーケンスの値がズレたことが原因
でした。
対策:シーケンスを実データに合わせる
① テーブルの最大IDを確認
SELECT MAX(id) FROM sample_table;
例:結果が 100
② シーケンスをリセット
SELECT setval('sample_table_id_seq', 100, true);
- 第2引数:現在の最大ID
- 第3引数:
true- 次のINSERTで
101が採番される
- 次のINSERTで
※ シーケンス名は以下で確認できます。
\d sample_table
対応後の結果
- シーケンス値と実データが整合
- INSERT時の一意性制約違反は発生しなくなった
- 問題は完全に解消
再発防止のための注意点
✅ 原則
-
bigserialのカラムは INSERT文で指定しない
✅ 例外的に指定する場合
- 手動でIDを指定してINSERTしたら
- 必ずシーケンスを進める
例:
SELECT setval(
'sample_table_id_seq',
(SELECT MAX(id) FROM sample_table),
true
);
まとめ
-
bigserialでも 過去の手動INSERTで簡単に壊れる - INSERT文が正しくても、**原因は「昔のデータ操作」**ということがある
- 23505エラーが出たら シーケンスと最大IDのズレを疑う