2026年6月22日
2026年6月22日
PostgreSQLのシーケンスとAUTO INCREMENT(IDENTITY列・SERIALの違いとsetvalでのずれ修正)
PostgreSQL には MySQL の AUTO_INCREMENT にあたる属性はなく、連番は「シーケンス」という専用のオブジェクトが払い出します。新しく作るテーブルでは、SQL 標準の GENERATED ... AS IDENTITY(IDENTITY 列)を使い、型は bigint にしておくのがおすすめです。この記事では、IDENTITY 列と従来の SERIAL の違い、挿入した ID の受け取り方、データ移行後によく起きる「duplicate key」エラーの直し方、連番に欠番が出る理由までを説明します。
MySQL から PostgreSQL へテーブルを移す人や、両方を使い分けている人を主な読者として想定しています。なお、WordPress のコアは MySQL・MariaDB を対象にしており、PostgreSQL は公式にはサポートされていません。
MySQL の AUTO_INCREMENT との対応
MySQL では列に AUTO_INCREMENT を付けると、テーブルが次の番号を管理します。PostgreSQL では、テーブルとは別にシーケンスがあり、列の既定値として nextval('シーケンス名') を呼ぶ形で連番を振ります。書き方は次の3通りです。
-- 1. IDENTITY 列(PostgreSQL 10 以降・SQL 標準)
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE
);
-- 2. SERIAL / BIGSERIAL(従来からある省略記法)
CREATE TABLE logs (
id bigserial PRIMARY KEY,
message text
);
-- 3. シーケンスを自分で作って既定値に指定する
CREATE SEQUENCE order_no_seq START WITH 1000;
CREATE TABLE orders (
order_no bigint PRIMARY KEY DEFAULT nextval('order_no_seq'),
total numeric NOT NULL
);
SERIAL は型ではなく、「シーケンスを作り、列の既定値に nextval を設定し、列が削除されたらシーケンスも削除されるよう紐付ける」という処理をまとめた省略記法です。
IDENTITY 列をすすめる理由
新しいテーブルで IDENTITY 列をすすめる理由は次のとおりです。
- SQL 標準の書き方で、ほかのデータベースとの移行がしやすい
- シーケンスがテーブルの一部として扱われるため、テーブルのコピーや権限の付け方で、シーケンスを別に考える場面が減る。
SERIALでは、INSERT するロールにシーケンスのUSAGE権限を付け忘れて、「permission denied for sequence」で失敗する、というつまずきがよくあります - GENERATED ALWAYS なら、誤って ID を手で指定した INSERT を拒否できる
GENERATED ALWAYS と GENERATED BY DEFAULT の違いは、ID を明示的に指定した INSERT を受け付けるかどうかです。
-- GENERATED ALWAYS の列に値を指定するとエラーになる
INSERT INTO users (id, email) VALUES (5, 'a@example.com');
-- 移行などで意図的に指定するときは OVERRIDING SYSTEM VALUE を付ける
INSERT INTO users (id, email) OVERRIDING SYSTEM VALUE VALUES (5, 'a@example.com');
既存データを取り込む予定がある、アプリケーションが ID を指定して INSERT する、といった事情があるなら BY DEFAULT を選びます。そうでなければ ALWAYS のほうが事故を防げます。どちらの場合も、ID を指定して入れた行はシーケンスを進めないため、取り込み後には後述の setval による調整が要ります。
型は bigint にしておく
SERIAL と integer の上限は約21億(2,147,483,647)です。ログや履歴のように行が増え続けるテーブルでは、この上限に達すると INSERT ができなくなり、後から bigint に変えるにはテーブル全体の書き換えが必要になります。最初から bigint(BIGSERIAL)にしておけば、この心配はほぼなくなります。
挿入した ID を受け取る
MySQL の LAST_INSERT_ID() に近い使い方をしたい場合、PostgreSQL では RETURNING 句を使うのが確実で簡単です。
INSERT INTO users (email) VALUES ('b@example.com') RETURNING id;
currval('シーケンス名') は、同じセッションで直前に nextval が返した値を返します。ほかのセッションの INSERT の影響は受けませんが、そのセッションでまだ nextval を呼んでいないとエラーになります。トリガーなどで複数のシーケンスが動く場合は、どの値が返るか分かりにくくなるため、RETURNING を使うほうが安全です。
PHP の PDO でも、RETURNING id を付けたクエリを実行して結果を読み取る方法が分かりやすく、ドライバーごとの lastInsertId() の挙動の違いを気にせずに済みます。
duplicate key エラーの原因はシーケンスのずれ
データ移行やバックアップからの部分的な復元のあとで、新しい行を INSERT すると次のエラーが出ることがあります。
ERROR: duplicate key value violates unique constraint "users_pkey"
DETAIL: Key (id)=(1) already exists.
原因は、ID を明示して行を取り込んだため、テーブルには大きな ID の行があるのに、シーケンスは古い値のまま進んでいないことです。シーケンスは列の値を見ているわけではないため、自動では追いつきません。
まず、現在の最大値とシーケンスの位置を比べます。
SELECT max(id) FROM users;
-- 列に紐付いたシーケンス名を調べる(SERIAL でも IDENTITY でも使える)
SELECT pg_get_serial_sequence('users', 'id');
SELECT last_value, is_called FROM public.users_id_seq; -- 上で調べた名前を指定
シーケンスが最大値より小さければ、setval で最大値の次から払い出すように合わせます。
SELECT setval(
pg_get_serial_sequence('users', 'id'),
COALESCE((SELECT max(id) FROM users), 0) + 1,
false
);
第3引数を false にすると、「次の nextval で第2引数の値そのものを返す」という意味になります。行が0件のテーブルでも1から始まるよう、COALESCE で0を補っています。IDENTITY 列なら ALTER TABLE users ALTER COLUMN id RESTART WITH 値; でも同じことができます。
テーブルが多い場合は、移行の手順の最後に、すべての連番列について同じ処理を流すようにしておくと、取りこぼしを防げます。pg_dump で丸ごと書き出して復元した場合は、シーケンスの値も setval として出力に含まれるため、通常この問題は起きません。
本番で実行する前に、念のためバックアップを取り、ステージング環境で INSERT が通るかを確認してください。
連番に欠番が出る理由
シーケンスは、値の払い出しをトランザクションの外で行います。そのため、次の場面で番号が飛びます。
- INSERT がエラーになったり、トランザクションを
ROLLBACKしたりしても、払い出した値は戻らない - シーケンスに
CACHEを設定していると、セッションが先にまとめて確保した番号が、使われないまま捨てられることがある - サーバーのクラッシュや、スタンバイへの切り替えのあとに、番号が先に進むことがある
これは複数のセッションが同時に待たずに番号を取れるようにするための仕様で、不具合ではありません。欠番があってはならない請求書番号のような用途に、シーケンスの値をそのまま使うのは避けてください。そうした番号は、採番用のテーブルを行ロックで更新するなど、別の仕組みで管理します。
よく使う操作
-- 次の値を取得する(呼ぶたびに1つ進み、元には戻せない)
SELECT nextval('order_no_seq');
-- 開始値や増分を変える
ALTER SEQUENCE order_no_seq RESTART WITH 5000;
ALTER SEQUENCE order_no_seq INCREMENT BY 1;
-- テーブルを空にして、連番も最初に戻す
TRUNCATE TABLE logs RESTART IDENTITY;
-- SERIAL のテーブルに INSERT するロールへ、シーケンスの権限を付ける
GRANT USAGE ON SEQUENCE logs_id_seq TO app_user;
TRUNCATE ... RESTART IDENTITY は全行を削除する操作です。実行するデータベースとテーブル名をよく確認し、必要ならバックアップを取ってから使ってください。
ID を連番にしない選択肢
ID を URL に出すと、件数や登録順が推測されることがあります。外部に見せる識別子には UUID を使う方法もあります。PostgreSQL 13 以降は gen_random_uuid() が標準で使え、新しいバージョンでは時刻順に並ぶ UUID(UUIDv7)を作る関数も加わっています(2026年10月現在)。使えるかどうかはバージョンによります。ただし UUID は bigint より大きく、ランダムな値はインデックスの効率を下げることもあるため、内部の主キーは bigint の連番、外部公開用には別に UUID の列、という分け方もよく使われます。
まとめ
- PostgreSQL の連番はシーケンスが払い出す。新しいテーブルは
bigint GENERATED ... AS IDENTITYが基本 ALWAYSは手入力の ID を拒否し、BY DEFAULTは受け付ける。移行の予定で選ぶ- 挿入した ID は
RETURNINGで受け取るのが確実 - 移行後の duplicate key エラーは、
pg_get_serial_sequenceとsetvalでシーケンスを最大値に合わせて直す - シーケンスには欠番が出る。欠番が許されない番号には別の仕組みを使う