自增类型(SERIAL / IDENTITY)
本节讲解 PostgreSQL 中实现自增列的两套方案:历史遗留的 SERIAL 系列,以及 SQL 标准推荐的 IDENTITY 系列。你将理解它们背后的序列(Sequence)机制,并能熟练操作序列、处理空洞与重置问题。
前置知识
阅读本节前,建议先了解:整数类型(SMALLINT / INTEGER / BIGINT)
基础概念
PostgreSQL 没有 MySQL 那种"列属性级别"的 AUTO_INCREMENT。实现自增的本质是:
序列(Sequence) + 默认值(Default)+ 可选的所有权绑定(Ownership)
序列(Sequence)是一个独立的对象,内部维护一个 BIGINT 计数器,通过 nextval() 递增、通过 currval() 读取当前值。它不属于任何表,只是一个带原子递增能力的"号码机"。
| 写法 | 是否为数据类型 | 底层列类型 | 标准 | 推荐度 |
|---|---|---|---|---|
SMALLSERIAL | 否,语法糖 | SMALLINT | 否 | 不推荐 |
SERIAL | 否,语法糖 | INTEGER | 否 | 不推荐 |
BIGSERIAL | 否,语法糖 | BIGINT | 否 | 兼容旧项目 |
GENERATED BY DEFAULT AS IDENTITY | 是,列约束 | 由你指定 | SQL 标准 | 需要手工导入时用 |
GENERATED ALWAYS AS IDENTITY | 是,列约束 | 由你指定 | SQL 标准 | 新项目首选 |
术语补充:
- 身份列(Identity Column):
IDENTITY定义的列,序列的所有权强绑定在列上。 - 序列空洞(Gap):自增值不连续。回滚、冲突、缓存预分配都会产生空洞,这是设计特性而非缺陷。
- 序列缓存(Cache):序列一次预取多个值到内存以减少 I/O,进程崩溃会丢失未使用的预取值。
语法与用法
两套方案的最小写法对比:
-- 方案一:SERIAL 系列(历史写法)
CREATE TABLE t_serial_way (
id BIGSERIAL PRIMARY KEY
);
-- 方案二:IDENTITY 系列(SQL 标准,推荐)
CREATE TABLE t_identity_way (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
);IDENTITY 的完整语法:
列名 类型 GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY
[ ( sequence_options ) ]
-- sequence_options 可包含:
-- START WITH 起始值
-- INCREMENT BY 步长
-- MINVALUE / MAXVALUE
-- CACHE 缓存个数
-- CYCLE / NO CYCLE-- 最简可运行示例
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO users (email) VALUES ('alice@example.com'), ('bob@example.com')
RETURNING id, email; id | email
----+--------------------
1 | alice@example.com
2 | bob@example.com
(2 rows)详细说明
SERIAL 的真实面目
SERIAL 在解析阶段就被展开成具体的 DDL。可以用 pg_get_serial_sequence() 与系统目录验证:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
-- 找到 SERIAL 列背后的序列名
SELECT pg_get_serial_sequence('products', 'id') AS sequence_name; sequence_name
---------------------------
public.products_id_seq
(1 row)-- 查看驱动该列的默认值表达式
SELECT
a.attname AS column_name,
format_type(a.atttypid, a.atttypmod) AS data_type,
pg_get_expr(d.adbin, d.adrelid) AS default_expr,
a.attnotnull AS not_null
FROM pg_attribute a
JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
WHERE a.attrelid = 'products'::regclass
AND a.attnum > 0; column_name | data_type | default_expr
-------------+-----------+--------------------------------------
id | integer | nextval('products_id_seq'::regclass)
(1 row)-- 查看序列自身的元数据
SELECT
sequencename AS seq_name,
data_type,
start_value,
increment_by,
min_value,
max_value,
cache_size,
cycle
FROM pg_sequences
WHERE sequencename = 'products_id_seq'; seq_name | data_type | start_value | increment_by | min_value | max_value | cache_size | cycle
------------------+-----------+-------------+--------------+-----------+---------------------+------------+-------
products_id_seq | integer | 1 | 1 | 1 | 2147483647 | 1 | f
(1 row)从输出可以清楚看到:SERIAL = integer 列 + nextval() 默认值 + 一个 integer 序列。这就是它最大的缺陷来源——列类型被硬编码为 INTEGER,上限 21 亿。
SERIAL 与 IDENTITY 的差异对比
-- 两者的行为差异演示
CREATE TABLE t_serial (
id SERIAL PRIMARY KEY,
v TEXT
);
CREATE TABLE t_identity (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
v TEXT
);差异一:手工插入 id 的许可
-- SERIAL:可以随意手工插入 id
INSERT INTO t_serial (id, v) VALUES (100, 'manual-ok');
-- IDENTITY ALWAYS:默认被拒绝
INSERT INTO t_identity (id, v) VALUES (100, 'manual');ERROR: cannot insert a non-DEFAULT value into column "id"
DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS.
HINT: Use OVERRIDING SYSTEM VALUE to override.-- 需要显式声明 OVERRIDING SYSTEM VALUE 才能覆盖
INSERT INTO t_identity (id, v) OVERRIDING SYSTEM VALUE VALUES (100, 'manual-ok')
RETURNING id, v; id | v
-----+-----------
100 | manual-ok
(1 row)差异二:序列所有权与清理
-- SERIAL 的序列是弱绑定:删除列后序列可能残留
ALTER TABLE t_serial DROP COLUMN id;ALTER TABLE-- 序列仍然存在,成为"孤儿序列"
SELECT sequencename FROM pg_sequences WHERE sequencename = 't_serial_id_seq'; sequencename
--------------------
t_serial_id_seq
(1 row)-- IDENTITY 的序列随列一起删除,不会残留
ALTER TABLE t_identity DROP COLUMN id;
SELECT count(*) AS leftover FROM pg_sequences WHERE sequencename LIKE 't_identity%'; leftover
----------
0
(1 row)差异三:权限
SERIAL 需要单独给序列授 USAGE 权限;IDENTITY 的序列权限随列权限自动管理:
-- SERIAL 场景:必须额外授权,否则应用账号插入报权限错误
-- GRANT USAGE ON SEQUENCE public.products_id_seq TO app_user;
-- IDENTITY 场景:授予表权限即可
-- GRANT INSERT ON public.orders TO app_user;完整对比表:
| 对比项 | SERIAL | GENERATED ... AS IDENTITY |
|---|---|---|
| 是否 SQL 标准 | 否,PostgreSQL 私有 | 是 |
| 列类型 | 固定 integer / bigint / smallint | 自选 |
| 序列所有权 | 弱绑定,可残留孤儿序列 | 强绑定,随列删除 |
| 序列权限 | 需单独 GRANT USAGE | 随列权限 |
| 手工插入 | 允许 | ALWAYS 需 OVERRIDING,BY DEFAULT 允许 |
能否 ALTER 序列参数 | 通过 ALTER SEQUENCE | 通过 ALTER COLUMN ... SET 或 ALTER SEQUENCE |
INSERT 时序列推进 | 是 | 是 |
pg_get_serial_sequence() | 支持 | 支持 |
| 推荐度 | 兼容旧项目 | 新项目首选 |
IDENTITY 的两种模式:ALWAYS 与 BY DEFAULT
CREATE TABLE orders (
-- ALWAYS:最严格,防止应用代码意外写入 id
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no TEXT NOT NULL UNIQUE,
amount NUMERIC(12, 2) NOT NULL
);
CREATE TABLE legacy_orders (
-- BY DEFAULT:与 SERIAL 行为一致,允许手工指定
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_no TEXT NOT NULL UNIQUE,
amount NUMERIC(12, 2) NOT NULL
);| 模式 | 手工插入 id | 适用场景 |
|---|---|---|
GENERATED ALWAYS AS IDENTITY | 拒绝,除非加 OVERRIDING SYSTEM VALUE | 生产表,防止误写 |
GENERATED BY DEFAULT AS IDENTITY | 允许 | 数据迁移、需要保留历史 id |
BY DEFAULT 模式下手工插入会推进序列吗?答案是不会,这正是需要警惕的地方:
INSERT INTO legacy_orders (id, order_no, amount) VALUES (500, 'LEG-500', 100.00);
-- 序列仍停在 1,下一条自动插入还是 1
INSERT INTO legacy_orders (order_no, amount) VALUES ('LEG-AUTO', 200.00)
RETURNING id, order_no; id | order_no
----+----------
1 | LEG-AUTO
(1 row)此时如果 order_no 不是唯一键,就可能出现 id 冲突。修复办法见后文"重置序列"。
序列操作函数
三个核心函数构成序列操作的基础:
| 函数 | 作用 | 是否需要先调用 nextval() |
|---|---|---|
nextval(seq) | 推进并返回下一个值 | — |
currval(seq) | 返回当前会话最近一次取到的值 | 是 |
setval(seq, n [, is_called]) | 直接设置当前值 | 否 |
lastval() | 返回当前会话最近一次 nextval 的值(不指定序列名) | 是 |
CREATE SEQUENCE demo_seq START WITH 100 INCREMENT BY 10;
-- nextval 每调用一次推进一次
SELECT nextval('demo_seq') AS v1, nextval('demo_seq') AS v2; v1 | v2
-----+-----
100 | 110
(1 row)注意在上面的语句中,同一行内的两个 nextval 从左到右求值,所以得到 100 和 110。这不是可靠的行为——不要在一条语句里依赖多个 nextval 的相对顺序,PostgreSQL 不保证求值顺序。
-- currval 返回本会话最近一次取到的值(不推进)
SELECT currval('demo_seq') AS current; current
---------
110
(1 row)-- 新会话中未调用过 nextval 就调用 currval,会报错
-- 换一个连接执行:SELECT currval('demo_seq');ERROR: currval of sequence "demo_seq" is not yet defined in this session-- setval 直接定位,第三个参数决定"当前位置是否已消费"
SELECT setval('demo_seq', 200) AS after_set; -- 下次 nextval 返回 210
SELECT setval('demo_seq', 300, false) AS after_set_false; -- 下次 nextval 返回 300
SELECT nextval('demo_seq') AS next_value; after_set | after_set_false | next_value
-----------+-----------------+------------
200 | 300 | 300
(1 row)setval 的第三个参数 is_called 语义必须记牢:
| 调用 | 语义 | 下次 nextval 返回 |
|---|---|---|
setval('s', 300, true) | 300 已被使用 | 310(步长 10 时) |
setval('s', 300, false) | 300 尚未被使用 | 300 |
setval('s', 300) | 等价于 true | 310 |
序列缓存与空洞
序列缓存是性能优化,也是最容易引发"数值跳跃"的原因:
-- 无缓存:每次 nextval 都可能写 WAL 与刷盘
CREATE SEQUENCE seq_nocache CACHE 1;
-- 有缓存:一次预取 100 个值到后端进程内存
CREATE SEQUENCE seq_cached CACHE 100;查看缓存设置:
SELECT sequencename, cache_size FROM pg_sequences WHERE sequencename LIKE 'seq_%'; sequencename | cache_size
----------------+------------
seq_nocache | 1
seq_cached | 100
(1 row)在单个会话里,缓存序列看起来是连续的:
SELECT nextval('seq_cached') AS a, nextval('seq_cached') AS b, nextval('seq_cached') AS c; a | b | c
----+----+----
1 | 2 | 3
(1 row)但另一个会话拿到的会是 101、102……而不是 4:
-- 另开一个连接执行
SELECT nextval('seq_cached') AS other_session_value; other_session_value
---------------------
101
(1 row)如果第一个会话在只用掉 3 个值时就退出(例如客户端断开、进程崩溃、事务回滚),剩余 97 个值就永久丢失,形成空洞。
空洞的四大来源:
| 来源 | 说明 | 是否可避免 |
|---|---|---|
| 事务回滚 | INSERT 后 ROLLBACK,序列不回收 | 不可,属设计特性 |
| 唯一键冲突 | INSERT ... ON CONFLICT 失败的行也消耗序列值 | 不可,属设计特性 |
| 序列缓存丢失 | 进程崩溃导致预取值丢失 | 可,用 CACHE 1 换性能 |
| 批量预分配 | SELECT nextval() FROM generate_series(1, n) 提前占号 | 可,按需调用 |
关键结论:自增 id 不允许假设连续。 如果业务需要"流水号连续",必须用独立的计数器表 + 行锁实现,而不能依赖序列。
重置序列
场景一:手工插入过大 id 后,序列落后,后续插入会主键冲突。
CREATE TABLE invoices (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
invoice_no TEXT NOT NULL
);
-- 迁移历史数据时手工指定了较大的 id
INSERT INTO invoices (id, invoice_no) VALUES (1000, 'INV-1000'), (1001, 'INV-1001');
-- 此时自动插入会从 1 开始,虽然暂时不冲突,但需要修正
SELECT max(id) AS current_max FROM invoices; current_max
-------------
1001
(1 row)-- 重置序列到最大 id 之后
SELECT setval(
pg_get_serial_sequence('invoices', 'id'), -- 拿到序列名
(SELECT max(id) FROM invoices), -- 当前最大值
true -- 该值已被占用
) AS new_seq_value; new_seq_value
---------------
1001
(1 row)-- 下一条自动插入从 1002 开始
INSERT INTO invoices (invoice_no) VALUES ('INV-AUTO') RETURNING id, invoice_no; id | invoice_no
------+------------
1002 | INV-AUTO
(1 row)更简洁的写法(PostgreSQL 支持在 ALTER TABLE 里直接重置身份列):
-- PostgreSQL 10+
ALTER TABLE invoices ALTER COLUMN id RESTART WITH 2000;
INSERT INTO invoices (invoice_no) VALUES ('INV-RESTART') RETURNING id; id | invoice_no
------+-------------
2000 | INV-RESTART
(1 row)场景二:批量导入后统一校正所有序列(迁移项目时的排查脚本):
-- 生成一段"校正所有 identity 列序列"的 SQL(供人工复核后执行)
SELECT format(
'SELECT setval(pg_get_serial_sequence(%L, %L), COALESCE(max(%I), 1), max(%I) IS NOT NULL) FROM %I.%I;',
c.table_schema || '.' || c.table_name,
c.column_name,
c.column_name,
c.column_name,
c.table_schema,
c.table_name
) AS fix_sql
FROM information_schema.columns c
WHERE c.is_identity = 'YES'
AND c.table_schema = 'public'; fix_sql
-------------------------------------------------------------------------------------------
SELECT setval(pg_get_serial_sequence('public.invoices', 'id'), COALESCE(max("id"), 1), max("id") IS NOT NULL) FROM public.invoices;
(1 row)序列参数调整
生产环境常见需求:换更大容量的序列、调大缓存、或改成负向递减。
-- 把 integer 序列升级为 bigint(SERIAL 遗留项目升级时用)
ALTER SEQUENCE products_id_seq AS BIGINT;
SELECT sequencename, data_type, max_value
FROM pg_sequences WHERE sequencename = 'products_id_seq'; sequencename | data_type | max_value
--------------------+-----------+---------------------
products_id_seq | bigint | 9223372036854775807
(1 row)-- 调整缓存与步长(业务分片时常用:不同实例用不同步长错开)
ALTER SEQUENCE seq_cached CACHE 500 INCREMENT BY 1;
-- 递减序列:从 0 开始往下走
CREATE SEQUENCE seq_down START WITH 0 INCREMENT BY -1 MINVALUE -1000;
SELECT nextval('seq_down') AS v1, nextval('seq_down') AS v2; v1 | v2
----+-----
0 | -1
(1 row)注意 ALTER SEQUENCE ... AS BIGINT 只改序列类型,不会改列的类型。SERIAL 列仍是 integer,需要单独改列:
-- 遗留 SERIAL 项目扩容的完整步骤(大表上会重写,务必评估锁)
-- ALTER SEQUENCE products_id_seq AS BIGINT;
-- ALTER TABLE products ALTER COLUMN id TYPE BIGINT;序列的并发安全性
序列的递增是非事务性且原子的:多个会话并发调用 nextval() 不会拿到重复值,也不需要加锁。
CREATE TABLE seq_concurrent (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tag TEXT);
-- 用 4 个并发会话各插入 1000 行(实际执行时用 shell 并发发起)
-- psql -c "INSERT INTO seq_concurrent (tag) SELECT 'w1' FROM generate_series(1, 1000);" &
-- psql -c "INSERT INTO seq_concurrent (tag) SELECT 'w2' FROM generate_series(1, 1000);" &
-- 校验:无重复
SELECT count(*) AS total, count(DISTINCT id) AS distinct_ids FROM seq_concurrent; total | distinct_ids
-------+--------------
2000 | 2000
(1 row)因此序列不会成为写入瓶颈——这一点和 MySQL 的自增锁机制完全不同,PostgreSQL 的序列无需等待其他事务提交。
代价是:序列值不遵循事务可见性。会话 A 取到 5 并回滚,会话 B 仍会取到 6。
实战示例
场景:为一个多租户 SaaS 的订单表设计自增主键,演示严格模式、序列校正、以及用序列实现"租户内连续单号"。
第 1 步:建表(严格模式)
CREATE TABLE IF NOT EXISTS orders (
id BIGINT GENERATED ALWAYS AS IDENTITY (
START WITH 100000
INCREMENT BY 1
CACHE 50
NO CYCLE
) PRIMARY KEY,
tenant_id BIGINT NOT NULL,
order_no TEXT NOT NULL,
amount NUMERIC(12, 2) NOT NULL CHECK (amount >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_orders_tenant_no UNIQUE (tenant_id, order_no)
);CREATE TABLE第 2 步:查看自动创建的序列
SELECT
pg_get_serial_sequence('orders', 'id') AS seq_name;
SELECT sequencename, start_value, increment_by, cache_size, cycle
FROM pg_sequences WHERE sequencename = 'orders_id_seq'; seq_name
------------------------
public.orders_id_seq
(1 row)
sequencename | start_value | increment_by | cache_size | cycle
-----------------+-------------+--------------+------------+-------
orders_id_seq | 100000 | 1 | 50 | f
(1 row)第 3 步:正常插入
INSERT INTO orders (tenant_id, order_no, amount) VALUES
(1, 'SO-0001', 199.00),
(1, 'SO-0002', 299.50),
(2, 'SO-0001', 88.00)
RETURNING id, tenant_id, order_no; id | tenant_id | order_no
--------+-----------+----------
100001 | 1 | SO-0001
100002 | 1 | SO-0002
100003 | 2 | SO-0001
(3 rows)第 4 步:验证严格模式保护
-- 试图手工写入 id,被拒绝
INSERT INTO orders (id, tenant_id, order_no, amount) VALUES (1, 3, 'SO-0001', 10.00);ERROR: cannot insert a non-DEFAULT value into column "id"
DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS.
HINT: Use OVERRIDING SYSTEM VALUE to override.-- 历史数据迁移需要保留原 id 时,显式加 OVERRIDING SYSTEM VALUE
INSERT INTO orders (id, tenant_id, order_no, amount)
OVERRIDING SYSTEM VALUE
VALUES (500000, 9, 'SO-MIGRATE-1', 999.00)
RETURNING id, tenant_id; id | tenant_id
--------+-----------
500000 | 9
(1 row)第 5 步:迁移后校正序列(关键步骤)
-- 迁移后序列仍停在 100003,不改会造成主键冲突
SELECT currval(pg_get_serial_sequence('orders', 'id')) AS before_fix; before_fix
------------
100003
(1 row)-- 校正到当前最大值之后
SELECT setval(
pg_get_serial_sequence('orders', 'id'),
(SELECT max(id) FROM orders),
true
) AS after_fix; after_fix
-----------
500000
(1 row)-- 下一条自动插入不再冲突
INSERT INTO orders (tenant_id, order_no, amount) VALUES (9, 'SO-MIGRATE-2', 1.00)
RETURNING id; id
--------
500001
(1 row)第 6 步:用序列实现"租户内连续单号"
自增 id 不能保证连续,但业务常需要"每个租户从 0001 开始的单号"。用一张计数器表 + UPDATE ... RETURNING 实现(这在事务内可回滚,与序列语义相反):
CREATE TABLE tenant_counters (
tenant_id BIGINT PRIMARY KEY,
last_no INTEGER NOT NULL DEFAULT 0
);
INSERT INTO tenant_counters (tenant_id, last_no) VALUES (1, 0), (2, 0)
ON CONFLICT (tenant_id) DO NOTHING;
-- 取下一个单号:行锁保证同一租户串行,事务回滚会归还号码
UPDATE tenant_counters
SET last_no = last_no + 1
WHERE tenant_id = 1
RETURNING tenant_id, last_no; tenant_id | last_no
-----------+---------
1 | 1
(1 row)-- 验证连续性:同一租户连续取号
UPDATE tenant_counters SET last_no = last_no + 1 WHERE tenant_id = 1 RETURNING last_no;
UPDATE tenant_counters SET last_no = last_no + 1 WHERE tenant_id = 1 RETURNING last_no; last_no
---------
2
(1 row)
last_no
---------
3
(1 row)第 7 步:校验序列无重复
SELECT
count(*) AS total_rows,
count(DISTINCT id) AS distinct_ids,
min(id) AS min_id,
max(id) AS max_id
FROM orders; total_rows | distinct_ids | min_id | max_id
------------+--------------+--------+--------
5 | 5 | 100001 | 500001
(1 row)第 8 步:清理
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS tenant_counters;
DROP TABLE IF EXISTS seq_concurrent;
DROP SEQUENCE IF EXISTS demo_seq;
DROP SEQUENCE IF EXISTS seq_nocache;
DROP SEQUENCE IF EXISTS seq_cached;
DROP SEQUENCE IF EXISTS seq_down;注意事项
- 新项目不要用
SERIAL。它的列类型被硬编码为INTEGER,21 亿上限在日志、订单、事件表上会真正触顶。
-- 不推荐:列类型被固定为 integer
CREATE TABLE t_v1 (id SERIAL PRIMARY KEY);
-- 推荐:类型可控,序列强绑定
CREATE TABLE t_v2 (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY);- 自增 id 会有空洞,业务不能假设连续。回滚与冲突都会消耗序列值。
CREATE TABLE t_gap (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY);
BEGIN;
INSERT INTO t_gap DEFAULT VALUES; -- 消耗 id = 1
ROLLBACK; -- 回滚,但 id = 1 已永久消耗
INSERT INTO t_gap DEFAULT VALUES RETURNING id; -- 返回 2,不是 1 id
----
2
(1 row)BY DEFAULT模式下手工插入 id 不会推进序列,必须手动setval,否则会主键冲突。
-- 错误!手工插入 id=5000 后直接自动插入,序列仍从 1 开始
-- INSERT INTO legacy_orders (id, order_no) VALUES (5000, 'X');
-- 正确:手工插入后校正序列
SELECT setval(pg_get_serial_sequence('legacy_orders', 'id'), (SELECT max(id) FROM legacy_orders));currval()有会话限制,必须在同一连接里先调用过nextval()。
-- 错误!新连接直接调用会报错
-- SELECT currval('demo_seq');
-- 正确:先 nextval 再 currval,或用 last_value 直接读(有坑,见下条)
SELECT nextval('demo_seq'), currval('demo_seq');- 不要用
SELECT last_value FROM seq读"下一个值"。last_value是序列文件里的落盘值,CACHE > 1时它停在缓存块的边界,会误导。
-- 不推荐:cache 50 时 last_value 是预取的边界值,不是真实使用位置
SELECT last_value FROM orders_id_seq;
-- 推荐:从 pg_sequences 或 information_schema 看元数据
SELECT last_value FROM pg_sequences WHERE sequencename = 'orders_id_seq';DROP TABLE不会删除SERIAL的序列(若所有权绑定丢失),也不会因序列仍被引用而阻止删表。
-- 检查孤儿序列的实用查询
SELECT n.nspname AS schema, c.relname AS orphan_sequence
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'S'
AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = c.oid AND d.deptype = 'a');不要在同一事务里依赖序列值做业务判断。序列值在事务提交前就对其他会话可见(不存在"隔离"),从序列取号不等于占位。
OVERRIDING SYSTEM VALUE会绕过保护,只能用于数据迁移脚本,不能出现在日常业务代码里。
-- 仅在数据迁移时使用
-- INSERT INTO orders (id, ...) OVERRIDING SYSTEM VALUE VALUES (...);ALTER SEQUENCE ... AS BIGINT不改列类型。SERIAL列扩容必须同时改列,大表上是重写操作。
-- SERIAL 项目扩容的两步(第二步在大表上代价高)
-- ALTER SEQUENCE products_id_seq AS BIGINT;
-- ALTER TABLE products ALTER COLUMN id TYPE BIGINT;CACHE越大,空洞越大。高并发短事务场景用大CACHE(如 100~1000)能显著降低序列争用,但服务器崩溃时丢号更多。能接受空洞就用大CACHE。不要在生成列或复杂表达式中混用
nextval()。一条INSERT ... SELECT里会为每一行调用一次,容易误伤。
-- 危险:generate_series 里每行都会消耗一个序列值
-- INSERT INTO t (id) SELECT nextval('some_seq') FROM generate_series(1, 1000000);restart_identity慎用。TRUNCATE ... RESTART IDENTITY会重置序列,若表被外键引用或已有数据关联,后果严重。
-- 危险:级联清空并重置序列
-- TRUNCATE orders RESTART IDENTITY CASCADE;
-- 安全:只清数据,保留序列
TRUNCATE orders;最佳实践
- 统一使用
BIGINT GENERATED ALWAYS AS IDENTITY作为代理主键,把类型固定、权限清晰、序列强绑定三个好处一次拿到。 - 不要用
SERIAL/BIGSERIAL,除非是维护遗留项目且改动成本高于收益。 - 只把自增 id 当"内部代理键",对外暴露的编号(订单号、发票号)另设业务列并加唯一约束。
- 需要"连续编号"时用计数器表
UPDATE ... RETURNING,不要试图修复序列的空洞。 - 数据迁移后必须校正序列,把
setval()写进迁移脚本的最后一步并纳入回归测试。 - 高并发写表调大序列
CACHE(如CACHE 100),用可接受的大空洞换写入吞吐;反之审计类表可用CACHE 1缩小空洞。 ALTER TABLE ... RESTART WITH是重置身份列的首选语法(PostgreSQL 10+),比手工setval更直观且会自动识别序列。- 序列名保持一致命名约定(
表名_列名_seq),便于批量运维与排查孤立序列。 - 应用层不要缓存或复用自增值,每次插入都让数据库分配。
- 定期检查孤儿序列(
pg_class.relkind = 'S'且无pg_depend关联),迁移或重构后清理。 - 分区表注意:
IDENTITY定义在父表上时,父表序列被所有分区共享,id全局唯一;这是推荐做法,不要给每个分区单独建序列。
下一节