PostgreSQL 序列分配数值,并不以调用方事务最终提交为前提。这让并发场景中的普通标识分配更实用,也意味着空洞是正常现象。一个序列号只说明发生过分配,不能证明对应行存在、事务已经提交,或所有更小的数值都已经使用。把取号与业务记录的生命周期分开,才能正确解释缺号和顺序。
原子分配也有前提
nextval 会原子推进序列。普通非循环序列在没有被重置到已用范围的情况下,能为并发调用方分配不同数值。这不代表实现完全不使用锁,也不代表任何管理操作都不会影响它。CYCLE 和显式重置都可能重新使用数值,因此,已存储行的唯一性仍应由表上的 PRIMARY KEY 或 UNIQUE 约束负责。
序列也有有限边界。非循环序列无法在范围内继续分配时会报错,不会自动扩张成无限整数。应明确选择底层类型和步长;高频标识接近配置上限时,要关注剩余容量。即使序列使用更宽的类型,也不会自动扩宽接收它的表字段,不能只检查分配器而忽略目标列的类型限制。
identity 和 serial 列通过序列提供默认值。序列负责分配,字段约束负责决定哪些行允许写入。显式导入的标识可能让分配器落后于表中数据。修复这种错位时,要考虑活跃写入者和已经预留的数值,不能在业务仍并发写入时,随意根据一次 max 查询执行 setval,然后假设风险已经消失。
空洞与顺序是不同属性
事务回滚不会收回 nextval 已分配的数值。取号后没有使用、插入中止,或 INSERT 走了 ON CONFLICT 分支,都可能产生空洞。删除表中一行也不会让序列倒退。因此,单凭编号缺失,不能认定记录丢失或序列损坏;需要结合事务和业务记录查明实际含义。
CACHE 会按会话预留一批数值。如果一个会话保留一到十,另一个保留十一到二十,外部观察的调用结果就可能是一、十一、二。会话结束时,未使用的预留值也可能被放弃。缓存提高分配效率,却会影响连续性和全局观察顺序;即使没有批量缓存,分配先后也不等于事务提交先后。
setval 对序列状态的修改不会因调用方回滚而撤销。不能把这一点推广到所有序列命令:ALTER SEQUENCE RESTART 是事务性的,并会阻止并发取号直到事务结束。其他会话还可能持有原先缓存的数值,所以重置活跃分配器不是安全的消除空洞方法。如果业务要求连续编号,应明确设计事务内分配与取消语义。这可能需要串行访问一个专用计数器,但不意味着必须锁住业务表中的每一行。不能把普通技术主键与这种业务编号混为一谈。
坏掉的样子
currval 保存指定序列的会话局部值,通常由 nextval 建立。本会话尚未建立这个值时,调用会报错;is_called 为真的 setval 也会修改该会话值。其他连接取过号,并不能为当前连接初始化它。使用连接池时,如果 nextval 与 currval 落到不同数据库会话,原本看似连续的两步操作就可能失败。
应用需要刚插入那一行的标识时,INSERT RETURNING 通常更直接。lastval 比 currval 更宽泛,指向本会话最近使用过的任意序列,在触发器也取号的场景下容易产生误解。两者都不是“查询表中最新已提交行”的通用接口,不能拿会话分配历史代替行级结果。
在事务提交之前,就把分配值持久发布到数据库外,也是需要明确的边界。崩溃恢复后,尚未提交的序列状态变化可能存在不确定性。因此,官方序列文档要求:如果外部持久用途依赖该分配,应先提交取号事务。仅有一个数值,不等于已经形成持久的业务事件。
快速验证
把下面内容保存为 sequence.sql,在可丢弃的 PostgreSQL 18 数据库中执行 psql -X -v ON_ERROR_STOP=1 -f sequence.sql。显式事务之外保持默认自动提交。全部对象都是临时对象,故意执行的重置只作用于本会话的实验序列。
\set ON_ERROR_STOP on
CREATE TEMP SEQUENCE ids;
CREATE TEMP TABLE allocated (stage text,id bigint);
DO $$ BEGIN
PERFORM currval('ids');
RAISE EXCEPTION 'currval was defined before nextval';
EXCEPTION WHEN object_not_in_prerequisite_state THEN
RAISE NOTICE 'currval is undefined in this session before nextval';
END $$;
INSERT INTO allocated VALUES ('first',nextval('ids')) RETURNING *;
BEGIN;
SELECT nextval('ids') AS allocation_in_aborted_transaction;
ROLLBACK;
INSERT INTO allocated VALUES ('after rollback',nextval('ids')) RETURNING *;
SELECT currval('ids') AS session_value;
DO $$ BEGIN
IF (SELECT array_agg(id ORDER BY id) FROM allocated) IS DISTINCT FROM ARRAY[1,3]::bigint[] THEN
RAISE EXCEPTION 'rollback unexpectedly reclaimed a value';
END IF;
END $$;
CREATE TEMP TABLE unique_rows (id bigint PRIMARY KEY);
INSERT INTO unique_rows SELECT id FROM allocated;
SELECT setval('ids',1,false);
DO $$ BEGIN
INSERT INTO unique_rows VALUES (nextval('ids'));
RAISE EXCEPTION 'duplicate identifier accepted';
EXCEPTION WHEN unique_violation THEN
RAISE NOTICE 'the primary key rejected the reset sequence value';
END $$;
SELECT currval('ids') AS reissued_value;
DROP TABLE unique_rows,allocated;
DROP SEQUENCE ids;
本轮 PostgreSQL 18.6 单用户重跑首先拒绝尚未取号时的 currval,再返回一,在中止事务中返回二,回滚后返回三。SQL 命令边界得到保留,未测试 psql 传输。存储下来的分配列表是一和三,证明回滚没有回收二。随后 currval 返回本会话最近拿到的三,既没有追踪其他连接,也没有自动补上缺号。
最后的重置要求下次调用重新返回一。序列照常给出一,但主键拒绝了这个重复标识,currval 则记录重新发出的数值。这说明普通原子分配与重置后的表完整性是两层保证。脚本结束时删除临时对象,不会重排应用已有记录,也不是用来修补生产环境编号空洞的操作方案。
来源
PostgreSQL 序列函数,原子分配、会话值、回滚和外部持久性,已核验 2026-10-15。
PostgreSQL CREATE SEQUENCE,边界、循环、缓存和取号顺序,已核验 2026-10-15。
PostgreSQL ALTER SEQUENCE,RESTART 的事务与阻塞语义,已核验 2026-10-15。