序列(SEQUENCE)与生成列
本教程共 50 篇 · 第 43 篇 · 更新于 2026-07-31 · 约 7 分钟阅读
43. 序列(SEQUENCE)与生成列
本节目标:理解序列如何生成唯一数字,掌握 CREATE SEQUENCE 与生成列(GENERATED)的用法。
什么是序列
序列(SEQUENCE)是数据库里专门产唯一整数号的对象。最常见用途就是给主键发号。
其实你用 GENERATED AS IDENTITY 建主键时,PostgreSQL 背后就是自动建了一个序列。本章看看怎么自己掌控它,适合需要跨表统一发号、或自定义起始值的场景。
创建序列
postgres=# CREATE SEQUENCE order_no_seq
postgres=# START WITH 1000
postgres=# INCREMENT BY 1;
可选参数:
INCREMENT BY n:步长,负数就是倒着生成。MINVALUE/MAXVALUE:上下限。CYCLE/NO CYCLE:到头了是否从头再来(默认不循环)。CACHE n:预取 n 个到内存,提升并发性能,但崩溃可能丢失一段号。OWNED BY 表.列:列删了序列跟着删。
postgres=# CREATE SEQUENCE order_no_seq
postgres=# START WITH 1000
postgres=# INCREMENT BY 1
postgres=# CACHE 10
postgres=# OWNED BY orders.id;
取号:nextval 与 currval
nextval 取下一个号,并且一定自增,多会话并发也安全:
postgres=# SELECT nextval('order_no_seq'); -- 1000
postgres=# SELECT nextval('order_no_seq'); -- 1001
currval 拿”本次会话上一次 nextval 取到的值”:
postgres=# SELECT currval('order_no_seq'); -- 1001
还有 lastval(),返回本次会话里”任意序列”最近一次 nextval 的值,不用写序列名:
postgres=# SELECT lastval();
Warning没在本会话调用过
nextval就直接用currval或lastval,会报错。它们是会话级别的,别的会话取到的值你看不到。
setval 可以手动把序列拨到某个值:
postgres=# SELECT setval('order_no_seq', 5000); -- 下次 nextval 从 5001 起
postgres=# SELECT setval('order_no_seq', 5000, false); -- 下次 nextval 就是 5000
setval 的第三个参数 is_called:true(默认)表示”已用过这个数”,下次从 5001 起;false 表示”下次就用 5000”。
Note序列号不保证连续。事务回滚、ON CONFLICT 冲突,都会”浪费”号,留下空洞,这是正常现象。别拿序列号当”连续业务单号”去对账。
在表里用序列
手动把序列接到列上:
postgres=# CREATE TABLE orders (
postgres=# id INT DEFAULT nextval('order_no_seq') PRIMARY KEY,
postgres=# user_id INT,
postgres=# amount NUMERIC(10,2)
postgres=# );
IDENTITY 与序列的关系
第 15 章讲过,主键推荐用 GENERATED ALWAYS AS IDENTITY,它底层就是序列,但由系统托管:
postgres=# CREATE TABLE orders (
postgres=# id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
postgres=# ...
postgres=# );
想重置它的起始值,用 ALTER TABLE ... RESTART:
postgres=# ALTER TABLE orders ALTER COLUMN id RESTART WITH 1000;
Tip想给主键自动发号,优先用
GENERATED AS IDENTITY,比手写序列更省心(删表时序列自动清理)。序列适合需要跨表统一发号、或自定义起始值 / 步长的场景。
生成列(GENERATED)
生成列(Generated Column)的值由表达式自动算出来,插数据时你不能手动指定它。分两种:
GENERATED ALWAYS AS (表达式) STORED:结果真实存到磁盘。GENERATED ALWAYS AS (表达式) VIRTUAL:不存,查询时现算。PostgreSQL 18 起正式支持 VIRTUAL,而且省略关键字时默认就是 VIRTUAL。
postgres=# CREATE TABLE order_items (
postgres=# id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
postgres=# qty INT,
postgres=# price NUMERIC(10,2),
postgres=# total NUMERIC(12,2) GENERATED ALWAYS AS (qty * price) STORED
postgres=# );
postgres=# INSERT INTO order_items (qty, price) VALUES (3, 9.5);
postgres=# SELECT * FROM order_items; -- total 自动是 28.50
生成列可以引用其它普通列,甚至引用另一个生成列:
postgres=# CREATE TABLE order_items2 (
postgres=# id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
postgres=# qty INT,
postgres=# price NUMERIC(10,2),
postgres=# subtotal NUMERIC(12,2) GENERATED ALWAYS AS (qty * price) STORED,
postgres=# tax NUMERIC(12,2) GENERATED ALWAYS AS (subtotal * 0.13) STORED
postgres=# );
Note生成列和视图不同:生成列是真实存着的列,跟着行一起读写;它也不能在 INSERT 里赋值,强行写会报错。它可以建索引,适合常按派生值筛选的场景。
生成列的限制
- 不能在 INSERT / UPDATE 里给生成列赋值(除非用
DEFAULT关键字)。 - 生成列不能写自己的 DEFAULT。
- 生成列的表达式不能含易变函数(如
random()、now()),也不能引用其它表的列。
postgres=# INSERT INTO order_items (qty, price, total) VALUES (3, 9.5, 30);
ERROR: cannot insert into column "total"
DETAIL: Column "total" is a generated column.
Tip想手动覆盖一次生成列的值,可以写
INSERT ... (qty, price, total) VALUES (3, 9.5, DEFAULT),让数据库照常计算,而不是报错。
序列的边界与循环
序列底层是 bigint,理论上限很大(约 922 亿亿亿)。如果 NO CYCLE(默认)且用到底,下一个 nextval 会报错。想让它到头后从头来,建时加 CYCLE:
postgres=# CREATE SEQUENCE seq_cycle START WITH 1 INCREMENT BY 1 MAXVALUE 3 CYCLE;
postgres=# SELECT nextval('seq_cycle'); -- 1, 2, 3, 然后又回到 1
Warning
CYCLE有坑:它会让”已经用过”的号重新出现,如果这号被当成唯一键就会冲突。一般只在对唯一性没要求的场景(比如循环计数器)才用。
复制表时序列的坑
用 CREATE TABLE new AS SELECT * FROM old 复制数据时,自增列会原样把当前值拷过去,但不会复制背后的序列,新表插入可能主键冲突。想连结构带序列复制,用 LIKE ... INCLUDING ALL:
postgres=# CREATE TABLE orders_bak (LIKE orders INCLUDING ALL);
如果只用 LIKE orders,IDENTITY 属性会保留但新序列从 1 开始,老数据可能占了 1 号,插入就撞。复制后记得 ALTER ... RESTART WITH 调整起点。
生成列上也能建索引
生成列是真实存在的列,当然能建索引,适合常按派生值筛选、排序的场景:
postgres=# CREATE INDEX idx_order_items_total ON order_items (total);
postgres=# SELECT * FROM order_items WHERE total > 100 ORDER BY total;
三种派生数据方式怎么选
- 视图:不存数据,实时,适合封装查询、控权限,但慢。
- 生成列:存数据,随行自动算,快,但只能依赖本行其它列。
- 触发器计算字段:最灵活,能跨表、跨行算,但写起来最重、拖慢写入。
Tip如果只是本行内的简单计算(如
qty * price),优先用生成列,最省心;需要跨行汇总才上触发器。
序列 vs IDENTITY 到底选谁
一句话:给主键发号,优先 GENERATED AS IDENTITY,系统帮你管序列、删表时自动清理,最省心。需要”跨多张表共用一套号""自定义起始值或循环""手动 setval 跳号”这类高级玩法,才自己建 SEQUENCE。多数业务用 IDENTITY 就够了,别为了炫技手搓序列。
生成列的适用边界
生成列适合”本行内、确定性的派生值”,比如金额小计、全名拼接。它不能跨行汇总(那是视图或触发器的事),也不能引用其他表。如果派生规则会失败(比如除零),生成列会直接拒绝写入——这其实是好事,保证了数据干净。
Note
GENERATED ALWAYS AS IDENTITY不允许你手动插入 id,写INSERT时硬塞值会报错。如果你确实需要临时手填(比如迁移老数据),改用GENERATED BY DEFAULT AS IDENTITY:它会优先用你给的值,没给时再由序列发号。两者底层都是序列,差别只在「是否允许手动指定」,迁移场景用 BY DEFAULT 最方便。但要注意,BY DEFAULT 下如果你手填的值和序列已发出的号撞了,就会主键冲突,所以手填前最好先RESTART WITH让序列跳到更大的起点。
常见误区
- 把序列号当连续业务号对账,结果因为空洞对不上。
- 往生成列里 INSERT 值,直接报错。
- 用
VOLATILE函数写生成列表达式,PG 不允许。 - 给生成列再写 DEFAULT,冲突报错——生成列本身就包含计算规则。