Skip to content

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_calledsetval 的第三参;决定下一次 nextval 返回 n 还是 n+1
序列空洞(Gap)因回滚、冲突、缓存丢失导致编号不连续;是设计特性而非缺陷
序列缓存(Cache)一次预取多个值到后端进程内存,减少 I/O,进程崩溃会丢弃未用值
所有权(OWNED BY)把序列绑定到某列,删列/删表时序列随之删除
regclass序列/表/视图等对象 OID 的类型,nextval('seq'::regclass) 用它解析名字

最重要的一条特性先摆在这里:nextval 的推进不在事务里。ROLLBACK 之后号码不会归还。

sql
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;
text
 id |   tag
----+-----------
  2 | committed
(1 row)

拿到的是 2 而不是 1。id 不连续是常态,业务代码不允许假设连续。

语法与用法 ​

text
-- 取下一个值(推进序列)
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)      -- 显式转型

最小示例:

sql
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;
text
 v1  | v2  | cur | last
-----+-----+-----+------
 100 | 110 | 110 |  110
(1 row)

详细说明 ​

nextval:非事务性是核心特性 ​

nextval 是原子且非事务的:并发调用不会拿到重复值,也不需要等锁;代价是值一旦取出就与事务脱钩。

sql
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
text
 inside_tx
-----------
         1
(1 row)

 after_rollback
----------------
              2
(1 row)

这个行为带来两个直接后果:

  1. 编号会有空洞,任何「流水号必须连续」的业务需求都不能用序列实现。
  2. 序列不会成为写入瓶颈,因为它不参与 MVCC,不需要等待其他事务提交。

空洞的来源与是否可避免:

来源说明可避免?
事务回滚INSERT 后 ROLLBACK,序列不回收不可,设计特性
唯一键冲突ON CONFLICT 失败的行也消耗了序列值不可,设计特性
缓存丢失后端进程崩溃/断开,未使用的缓存值丢弃可,用 CACHE 1 换性能
批量预分配SELECT nextval(...) FROM generate_series(...) 提前占号可,改成按需调用

currval:限本会话,且必须先 nextval ​

currval 返回当前会话最近一次 nextval 得到的值。它不推进序列,且有一个硬性前提:本会话必须已经调用过该序列的 nextval。

sql
-- 新会话直接调用:报错
SELECT currval('demo_seq');
text
ERROR:  currval of sequence "demo_seq" is not yet defined in this session

正确用法是先取号再读回:

sql
SELECT nextval('demo_seq') AS taken, currval('demo_seq') AS got;
text
 taken | got
-------+-----
   120 | 120
(1 row)

currval 的会话隔离在设计上是必要的:它让「取号 → 插入主表 → 用同一个号写子表」这条链路无需往返应用层。

sql
-- 典型用法:同一事务内,主表与明细表共用刚取到的号
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;
text
 id  | sku
-----+-------
 130 | SKU-A
(1 row)

lastval:跨序列的陷阱 ​

lastval() 不带序列名,返回本会话最近一次 nextval 的值,无论那个序列是什么。这在多序列场景下极易出错:

sql
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;
text
 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() 只在「本会话只用一个序列」的极简场景下才安全。

sql
-- 危险:中间夹了别的 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。
sql
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;
text
 set_called
------------
        300
(1 row)

 next_after_true
-----------------
             301
(1 row)
sql
-- 第二个会话(或重置后再来):设为未使用
SELECT setval('s_set', 500, false) AS set_uncalled;
SELECT nextval('s_set')            AS next_after_false;
text
 set_uncalled
--------------
          500
(1 row)

 next_after_false
------------------
              500
(1 row)

对照表,必须背下来:

调用is_called下次 nextval 返回适用场景
setval('s', 300)true(默认)301300 已存在于表中
setval('s', 300, true)true301同上
setval('s', 300, false)false300300 尚未使用,想让下一条用它
setval('s', 300, false) 后立刻 nextval—300,且此后变 true空表初始化为 300 起

为什么这个参数重要:回填历史数据后,如果表里最大 id 是 1000,你希望下一条自增从 1001 开始,所以必须用 true。如果用 false,下一条会得到 1000,直接主键冲突。

sql
-- 错!表里已有 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;
text
 correct_fix
-------------
        1000
(1 row)

 next_value
------------
       1001
(1 row)

regclass 与名字解析 ​

nextval / currval / setval 的形参类型是 regclass,字符串会被隐式转换。名字解析走 search_path,因此跨 schema 必须限定名:

sql
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;
text
 qualified | cast_regclass | via_to_regclass
-----------+---------------+-----------------
         1 |             2 |               3
(1 row)

用 regclass 的优势是绑定到对象 OID:序列若被重命名,已解析的 regclass 引用仍指向同一对象;而纯字符串每次都要重新走名字解析。

sql
-- 相对名在 search_path 不含 demo 时会报错
SET search_path = public;
SELECT nextval('orders_id_seq');
text
ERROR:  relation "orders_id_seq" does not exist

实践规则:函数、脚本、迁移里一律用 schema.seq_name 全限定名,不要依赖 search_path。

CREATE SEQUENCE 参数全集 ​

sql
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 typebigint序列取值范围;PostgreSQL 10+ 可指定 smallint / integer / bigint
INCREMENT1步长;负数表示递减
MINVALUE升序为 1,降序为类型最小值下界
MAXVALUE类型最大值上界
STARTMINVALUE(升序)/ MAXVALUE(降序)起始值
CACHE1每会话预取值个数;越大越省 I/O,空洞越大
CYCLENO CYCLE到达边界后是否回绕
OWNED BYNONE归属列;绑定后随列/表删除而删除

递减序列(从 0 往下走):

sql
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;
text
 v1 | v2
----+-----
  0 |  -1
(1 row)

CYCLE 到达上界后回绕到 MINVALUE:

sql
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;
text
 a | b | c | d
---+---+---+---
 1 | 2 | 3 | 1
(1 row)

CYCLE 用在主键上等同于埋雷——回绕后必然主键冲突。只适合「环形缓冲区序号」这类特殊场景。

CACHE:性能与空洞的取舍 ​

CACHE 是序列最主要的性能旋钮。每次 nextval 都要写 WAL 并保证崩溃后可恢复,这是 I/O 成本;CACHE > 1 让每个后端进程一次预取一批值到本地内存,之后在本进程内递增无需 I/O。

sql
DROP SEQUENCE IF EXISTS s_nocache;
DROP SEQUENCE IF EXISTS s_cached;
CREATE SEQUENCE s_nocache CACHE 1;
CREATE SEQUENCE s_cached  CACHE 100;
sql
-- 单个会话里看,缓存序列完全连续
SELECT nextval('s_cached') AS a, nextval('s_cached') AS b, nextval('s_cached') AS c;
text
 a | b | c
---+---+---
 1 | 2 | 3
(1 row)
sql
-- 另一个会话拿到的从 101 开始,而不是 4
SELECT nextval('s_cached') AS other_session;
text
 other_session
---------------
           101
(1 row)
现象原因
不同会话的值不连续每个会话独立缓存,各占一段
会话退出后剩下的值消失未使用的缓存值不回写(放弃即丢失)
崩溃后大段跳号同上,且跳的是一整块 CACHE

取舍:高并发短事务场景用大 CACHE(100~1000)显著降低序列争用;审计、发票这类对连续性敏感的表用 CACHE 1。但无论 CACHE 多大,都不能满足「绝对连续」需求——那需要计数器表。

pg_sequences 视图与 information_schema ​

pg_sequences 是查看序列元数据最方便的视图(PostgreSQL 10+):

sql
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;
text
 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。

sql
-- 不要用 SELECT last_value FROM seq 来读"当前值"
SELECT last_value FROM s_cached;   -- 可能是缓存边界值,误导
text
 last_value
------------
        103
(1 row)

information_schema.sequences 也能看,但信息更少且不包含 last_value(它按 SQL 标准只暴露通用元数据),日常运维优先用 pg_sequences。

所有权与 DROP TABLE 级联 ​

OWNED BY 把序列绑定到列上,形成「序列属于这一列」的关系。删除列或表时,序列随之删除:

sql
DROP TABLE IF EXISTS t_owned;
CREATE TABLE t_owned (id BIGINT PRIMARY KEY);
CREATE SEQUENCE s_owned OWNED BY t_owned.id;
sql
-- 找到某列拥有的序列
SELECT pg_get_serial_sequence('t_owned', 'id') AS seq_name;
text
    seq_name
----------------
 public.s_owned
(1 row)
sql
-- 删除列,序列跟着消失
ALTER TABLE t_owned DROP COLUMN id;

SELECT count(*) AS orphan_left FROM pg_sequences WHERE sequencename = 's_owned';
text
 orphan_left
-------------
           0
(1 row)

反向的坑也存在:SERIAL 的序列若所有权丢失(如被 ALTER TABLE ... DROP COLUMN 后残留),就会变成孤儿序列。检查孤儿序列:

sql
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:常见调整 ​

sql
-- 改缓存与步长(分片场景可用不同步长错开)
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,需要单独改列:

sql
-- 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 权限。

sql
-- 模拟:应用账号只拿到表的 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 与序列管理)。

并发:不阻塞,也不会回滚 ​

序列的递增是原子的,多个会话并发调用不会拿到重复值,也不会互相阻塞:

sql
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;
text
 total | distinct_ids
-------+--------------
  4000 |         4000
(1 row)

这是 PostgreSQL 序列相对 MySQL 自增锁的核心优势:取号不需要等待,插入也不因取号而串行。代价就是前面说的:值不遵循事务可见性,会话 A 取到 5 后回滚,会话 B 仍会取到 6。

实战示例 ​

场景:订单表在数据迁移时回填了历史 id,随后自动插入主键冲突——这是最高频的序列运维事故。

第 1 步:建表并模拟迁移(回填大 id)

sql
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;
text
CREATE TABLE
INSERT 0 2

第 2 步:不修复序列,直接自动插入 → 主键冲突

sql
-- 序列仍停在 2(因为回填的是 BY DEFAULT 列,不推进序列)
SELECT pg_get_serial_sequence('orders', 'id') AS seq_name;
SELECT last_value FROM pg_sequences WHERE sequencename = 'orders_id_seq';
text
        seq_name
------------------------
 public.orders_id_seq
(1 row)

 last_value
------------
          2
(1 row)
sql
-- 下一条自动插入会拿到 3,暂时不冲突,但一旦积累到 1000 就会撞上历史 id
INSERT INTO orders (order_no, amount) VALUES ('SO-NEW-1', 10.00) RETURNING id;
text
 id
----
  3
(1 row)

第 3 步:修复——把序列调到当前最大 id

sql
SELECT setval(
         pg_get_serial_sequence('orders', 'id'),   -- 拿到序列名
         (SELECT max(id) FROM orders),             -- 当前最大值
         true                                       -- 该值已被占用
       ) AS fixed_to;
text
 fixed_to
----------
     1005
(1 row)
sql
-- 下一条从 1006 开始
INSERT INTO orders (order_no, amount) VALUES ('SO-NEW-2', 20.00) RETURNING id;
text
  id
------
 1006
(1 row)

第 4 步:对照 is_called = false 的错误修复

sql
-- 错!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;
text
 wrong_way
-----------
      1006
(1 row)

 conflict_prone
----------------
           1006
(1 row)

这就是「is_called 写错 = 埋一颗冲突地雷」的具体形态。修复时永远用 true。

第 5 步:用 currval 跨主表/子表取号

sql
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';
text
  id  |  order_no  |  sku
------+------------+-------
 1007 | SO-COMBO-1 | SKU-X
(1 row)

第 6 步:批量校正所有 IDENTITY 列的序列(迁移收尾脚本)

先生成 SQL,人工复核后再执行——这是生产环境最安全的做法:

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;
text
                                                        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 步:验证无重复、无冲突

sql
SELECT count(*) AS total, count(DISTINCT id) AS distinct_ids, max(id) AS max_id FROM orders;
text
 total | distinct_ids | max_id
-------+--------------+--------
     5 |            5 |   1007
(1 row)

第 8 步:清理

sql
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;
text
DROP TABLE
DROP SEQUENCE
DROP SCHEMA

注意事项 ​

  • nextval 不回滚。ROLLBACK 之后号码不会归还,id 必然有空洞。
sql
BEGIN;
SELECT nextval('demo_seq');
ROLLBACK;
SELECT currval('demo_seq');   -- 仍是刚才取的值
  • currval 必须在同一会话先调用过 nextval,否则报 is not yet defined in this session。
sql
-- 新连接直接调用 → 报错
-- SELECT currval('orders_id_seq');
-- 正确顺序
SELECT nextval('orders_id_seq'), currval('orders_id_seq');
  • 不要用 lastval(),它跨序列且只记最后一个,多序列场景必然出错。一律用 currval('seq')。
sql
-- 危险
SELECT nextval('s_a'), nextval('s_b'), lastval();   -- 是 s_b 的值
-- 安全
SELECT nextval('s_a'), currval('s_a');
  • setval 的第三参是最易错点。表里已有最大值时用 true;只有「想把下一个号设为 n」时才用 false。
sql
-- 回填后修复:用 true
SELECT setval('orders_id_seq', (SELECT max(id) FROM orders), true);
  • SELECT last_value FROM seq 不可信。CACHE > 1 时它停在缓存块边界;要看元数据请查 pg_sequences。
sql
SELECT * FROM pg_sequences WHERE sequencename = 'orders_id_seq';
  • 序列需要单独的 USAGE 权限,只给表 INSERT 权限不够。
sql
GRANT USAGE, SELECT ON SEQUENCE public.orders_id_seq TO app_user;
  • ALTER SEQUENCE ... AS bigint 不改列类型,SERIAL 列扩容必须同时改列。

  • 跨 schema 一律用全限定名,不要依赖 search_path,否则函数与迁移脚本在不同会话下会失败。

sql
SELECT nextval('demo.orders_id_seq');   -- 明确
  • CYCLE 用在高频主键上是灾难,回绕必然冲突。默认保持 NO CYCLE。

  • 不要在一条语句里依赖多个 nextval 的求值顺序。虽然实测常是从左到右,但 SQL 标准不保证。

sql
-- 不要这样写
SELECT nextval('s_a'), nextval('s_a');   -- 相对顺序不保证
  • 不要在 generate_series 里批量预取号,会瞬间消耗大量序列值。
sql
-- 危险:一次性消耗 100 万个序列值
-- INSERT INTO t (id) SELECT nextval('s') FROM generate_series(1, 1000000);
  • DROP TABLE 不会删除未绑定所有权的序列(OWNED BY NONE 的序列会残留),定期检查孤儿序列。

  • 序列不是全局发号器:多实例部署下每个库各有自己的序列,跨库唯一性要靠 UUID 或分布式发号器(见 UUID 生成函数)。

最佳实践 ​

  • 新项目一律用 IDENTITY,不用 SERIAL,序列权限与所有权自动管理(详见 SERIAL 与序列管理)。
sql
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
  • 只把自增 id 当内部代理键,对外暴露的编号另设业务列并加唯一约束,避免把内部量级与空洞暴露给用户。

  • 数据迁移收尾必须校正序列,把 setval(..., true) 写进迁移脚本并纳入回归测试。

sql
SELECT setval(pg_get_serial_sequence('orders', 'id'), (SELECT max(id) FROM orders), true);
  • 取号与使用在同一事务内用 currval 串联,减少往返、避免读错号。
sql
INSERT INTO headers (id, ...) VALUES (nextval('seq'), ...);
INSERT INTO lines   (id, ...) VALUES (currval('seq'), ...);
  • 高并发写表调大 CACHE(100~1000),用可接受的大空洞换写入吞吐;审计类表用 CACHE 1。
sql
CREATE SEQUENCE s_fast CACHE 200;
  • setval 与 ALTER SEQUENCE ... RESTART 选择:日常修复用 ALTER TABLE ... ALTER COLUMN ... RESTART WITH n(IDENTITY 专用,最直观);脚本通用场景用 pg_get_serial_sequence() + setval。
sql
ALTER TABLE orders ALTER COLUMN id RESTART WITH 2000;
  • 序列命名统一约定(表名_列名_seq),便于批量运维与识别孤儿序列。

  • 定期检查孤儿序列(relkind = 'S' 且无 pg_depend 关联),迁移或重构后清理。

  • 显式声明 NO CYCLE,即使它是默认值——让读代码的人一眼看到「耗尽即报错」的意图。

  • 序列的监视纳入监控:对接近 max_value 的序列告警(尤其 integer 序列的 21 亿上限)。

sql
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 与序列管理

参考链接 ​