Day4 後半のゴール
「“連番まわりでハマらない”ための実践イメージを持つ」
前半で、SERIAL / BIGSERIAL の正体が「整数+シーケンス+デフォルト値のセット」だと分かりました。
後半では、もう一歩踏み込んで「実務でどう使うか」「どこでハマりやすいか」「MySQLから移行するとき何に気をつけるか」を具体例で固めていきます。
ここでのゴールはこうです。SERIAL カラムに対して、INSERT・DELETE・リセットがどう効くかイメージできる。
シーケンスの「値が飛ぶ」「戻さない」という性質を、怖がらずに理解できる。
MySQLの AUTO_INCREMENT からPostgreSQLに移すときの、設計とマインドの差分を説明できる。
SERIALカラムに対するINSERTと“値が飛ぶ”話
「削除しても番号は戻さない、それでいい」
まずは、一番よくある「idが飛ぶ問題」から整理します。
次のようなテーブルを考えます。
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
SQLここに、データを3件入れます。
INSERT INTO users (name) VALUES ('Taro'); -- id = 1
INSERT INTO users (name) VALUES ('Jiro'); -- id = 2
INSERT INTO users (name) VALUES ('Hanako');-- id = 3
SQLここまでは素直な連番です。
次に、id=2 を削除します。
DELETE FROM users WHERE id = 2;
SQLこの状態で、もう1件INSERTします。
INSERT INTO users (name) VALUES ('Saburo'); -- id はいくつ?
SQLこのとき、id は 4 になります。
「2が空いているから2を再利用しよう」とはなりません。
これはMySQLの AUTO_INCREMENT と同じで、
「連番は“ユニークな識別子”であって、“隙間なく連続した番号”ではない」という考え方です。
ここで大事なのは、「番号が飛ぶのは正常」「戻そうとしない」というマインドです。
id は「人間が数えて気持ちよくなるための番号」ではなく、「機械が一意に識別するためのキー」です。
シーケンスを直接触るときのイメージ
「nextval / currval / setval の役割」
普段は SERIAL に任せておけばOKですが、
シーケンスを直接触りたくなる場面もあります。
代表的な関数は3つです。
nextval('seq'):次の値を取り出し、シーケンスを進めるcurrval('seq'):そのセッションで最後に nextval した値を返すsetval('seq', n):シーケンスの現在値を n に設定する
例えば、users_id_seq というシーケンスがあるとします。
SELECT nextval('users_id_seq'); -- 1
SELECT nextval('users_id_seq'); -- 2
SELECT currval('users_id_seq'); -- 2
SQLcurrval は、「この接続の中で最後に nextval した値」を返すので、
「さっきINSERTした行のidを知りたい」ときに使えます。
ただし、実務では RETURNING を使う方がスマートです。
INSERT INTO users (name) VALUES ('Taro') RETURNING id;
SQLこれで、「INSERTした行のid」を一発で取れます。
シーケンスを意識せずに済むので、アプリコードからは RETURNING を使うのが定番です。
シーケンスとトランザクションの関係
「ロールバックしても番号は戻らない、を理解しておく」
PostgreSQL初心者がよく驚くポイントが、「トランザクションをロールバックしてもシーケンスは戻らない」という挙動です。
例えば、こういう流れを考えます。
BEGIN;
INSERT INTO users (name) VALUES ('Taro'); -- シーケンスは 1 を発行
-- 何かエラーが起きたとする
ROLLBACK;
SQLこのとき、INSERTはロールバックされるので、テーブルには行が残りません。
しかし、シーケンスは「1を発行した」という事実を覚えています。
次に、改めてINSERTすると、
INSERT INTO users (name) VALUES ('Jiro'); -- id = 2
SQLid は 1 ではなく 2 になります。
「ロールバックしたから1に戻るだろう」とはならないのがポイントです。
これは、「シーケンスはトランザクションとは独立して動く」という設計です。
理由はシンプルで、「複数のトランザクションから同時に nextval される世界」で、
ロールバックのたびに番号を戻していたら、整合性が崩れるからです。
ここでの大事な理解は、「idが飛ぶのは正常」「ロールバックしても番号は戻らない」「それを前提に設計する」ということです。
AUTO_INCREMENTからSERIALに移行するときの“考え方の差分”
「“カラムの属性”から“外部の番号発生器”へ」
MySQLからPostgreSQLに移るとき、よくある変換はこうです。
MySQL:
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL
);
SQLPostgreSQL:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
SQL見た目はほぼ同じですが、頭の中のモデルは少し変えた方がいいです。
MySQLでは、「idカラムが勝手に増える」という感覚でした。
PostgreSQLでは、「idカラムは“シーケンスから値をもらっているだけ”」という感覚です。
この違いを意識すると、次のような場面で迷いにくくなります。
テーブルをDROPしたとき、シーケンスはどうなるか。
テーブルをバックアップ・リストアしたとき、シーケンスの値はどう扱われるか。
SERIAL で作った場合は、テーブルとシーケンスの紐付けがされるので、
DROP TABLE時にシーケンスも一緒に消えます。
逆に、手動で作ったシーケンスは、自分で管理する必要があります。
「連番の本体はシーケンス」という意識を持っておくと、
こうした挙動も「まあそうだよね」と納得しやすくなります。
これからの“より標準的な書き方”への布石
「SERIALからIDENTITYへの橋渡しとして理解しておく」
少し先の話ですが、PostgreSQLには GENERATED ... AS IDENTITY という、
より標準SQL寄りの自動採番の書き方もあります。
例えば、こうです。
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL
);
SQL内部的にはやはりシーケンスを使いますが、
「SERIALはPostgreSQL方言」「IDENTITYは標準寄り」という位置づけです。
今は SERIAL を使っておけば十分ですが、
「どのみち中身はシーケンスなんだ」という理解を持っておくと、
将来IDENTITYに移行するときもスムーズです。
Day4では、「PostgreSQLの連番=シーケンス」という軸さえしっかり持てればOKです。
Day4 後半のまとめ
SERIAL カラムは、INSERTのたびに裏で紐づいたシーケンスから nextval で番号をもらっているだけなので、行をDELETEしても番号は詰め直されず、トランザクションをロールバックしても「発行済みの番号」は戻らず、idが飛ぶのは正常な挙動として受け入れる必要がある。
シーケンスは nextval(次の値を発行)、currval(そのセッションで最後に発行した値)、setval(現在値の調整)といった関数で直接触れる独立オブジェクトであり、実務では INSERT ... RETURNING id を使うことで、シーケンスを意識せずに「今INSERTした行のid」を安全に取得できる。
MySQLの AUTO_INCREMENT が「カラムに埋め込まれたカウンタ」という感覚なのに対し、PostgreSQLは「外部の番号発生器=シーケンスをカラムが利用している」というモデルで設計されており、この違いを理解しておくことで、削除・ロールバック・バックアップ/リストア時の挙動や、将来的に GENERATED ... AS IDENTITY のような標準SQL寄りの自動採番に進むときにも迷わずに済む。
