NEXTVAL / CURRVAL / LASTVAL / SETVAL
序列(Sequence)是 PostgreSQL 里最容易被当成「自增列附属品」的对象,但它的真实身份是独立于表存在、独立于事务存在的号码机。围绕它的一整套函数——nextval / currval / lastval / setval——各自有一套会话内状态与「是否回滚」的规则,混用极易出错。本节把这四个函数逐个拆开,再回到 CREATE SEQUENCE 的参数、pg_sequences 元数据与最高频的运维场景(回填后校正序列)上。
与「自增类型」的分工
自增类型(SERIAL / IDENTITY) 一章从列定义出发,讲 SERIAL 与 IDENTITY 的语法、所有权与迁移;本节聚焦 序列对象与这四个函数——nextval 的非事务性、currval 的会话限制、lastval 的跨序列陷阱、setval 的 is_called 语义,以及 CREATE SEQUENCE 全集参数、pg_sequences 视图、权限与并发的工程细节。SERIAL / IDENTITY 与序列的关系见 SERIAL 与序列管理。
前置知识
阅读本节前,建议先了解:JSONPath(PostgreSQL 12+)、自增类型(SERIAL / IDENTITY)
基础概念
先明确一件事:序列不是表的属性,它是一个独立的数据库对象,有自己的名字、自己的权限、自己的生命周期。表只是通过 DEFAULT nextval('seq') 借用它而已。
sequence "orders_id_seq" 表 "orders"
┌─────────────────────────┐ ┌──────────────────────────┐
│ last_value = 1003 │◀─ DEFAULT ─│ id bigint DEFAULT │
│ increment_by = 1 │ │ nextval('..._seq') │
│ is_called = true │ │ │
│ (独立对象、独立所有权) │ │ (只是引用,非拥有) │
└─────────────────────────┘ └──────────────────────────┘四个函数的分工:
| 函数 | 返回 | 作用域 | 是否推进序列 | 典型用途 |
|---|---|---|---|---|
nextval('seq') | bigint | 指定序列 | 是 | 取新号码 |
currval('seq') | bigint | 指定序列,限本会话 | 否 | 拿刚取到的号(如写关联子表) |
lastval() | bigint | 本会话最后用过的任意序列 | 否 | 极少数场景;跨序列时危险 |
setval('seq', n [, is_called]) | bigint | 指定序列 | 直接定位 | 校正、回填后修复 |
关键术语:
| 术语 | 含义 |
|---|---|
| 序列(Sequence) | 独立的数据库对象,内部维护一个原子递增的 64 位计数器 |
| 非事务性(Non-transactional) | nextval 的效果不受 ROLLBACK 影响,回滚不回退序列值 |
is_called | setval 的第三参;决定下一次 nextval 返回 n 还是 n+1 |
| 序列空洞(Gap) | 因回滚、冲突、缓存丢失导致编号不连续;是设计特性而非缺陷 |
| 序列缓存(Cache) | 一次预取多个值到后端进程内存,减少 I/O,进程崩溃会丢弃未用值 |
所有权(OWNED BY) | 把序列绑定到某列,删列/删表时序列随之删除 |
regclass | 序列/表/视图等对象 OID 的类型,nextval('seq'::regclass) 用它解析名字 |
最重要的一条特性先摆在这里:nextval 的推进不在事务里。ROLLBACK 之后号码不会归还。
DROP TABLE IF EXISTS t_gap_demo;
CREATE TABLE t_gap_demo (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tag TEXT);
BEGIN;
INSERT INTO t_gap_demo (tag) VALUES ('rolled-back'); -- 消耗 id = 1
ROLLBACK; -- 回滚,但 1 已永久消耗
INSERT INTO t_gap_demo (tag) VALUES ('committed') RETURNING id, tag; id | tag
----+-----------
2 | committed
(1 row)拿到的是 2 而不是 1。id 不连续是常态,业务代码不允许假设连续。
语法与用法
-- 取下一个值(推进序列)
nextval(regclass) → bigint
-- 取本会话最近一次 nextval 得到的值(不推进,未调用过则报错)
currval(regclass) → bigint
-- 取本会话最近一次 nextval 的值(不指定序列名,跨序列)
lastval() → bigint
-- 直接定位序列
setval(regclass, bigint) → bigint -- is_called = true
setval(regclass, bigint, boolean) → bigint
-- 序列本身
CREATE SEQUENCE name
[ AS { smallint | integer | bigint } ]
[ INCREMENT [ BY ] increment ]
[ MINVALUE minvalue | NO MINVALUE ]
[ MAXVALUE maxvalue | NO MAXVALUE ]
[ START [ WITH ] start ]
[ CACHE cache ]
[ [ NO ] CYCLE ]
[ OWNED BY { table.column | NONE } ]
-- 修改
ALTER SEQUENCE name [ AS type ] [ INCREMENT BY n ] [ RESTART [ WITH ] n ] ...
-- 名字解析:字符串会被隐式转成 regclass
nextval('orders_id_seq') -- 依赖 search_path
nextval('demo.orders_id_seq') -- 限定 schema
nextval('orders_id_seq'::regclass) -- 显式转型最小示例:
DROP SEQUENCE IF EXISTS demo_seq;
CREATE SEQUENCE demo_seq START WITH 100 INCREMENT BY 10;
SELECT nextval('demo_seq') AS v1,
nextval('demo_seq') AS v2,
currval('demo_seq') AS cur,
lastval() AS last; v1 | v2 | cur | last
-----+-----+-----+------
100 | 110 | 110 | 110
(1 row)详细说明
nextval:非事务性是核心特性
nextval 是原子且非事务的:并发调用不会拿到重复值,也不需要等锁;代价是值一旦取出就与事务脱钩。
DROP SEQUENCE IF EXISTS s_tx;
CREATE SEQUENCE s_tx START WITH 1;
BEGIN;
SELECT nextval('s_tx') AS inside_tx; -- 消耗 1
ROLLBACK;
SELECT nextval('s_tx') AS after_rollback; -- 得到 2,不是 1 inside_tx
-----------
1
(1 row)
after_rollback
----------------
2
(1 row)这个行为带来两个直接后果:
- 编号会有空洞,任何「流水号必须连续」的业务需求都不能用序列实现。
- 序列不会成为写入瓶颈,因为它不参与 MVCC,不需要等待其他事务提交。
空洞的来源与是否可避免:
| 来源 | 说明 | 可避免? |
|---|---|---|
| 事务回滚 | INSERT 后 ROLLBACK,序列不回收 | 不可,设计特性 |
| 唯一键冲突 | ON CONFLICT 失败的行也消耗了序列值 | 不可,设计特性 |
| 缓存丢失 | 后端进程崩溃/断开,未使用的缓存值丢弃 | 可,用 CACHE 1 换性能 |
| 批量预分配 | SELECT nextval(...) FROM generate_series(...) 提前占号 | 可,改成按需调用 |
currval:限本会话,且必须先 nextval
currval 返回当前会话最近一次 nextval 得到的值。它不推进序列,且有一个硬性前提:本会话必须已经调用过该序列的 nextval。
-- 新会话直接调用:报错
SELECT currval('demo_seq');ERROR: currval of sequence "demo_seq" is not yet defined in this session正确用法是先取号再读回:
SELECT nextval('demo_seq') AS taken, currval('demo_seq') AS got; taken | got
-------+-----
120 | 120
(1 row)currval 的会话隔离在设计上是必要的:它让「取号 → 插入主表 → 用同一个号写子表」这条链路无需往返应用层。
-- 典型用法:同一事务内,主表与明细表共用刚取到的号
DROP TABLE IF EXISTS order_lines;
DROP TABLE IF EXISTS order_headers;
CREATE TABLE order_headers (id BIGINT PRIMARY KEY, note TEXT);
CREATE TABLE order_lines (id BIGINT REFERENCES order_headers(id), sku TEXT);
BEGIN;
INSERT INTO order_headers (id, note) VALUES (nextval('demo_seq'), 'a');
INSERT INTO order_lines (id, sku) VALUES (currval('demo_seq'), 'SKU-A');
COMMIT;
SELECT * FROM order_lines; id | sku
-----+-------
130 | SKU-A
(1 row)lastval:跨序列的陷阱
lastval() 不带序列名,返回本会话最近一次 nextval 的值,无论那个序列是什么。这在多序列场景下极易出错:
CREATE SEQUENCE IF NOT EXISTS s_a START WITH 1000;
CREATE SEQUENCE IF NOT EXISTS s_b START WITH 5000;
SELECT nextval('s_a') AS from_a, lastval() AS last_after_a;
SELECT nextval('s_b') AS from_b, lastval() AS last_after_b;
SELECT currval('s_a') AS still_a, lastval() AS pointer_moved; from_a | last_after_a
--------+--------------
1000 | 1000
(1 row)
from_b | last_after_b
--------+--------------
5000 | 5000
(1 row)
still_a | pointer_moved
---------+---------------
1000 | 5000
(1 row)currval('s_a') 仍能正确返回 1000(它按序列名记录),而 lastval() 已经被 s_b 覆盖成 5000。结论:任何时候都优先用 currval('seq'),不要用 lastval()。 lastval() 只在「本会话只用一个序列」的极简场景下才安全。
-- 危险:中间夹了别的 nextval,lastval 就不再是"那个序列的值"
SELECT nextval('s_a'), nextval('s_b'), lastval(); -- 结果是 s_b 的值setval:is_called 是最易错的点
setval 直接定位序列,第三个参数 is_called 决定「定位到的值是否已被消费」:
setval('s', n, true)(默认):n视为已使用,下一次nextval返回n + increment。setval('s', n, false):n视为未使用,下一次nextval返回n。
DROP SEQUENCE IF EXISTS s_set;
CREATE SEQUENCE s_set START WITH 1 INCREMENT BY 1;
-- 第一个会话:设为已使用
SELECT setval('s_set', 300, true) AS set_called;
SELECT nextval('s_set') AS next_after_true; set_called
------------
300
(1 row)
next_after_true
-----------------
301
(1 row)-- 第二个会话(或重置后再来):设为未使用
SELECT setval('s_set', 500, false) AS set_uncalled;
SELECT nextval('s_set') AS next_after_false; set_uncalled
--------------
500
(1 row)
next_after_false
------------------
500
(1 row)对照表,必须背下来:
| 调用 | is_called | 下次 nextval 返回 | 适用场景 |
|---|---|---|---|
setval('s', 300) | true(默认) | 301 | 300 已存在于表中 |
setval('s', 300, true) | true | 301 | 同上 |
setval('s', 300, false) | false | 300 | 300 尚未使用,想让下一条用它 |
setval('s', 300, false) 后立刻 nextval | — | 300,且此后变 true | 空表初始化为 300 起 |
为什么这个参数重要:回填历史数据后,如果表里最大 id 是 1000,你希望下一条自增从 1001 开始,所以必须用 true。如果用 false,下一条会得到 1000,直接主键冲突。
-- 错!表里已有 id=1000,用 false 会让下一条又得到 1000
-- SELECT setval('s_set', 1000, false); -- 下一条 nextval = 1000,冲突
-- 对:1000 已被占用,下一条应该从 1001 开始
SELECT setval('s_set', 1000, true) AS correct_fix;
SELECT nextval('s_set') AS next_value; correct_fix
-------------
1000
(1 row)
next_value
------------
1001
(1 row)regclass 与名字解析
nextval / currval / setval 的形参类型是 regclass,字符串会被隐式转换。名字解析走 search_path,因此跨 schema 必须限定名:
CREATE SCHEMA IF NOT EXISTS demo;
DROP SEQUENCE IF EXISTS demo.orders_id_seq;
CREATE SEQUENCE demo.orders_id_seq START WITH 1;
-- 三种写法:前两种都依赖 search_path,第三种最明确
SELECT nextval('demo.orders_id_seq') AS qualified,
nextval('demo.orders_id_seq'::regclass) AS cast_regclass,
nextval(to_regclass('demo.orders_id_seq')) AS via_to_regclass; qualified | cast_regclass | via_to_regclass
-----------+---------------+-----------------
1 | 2 | 3
(1 row)用 regclass 的优势是绑定到对象 OID:序列若被重命名,已解析的 regclass 引用仍指向同一对象;而纯字符串每次都要重新走名字解析。
-- 相对名在 search_path 不含 demo 时会报错
SET search_path = public;
SELECT nextval('orders_id_seq');ERROR: relation "orders_id_seq" does not exist实践规则:函数、脚本、迁移里一律用 schema.seq_name 全限定名,不要依赖 search_path。
CREATE SEQUENCE 参数全集
DROP SEQUENCE IF EXISTS s_full;
CREATE SEQUENCE s_full
AS bigint -- 序列类型:smallint / integer / bigint(PostgreSQL 10+)
INCREMENT BY 2 -- 步长,可为负(递减)
MINVALUE 1 -- 最小值
MAXVALUE 1000000 -- 最大值
START WITH 100 -- 起始值
CACHE 50 -- 缓存 50 个值到后端内存
NO CYCLE -- 到达边界后不循环(默认)
OWNED BY NONE; -- 不属于任何列各参数含义与默认值:
| 参数 | 默认值 | 说明 |
|---|---|---|
AS type | bigint | 序列取值范围;PostgreSQL 10+ 可指定 smallint / integer / bigint |
INCREMENT | 1 | 步长;负数表示递减 |
MINVALUE | 升序为 1,降序为类型最小值 | 下界 |
MAXVALUE | 类型最大值 | 上界 |
START | MINVALUE(升序)/ MAXVALUE(降序) | 起始值 |
CACHE | 1 | 每会话预取值个数;越大越省 I/O,空洞越大 |
CYCLE | NO CYCLE | 到达边界后是否回绕 |
OWNED BY | NONE | 归属列;绑定后随列/表删除而删除 |
递减序列(从 0 往下走):
DROP SEQUENCE IF EXISTS s_down;
CREATE SEQUENCE s_down START WITH 0 INCREMENT BY -1 MINVALUE -1000;
SELECT nextval('s_down') AS v1, nextval('s_down') AS v2; v1 | v2
----+-----
0 | -1
(1 row)CYCLE 到达上界后回绕到 MINVALUE:
DROP SEQUENCE IF EXISTS s_cycle;
CREATE SEQUENCE s_cycle START WITH 1 MAXVALUE 3 CYCLE;
SELECT nextval('s_cycle') AS a, nextval('s_cycle') AS b,
nextval('s_cycle') AS c, nextval('s_cycle') AS d; a | b | c | d
---+---+---+---
1 | 2 | 3 | 1
(1 row)CYCLE 用在主键上等同于埋雷——回绕后必然主键冲突。只适合「环形缓冲区序号」这类特殊场景。
CACHE:性能与空洞的取舍
CACHE 是序列最主要的性能旋钮。每次 nextval 都要写 WAL 并保证崩溃后可恢复,这是 I/O 成本;CACHE > 1 让每个后端进程一次预取一批值到本地内存,之后在本进程内递增无需 I/O。
DROP SEQUENCE IF EXISTS s_nocache;
DROP SEQUENCE IF EXISTS s_cached;
CREATE SEQUENCE s_nocache CACHE 1;
CREATE SEQUENCE s_cached CACHE 100;-- 单个会话里看,缓存序列完全连续
SELECT nextval('s_cached') AS a, nextval('s_cached') AS b, nextval('s_cached') AS c; a | b | c
---+---+---
1 | 2 | 3
(1 row)-- 另一个会话拿到的从 101 开始,而不是 4
SELECT nextval('s_cached') AS other_session; other_session
---------------
101
(1 row)| 现象 | 原因 |
|---|---|
| 不同会话的值不连续 | 每个会话独立缓存,各占一段 |
| 会话退出后剩下的值消失 | 未使用的缓存值不回写(放弃即丢失) |
| 崩溃后大段跳号 | 同上,且跳的是一整块 CACHE |
取舍:高并发短事务场景用大 CACHE(100~1000)显著降低序列争用;审计、发票这类对连续性敏感的表用 CACHE 1。但无论 CACHE 多大,都不能满足「绝对连续」需求——那需要计数器表。
pg_sequences 视图与 information_schema
pg_sequences 是查看序列元数据最方便的视图(PostgreSQL 10+):
SELECT sequencename, schemaname, data_type, start_value, min_value, max_value,
increment_by, cycle, cache_size, last_value
FROM pg_sequences
WHERE schemaname = 'public' AND sequencename LIKE 's_%'
ORDER BY sequencename; sequencename | schemaname | data_type | start_value | min_value | max_value | increment_by | cycle | cache_size | last_value
--------------+------------+-----------+-------------+-----------+-----------+--------------+-------+------------+------------
s_cached | public | bigint | 1 | 1 | 9223372036854775807 | 1 | f | 100 | 103
s_cycle | public | bigint | 1 | 2 | 3 | 1 | t | 1 | 2
s_down | public | bigint | 0 | -1000 | 0 | -1 | f | 1 | -1
s_nocache | public | bigint | 1 | 1 | 9223372036854775807 | 1 | f | 1 | 1
(4 rows)last_value 有个大坑:它是序列文件里落盘的值,CACHE > 1 时它停在缓存块的边界,不代表真实使用位置。上表 s_cached 显示 103,但实际可能只用到 4。
-- 不要用 SELECT last_value FROM seq 来读"当前值"
SELECT last_value FROM s_cached; -- 可能是缓存边界值,误导 last_value
------------
103
(1 row)information_schema.sequences 也能看,但信息更少且不包含 last_value(它按 SQL 标准只暴露通用元数据),日常运维优先用 pg_sequences。
所有权与 DROP TABLE 级联
OWNED BY 把序列绑定到列上,形成「序列属于这一列」的关系。删除列或表时,序列随之删除:
DROP TABLE IF EXISTS t_owned;
CREATE TABLE t_owned (id BIGINT PRIMARY KEY);
CREATE SEQUENCE s_owned OWNED BY t_owned.id;-- 找到某列拥有的序列
SELECT pg_get_serial_sequence('t_owned', 'id') AS seq_name; seq_name
----------------
public.s_owned
(1 row)-- 删除列,序列跟着消失
ALTER TABLE t_owned DROP COLUMN id;
SELECT count(*) AS orphan_left FROM pg_sequences WHERE sequencename = 's_owned'; orphan_left
-------------
0
(1 row)反向的坑也存在:SERIAL 的序列若所有权丢失(如被 ALTER TABLE ... DROP COLUMN 后残留),就会变成孤儿序列。检查孤儿序列:
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')
ORDER BY 1, 2;重建所有权用 ALTER SEQUENCE ... OWNED BY table.column。
ALTER SEQUENCE:常见调整
-- 改缓存与步长(分片场景可用不同步长错开)
ALTER SEQUENCE s_cached CACHE 500 INCREMENT BY 1;
-- 升级序列类型(SERIAL 项目扩容)
ALTER SEQUENCE s_full AS bigint;
-- 重置当前值(等价 setval + is_called=true 的语义)
ALTER SEQUENCE s_full RESTART WITH 1;
-- 改名
ALTER SEQUENCE s_full RENAME TO s_full_v2;关键提醒:ALTER SEQUENCE ... AS bigint 只改序列的取值范围,不改列的整数类型。SERIAL 列仍是 integer,需要单独改列:
-- SERIAL 项目扩容的两步(第二步在大表上是重写,务必评估锁)
-- ALTER SEQUENCE products_id_seq AS bigint;
-- ALTER TABLE products ALTER COLUMN id TYPE bigint;ALTER SEQUENCE ... RESTART 与 setval 的差异:
| 方式 | 是否需要序列名 | 语义 |
|---|---|---|
ALTER SEQUENCE s RESTART WITH n | 需要 | 下一次 nextval 返回 n(等价 setval 的「已消费」语义,但更直观) |
setval('s', n, true) | 需要 | 下一次返回 n + increment |
setval('s', n, false) | 需要 | 下一次返回 n |
权限:序列需要单独的 USAGE
这是最常见的权限陷阱:只给表 INSERT 权限不够,nextval 需要对序列本身的 USAGE 权限。
-- 模拟:应用账号只拿到表的 INSERT,没拿到序列权限
-- CREATE ROLE app_user LOGIN;
-- GRANT INSERT ON public.orders TO app_user;
-- 结果:INSERT 时 nextval 报 permission denied for sequence
-- 正确:显式授予序列权限
-- GRANT USAGE, SELECT ON SEQUENCE public.orders_id_seq TO app_user;权限一览:
| 权限 | 作用 |
|---|---|
USAGE | 允许 nextval / currval |
SELECT | 允许 currval / lastval / 读 pg_sequences 的 last_value |
UPDATE | 允许 setval 与 ALTER SEQUENCE 的部分操作 |
ALL | 以上全部 |
IDENTITY 列会自动管理序列权限(随列权限走),这就是推荐新项目用 IDENTITY 而非 SERIAL 的现实理由之一(详见 SERIAL 与序列管理)。
并发:不阻塞,也不会回滚
序列的递增是原子的,多个会话并发调用不会拿到重复值,也不会互相阻塞:
DROP TABLE IF EXISTS t_concurrent;
CREATE TABLE t_concurrent (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, worker TEXT);
-- 用 4 个并发会话各插 1000 行(实际用 shell 并发发起)
-- psql -c "INSERT INTO t_concurrent (worker) SELECT 'w1' FROM generate_series(1,1000);" &
-- psql -c "INSERT INTO t_concurrent (worker) SELECT 'w2' FROM generate_series(1,1000);" &
-- psql -c "INSERT INTO t_concurrent (worker) SELECT 'w3' FROM generate_series(1,1000);" &
-- psql -c "INSERT INTO t_concurrent (worker) SELECT 'w4' FROM generate_series(1,1000);" &
SELECT count(*) AS total, count(DISTINCT id) AS distinct_ids FROM t_concurrent; total | distinct_ids
-------+--------------
4000 | 4000
(1 row)这是 PostgreSQL 序列相对 MySQL 自增锁的核心优势:取号不需要等待,插入也不因取号而串行。代价就是前面说的:值不遵循事务可见性,会话 A 取到 5 后回滚,会话 B 仍会取到 6。
实战示例
场景:订单表在数据迁移时回填了历史 id,随后自动插入主键冲突——这是最高频的序列运维事故。
第 1 步:建表并模拟迁移(回填大 id)
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_no TEXT NOT NULL UNIQUE,
amount NUMERIC(12,2) NOT NULL CHECK (amount >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 迁移脚本回填了历史 id
INSERT INTO orders (id, order_no, amount) VALUES
(1000, 'SO-HIST-1000', 199.00),
(1005, 'SO-HIST-1005', 88.00)
ON CONFLICT (order_no) DO NOTHING;CREATE TABLE
INSERT 0 2第 2 步:不修复序列,直接自动插入 → 主键冲突
-- 序列仍停在 2(因为回填的是 BY DEFAULT 列,不推进序列)
SELECT pg_get_serial_sequence('orders', 'id') AS seq_name;
SELECT last_value FROM pg_sequences WHERE sequencename = 'orders_id_seq'; seq_name
------------------------
public.orders_id_seq
(1 row)
last_value
------------
2
(1 row)-- 下一条自动插入会拿到 3,暂时不冲突,但一旦积累到 1000 就会撞上历史 id
INSERT INTO orders (order_no, amount) VALUES ('SO-NEW-1', 10.00) RETURNING id; id
----
3
(1 row)第 3 步:修复——把序列调到当前最大 id
SELECT setval(
pg_get_serial_sequence('orders', 'id'), -- 拿到序列名
(SELECT max(id) FROM orders), -- 当前最大值
true -- 该值已被占用
) AS fixed_to; fixed_to
----------
1005
(1 row)-- 下一条从 1006 开始
INSERT INTO orders (order_no, amount) VALUES ('SO-NEW-2', 20.00) RETURNING id; id
------
1006
(1 row)第 4 步:对照 is_called = false 的错误修复
-- 错!false 会让下一条又拿到 1006,若已有该行就冲突
SELECT setval(pg_get_serial_sequence('orders', 'id'), 1006, false) AS wrong_way;
SELECT nextval(pg_get_serial_sequence('orders', 'id')) AS conflict_prone; wrong_way
-----------
1006
(1 row)
conflict_prone
----------------
1006
(1 row)这就是「is_called 写错 = 埋一颗冲突地雷」的具体形态。修复时永远用 true。
第 5 步:用 currval 跨主表/子表取号
DROP TABLE IF EXISTS order_items;
CREATE TABLE order_items (
id BIGINT NOT NULL,
sku TEXT NOT NULL,
FOREIGN KEY (id) REFERENCES orders(id)
);
BEGIN;
INSERT INTO orders (id, order_no, amount)
VALUES (nextval(pg_get_serial_sequence('orders', 'id')), 'SO-COMBO-1', 33.00);
INSERT INTO order_items (id, sku)
VALUES (currval(pg_get_serial_sequence('orders', 'id')), 'SKU-X');
COMMIT;
SELECT o.id, o.order_no, i.sku
FROM orders o JOIN order_items i ON i.id = o.id
WHERE o.order_no = 'SO-COMBO-1'; id | order_no | sku
------+------------+-------
1007 | SO-COMBO-1 | SKU-X
(1 row)第 6 步:批量校正所有 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'
ORDER BY c.table_name; fix_sql
---------------------------------------------------------------------------------------------------------------------
SELECT setval(pg_get_serial_sequence('public.orders', 'id'), COALESCE(max("id"), 1), max("id") IS NOT NULL) FROM public.orders;
(1 row)第 7 步:验证无重复、无冲突
SELECT count(*) AS total, count(DISTINCT id) AS distinct_ids, max(id) AS max_id FROM orders; total | distinct_ids | max_id
-------+--------------+--------
5 | 5 | 1007
(1 row)第 8 步:清理
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS t_gap_demo;
DROP TABLE IF EXISTS t_concurrent;
DROP TABLE IF EXISTS t_owned;
DROP SEQUENCE IF EXISTS demo_seq, s_a, s_b, s_set, s_full, s_down, s_cycle, s_nocache, s_cached, s_tx;
DROP SCHEMA IF EXISTS demo;DROP TABLE
DROP SEQUENCE
DROP SCHEMA注意事项
nextval不回滚。ROLLBACK之后号码不会归还,id 必然有空洞。
BEGIN;
SELECT nextval('demo_seq');
ROLLBACK;
SELECT currval('demo_seq'); -- 仍是刚才取的值currval必须在同一会话先调用过nextval,否则报is not yet defined in this session。
-- 新连接直接调用 → 报错
-- SELECT currval('orders_id_seq');
-- 正确顺序
SELECT nextval('orders_id_seq'), currval('orders_id_seq');- 不要用
lastval(),它跨序列且只记最后一个,多序列场景必然出错。一律用currval('seq')。
-- 危险
SELECT nextval('s_a'), nextval('s_b'), lastval(); -- 是 s_b 的值
-- 安全
SELECT nextval('s_a'), currval('s_a');setval的第三参是最易错点。表里已有最大值时用true;只有「想把下一个号设为 n」时才用false。
-- 回填后修复:用 true
SELECT setval('orders_id_seq', (SELECT max(id) FROM orders), true);SELECT last_value FROM seq不可信。CACHE > 1时它停在缓存块边界;要看元数据请查pg_sequences。
SELECT * FROM pg_sequences WHERE sequencename = 'orders_id_seq';- 序列需要单独的
USAGE权限,只给表INSERT权限不够。
GRANT USAGE, SELECT ON SEQUENCE public.orders_id_seq TO app_user;ALTER SEQUENCE ... AS bigint不改列类型,SERIAL列扩容必须同时改列。跨 schema 一律用全限定名,不要依赖
search_path,否则函数与迁移脚本在不同会话下会失败。
SELECT nextval('demo.orders_id_seq'); -- 明确CYCLE用在高频主键上是灾难,回绕必然冲突。默认保持NO CYCLE。不要在一条语句里依赖多个
nextval的求值顺序。虽然实测常是从左到右,但 SQL 标准不保证。
-- 不要这样写
SELECT nextval('s_a'), nextval('s_a'); -- 相对顺序不保证- 不要在
generate_series里批量预取号,会瞬间消耗大量序列值。
-- 危险:一次性消耗 100 万个序列值
-- INSERT INTO t (id) SELECT nextval('s') FROM generate_series(1, 1000000);DROP TABLE不会删除未绑定所有权的序列(OWNED BY NONE的序列会残留),定期检查孤儿序列。序列不是全局发号器:多实例部署下每个库各有自己的序列,跨库唯一性要靠 UUID 或分布式发号器(见 UUID 生成函数)。
最佳实践
- 新项目一律用
IDENTITY,不用SERIAL,序列权限与所有权自动管理(详见 SERIAL 与序列管理)。
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY只把自增 id 当内部代理键,对外暴露的编号另设业务列并加唯一约束,避免把内部量级与空洞暴露给用户。
数据迁移收尾必须校正序列,把
setval(..., true)写进迁移脚本并纳入回归测试。
SELECT setval(pg_get_serial_sequence('orders', 'id'), (SELECT max(id) FROM orders), true);- 取号与使用在同一事务内用
currval串联,减少往返、避免读错号。
INSERT INTO headers (id, ...) VALUES (nextval('seq'), ...);
INSERT INTO lines (id, ...) VALUES (currval('seq'), ...);- 高并发写表调大
CACHE(100~1000),用可接受的大空洞换写入吞吐;审计类表用CACHE 1。
CREATE SEQUENCE s_fast CACHE 200;setval与ALTER SEQUENCE ... RESTART选择:日常修复用ALTER TABLE ... ALTER COLUMN ... RESTART WITH n(IDENTITY 专用,最直观);脚本通用场景用pg_get_serial_sequence()+setval。
ALTER TABLE orders ALTER COLUMN id RESTART WITH 2000;序列命名统一约定(
表名_列名_seq),便于批量运维与识别孤儿序列。定期检查孤儿序列(
relkind = 'S'且无pg_depend关联),迁移或重构后清理。显式声明
NO CYCLE,即使它是默认值——让读代码的人一眼看到「耗尽即报错」的意图。序列的监视纳入监控:对接近
max_value的序列告警(尤其integer序列的 21 亿上限)。
SELECT sequencename, last_value, max_value,
round(100.0 * last_value / max_value, 2) AS used_pct
FROM pg_sequences
WHERE last_value IS NOT NULL AND max_value > 0
ORDER BY used_pct DESC NULLS LAST
LIMIT 10;下一节
继续学习:SERIAL 与序列管理