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(一意性制約違反) を示すエラーコード。

今回のケース:主キーの重複

エラーメッセージの "duplicate key value violates unique constraint \"onidazo_pkey\"" から、以下のことが分かる。

どの列がユニーク制約(重複禁止)の対象かを確認する

制約定義を確認

エラーには ... violates unique constraint "onidazo_pkey" とあり、どの列が重複しているのかは制約定義を見ないと分からない。

そこで、該当の制約がどの列に設定されているかを確認した。

select
  conname,
  pg_get_constraintdef(oid) as def
from pg_constraint
where conname = 'onidazo_pkey';

_pkeyPostgreSQLで主キー制約名を省略した場合に自動で付く接尾辞(例:onidazo_pkey

結果が以下のように返ってきた。

connamedef
onidazo_pkeyPRIMARY 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"
  }
]

この場合、典型的には以下のどちらかである。

主キーが自動採番されているか確認するために、以下のクエリを実行した。

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_nameis_identityidentity_generationcolumn_default
onidazo_idNOnullnextval('onidazo_id_seq'::regclass')

column_defaultnextval('onidazo_id_seq'::regclass) とあることから、主キーは onidazo_id_seqnextval() によって採番されることが分かる。

is_identity = NO だが自動採番されている?

補足:nextval() とは?

補足:SERIAL型とIDENTITY列の違い

PostgreSQLで自動採番を実現する方法には主に2つある。

SERIAL型(従来の方式)

CREATE TABLE onidazo (
  onidazo_id SERIAL PRIMARY KEY,
  -- 他の列...
);

IDENTITY列(SQL標準、推奨)

CREATE TABLE onidazo (
  onidazo_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  -- または GENERATED BY DEFAULT AS IDENTITY
  -- 他の列...
);

どちらの方式でも自動採番は機能するが、今回のようなシーケンスのズレはどちらでも発生する可能性がある。

原因:シーケンスがテーブルの最大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;

今回は以下のように、番号がバラバラの結果だった。

この状態だと、次の INSERT でシーケンスが 986, 987, ... と進み、既存行の onidazo_id = 1005 に到達したタイミングで 主キー重複(23505エラー) が発生する。

補足:なぜこのようなズレが発生するのか?

シーケンスとテーブルの最大IDがズレる典型的な原因:

解決方法:setval でシーケンスを最大IDに合わせる

シーケンスをテーブルの最大IDに合わせることで解決できる。

select setval(
  'public.onidazo_id_seq'::regclass,
  (select max(onidazo_id) from public.onidazo),
  true
);

※ 第3引数の true は「この値を使用済みとする」という意味。これにより、次の nextval() は 最大ID + 1(例:1006)から始まり、重複が発生しなくなる。

まとめ