PostgreSQLで「主キーが重複エラー(23505)」が出たときの確認と対処(Supabase)
Supabaseでデータベース登録時に発生したエラー
Supabase(PostgREST)経由でテーブルにデータを登録しようとしたところ、エラーが発生して登録ができなかった。
DevToolsで確認(network→エラー選択→response)したところ、次のエラーが発生していた。
{
"code": "23505",
"details": null,
"hint": null,
"message": "duplicate key value violates unique constraint \"onidazo_pkey\""
}※仮に「onidazo」というテーブル名とする。
エラーコード 23505 とは?
23505 は PostgreSQL の unique_violation(一意性制約違反) を示すエラーコード。
- 主キー(PRIMARY KEY)制約だけでなく、UNIQUE制約全般で発生する
- 「既存のデータと重複する値を挿入・更新しようとした」ときに発生
- 参考:PostgreSQL エラーコード | 日本PostgreSQLユーザ会
今回のケース:主キーの重複
エラーメッセージの "duplicate key value violates unique constraint \"onidazo_pkey\"" から、以下のことが分かる。
onidazo_pkeyという制約に違反している(_pkeyは主キー制約の接尾辞)- つまり、既に存在する主キー値で新規追加(INSERT)しようとしているため、失敗している
どの列がユニーク制約(重複禁止)の対象かを確認する
制約定義を確認
エラーには ... violates unique constraint "onidazo_pkey" とあり、どの列が重複しているのかは制約定義を見ないと分からない。
そこで、該当の制約がどの列に設定されているかを確認した。
select
conname,
pg_get_constraintdef(oid) as def
from pg_constraint
where conname = 'onidazo_pkey';※ _pkey は PostgreSQLで主キー制約名を省略した場合に自動で付く接尾辞(例:onidazo_pkey)
結果が以下のように返ってきた。
| conname | def |
|---|---|
| onidazo_pkey | PRIMARY KEY (onidazo_id) |
つまり、重複しているのはonidazo_id(主キー列)である。
その主キー値で既存行があるか確認
select *
from public.onidazo
where onidazo_id = '<いま登録しようとしている値>';結果:
Success. No rows returnedこの結果から、現時点では該当する主キー値のデータは存在しないことが確認できた。しかし、それでも23505エラーが発生している。
payload に主キーが無いのに、なぜ重複するのか?
payload(リクエストボディ)に主キーを含めずに INSERT した場合でも、23505エラーが発生することがある。
例えば、今回実際に送っていた payload は以下のようなもので、主キー(id)が含まれていなかった。
[
{
"code": "999998",
"name": "テスト",
"updated_by": 0001,
"updated_machine": "machine1"
}
]この場合、典型的には以下のどちらかである。
- 主キーが DB側で自動採番されている(serial / identity)
INSERT前トリガーなどで主キーを埋めている
主キーが自動採番されているか確認するために、以下のクエリを実行した。
select
column_name, -- 列名
is_identity, -- IDENTITY列かどうか
identity_generation, -- IDENTITY生成方式
column_default -- デフォルト値(シーケンス採番の設定)
from information_schema.columns
where table_schema = 'public'
and table_name = 'onidazo'
and column_name = 'onidazo_id';結果:
| column_name | is_identity | identity_generation | column_default |
|---|---|---|---|
| onidazo_id | NO | null | nextval('onidazo_id_seq'::regclass') |
column_default に nextval('onidazo_id_seq'::regclass) とあることから、主キーは onidazo_id_seq の nextval() によって採番されることが分かる。
is_identity = NO だが自動採番されている?
is_identityはNOだが、column_defaultにnextval()があるため、実際には自動採番されている- これは 別のDBからの移行時によく見られるパターン(従来の
SERIAL型を使用している) - 新規にテーブルを作成する場合は、SQL標準の
IDENTITY列を使う方がベストプラクティス
補足:nextval() とは?
nextval()は PostgreSQL のシーケンス(連番生成器)から次の値を取得する関数onidazo_id_seqはシーケンスの名前::regclassはシーケンス名を内部IDに変換するキャスト(型変換)- つまり、この設定により
INSERT時に主キーが自動で連番採番される
補足:SERIAL型とIDENTITY列の違い
PostgreSQLで自動採番を実現する方法には主に2つある。
SERIAL型(従来の方式)
CREATE TABLE onidazo (
onidazo_id SERIAL PRIMARY KEY,
-- 他の列...
);- PostgreSQL独自の疑似型
- 内部的には
INTEGER+nextval('シーケンス名')の組み合わせ - 別のDBから移行した場合、この形式になっていることが多い
is_identity = NOと表示される
IDENTITY列(SQL標準、推奨)
CREATE TABLE onidazo (
onidazo_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
-- または GENERATED BY DEFAULT AS IDENTITY
-- 他の列...
);- SQL標準に準拠(PostgreSQL 10以降で利用可能)
is_identity = YESと表示されるGENERATED ALWAYS:値の手動指定を許可しない(より厳密)GENERATED BY DEFAULT:通常は自動採番、必要に応じて手動指定も可能- 新規テーブル作成時はこちらを推奨
どちらの方式でも自動採番は機能するが、今回のようなシーケンスのズレはどちらでも発生する可能性がある。
原因:シーケンスがテーブルの最大IDより古い(ズレ)
次に、テーブルの最大 onidazo_id と、シーケンスの現在値を確認した。
テーブルの最大 onidazo_id:
select max(onidazo_id) as max_id from public.onidazo;シーケンスの現在値:
select last_value, is_called
from public.onidazo_id_seq;今回は以下のように、番号がバラバラの結果だった。
max_id = 1005last_value = 985(is_called = true)
この状態だと、次の INSERT でシーケンスが 986, 987, ... と進み、既存行の onidazo_id = 1005 に到達したタイミングで 主キー重複(23505エラー) が発生する。
補足:なぜこのようなズレが発生するのか?
シーケンスとテーブルの最大IDがズレる典型的な原因:
- データの手動投入(IDを直接指定してINSERT)
- バックアップからの復元
- シーケンスの手動リセット
- テーブルのTRUNCATE後にシーケンスをリセットし忘れた場合
解決方法:setval でシーケンスを最大IDに合わせる
シーケンスをテーブルの最大IDに合わせることで解決できる。
select setval(
'public.onidazo_id_seq'::regclass,
(select max(onidazo_id) from public.onidazo),
true
);※ 第3引数の true は「この値を使用済みとする」という意味。これにより、次の nextval() は 最大ID + 1(例:1006)から始まり、重複が発生しなくなる。
まとめ
23505 duplicate keyは RLS(権限)ではなく DB 制約(主キー重複) で落ちている- payload に主キーが無い場合でも、DB側の シーケンス採番が原因 で重複することがある
max(onidazo_id)とonidazo_id_seqのlast_valueを比較するとズレが分かるsetvalでシーケンスを最大IDに合わせれば解決できる