Skip to content

自增类型(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,进程崩溃会丢失未使用的预取值。

语法与用法 ​

两套方案的最小写法对比:

sql
-- 方案一: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 的完整语法:

text
列名 类型 GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY
     [ ( sequence_options ) ]

-- sequence_options 可包含:
--   START WITH 起始值
--   INCREMENT BY 步长
--   MINVALUE / MAXVALUE
--   CACHE 缓存个数
--   CYCLE / NO CYCLE
sql
-- 最简可运行示例
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;
text
 id |       email
----+--------------------
  1 | alice@example.com
  2 | bob@example.com
(2 rows)

详细说明 ​

SERIAL 的真实面目 ​

SERIAL 在解析阶段就被展开成具体的 DDL。可以用 pg_get_serial_sequence() 与系统目录验证:

sql
CREATE TABLE products (
  id   SERIAL PRIMARY KEY,
  name TEXT NOT NULL
);

-- 找到 SERIAL 列背后的序列名
SELECT pg_get_serial_sequence('products', 'id') AS sequence_name;
text
      sequence_name
---------------------------
 public.products_id_seq
(1 row)
sql
-- 查看驱动该列的默认值表达式
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;
text
 column_name | data_type |             default_expr
-------------+-----------+--------------------------------------
 id          | integer   | nextval('products_id_seq'::regclass)
(1 row)
sql
-- 查看序列自身的元数据
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';
text
     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 的差异对比 ​

sql
-- 两者的行为差异演示
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 的许可

sql
-- SERIAL:可以随意手工插入 id
INSERT INTO t_serial (id, v) VALUES (100, 'manual-ok');

-- IDENTITY ALWAYS:默认被拒绝
INSERT INTO t_identity (id, v) VALUES (100, 'manual');
text
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.
sql
-- 需要显式声明 OVERRIDING SYSTEM VALUE 才能覆盖
INSERT INTO t_identity (id, v) OVERRIDING SYSTEM VALUE VALUES (100, 'manual-ok')
RETURNING id, v;
text
 id  |     v
-----+-----------
 100 | manual-ok
(1 row)

差异二:序列所有权与清理

sql
-- SERIAL 的序列是弱绑定:删除列后序列可能残留
ALTER TABLE t_serial DROP COLUMN id;
text
ALTER TABLE
sql
-- 序列仍然存在,成为"孤儿序列"
SELECT sequencename FROM pg_sequences WHERE sequencename = 't_serial_id_seq';
text
    sequencename
--------------------
 t_serial_id_seq
(1 row)
sql
-- IDENTITY 的序列随列一起删除,不会残留
ALTER TABLE t_identity DROP COLUMN id;
SELECT count(*) AS leftover FROM pg_sequences WHERE sequencename LIKE 't_identity%';
text
 leftover
----------
        0
(1 row)

差异三:权限

SERIAL 需要单独给序列授 USAGE 权限;IDENTITY 的序列权限随列权限自动管理:

sql
-- SERIAL 场景:必须额外授权,否则应用账号插入报权限错误
-- GRANT USAGE ON SEQUENCE public.products_id_seq TO app_user;

-- IDENTITY 场景:授予表权限即可
-- GRANT INSERT ON public.orders TO app_user;

完整对比表:

对比项SERIALGENERATED ... 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 ​

sql
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 模式下手工插入会推进序列吗?答案是不会,这正是需要警惕的地方:

sql
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;
text
 id | order_no
----+----------
  1 | LEG-AUTO
(1 row)

此时如果 order_no 不是唯一键,就可能出现 id 冲突。修复办法见后文"重置序列"。

序列操作函数 ​

三个核心函数构成序列操作的基础:

函数作用是否需要先调用 nextval()
nextval(seq)推进并返回下一个值—
currval(seq)返回当前会话最近一次取到的值是
setval(seq, n [, is_called])直接设置当前值否
lastval()返回当前会话最近一次 nextval 的值(不指定序列名)是
sql
CREATE SEQUENCE demo_seq START WITH 100 INCREMENT BY 10;

-- nextval 每调用一次推进一次
SELECT nextval('demo_seq') AS v1, nextval('demo_seq') AS v2;
text
 v1  | v2
-----+-----
 100 | 110
(1 row)

注意在上面的语句中,同一行内的两个 nextval 从左到右求值,所以得到 100 和 110。这不是可靠的行为——不要在一条语句里依赖多个 nextval 的相对顺序,PostgreSQL 不保证求值顺序。

sql
-- currval 返回本会话最近一次取到的值(不推进)
SELECT currval('demo_seq') AS current;
text
 current
---------
     110
(1 row)
sql
-- 新会话中未调用过 nextval 就调用 currval,会报错
-- 换一个连接执行:SELECT currval('demo_seq');
text
ERROR:  currval of sequence "demo_seq" is not yet defined in this session
sql
-- 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;
text
 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)等价于 true310

序列缓存与空洞 ​

序列缓存是性能优化,也是最容易引发"数值跳跃"的原因:

sql
-- 无缓存:每次 nextval 都可能写 WAL 与刷盘
CREATE SEQUENCE seq_nocache CACHE 1;

-- 有缓存:一次预取 100 个值到后端进程内存
CREATE SEQUENCE seq_cached CACHE 100;

查看缓存设置:

sql
SELECT sequencename, cache_size FROM pg_sequences WHERE sequencename LIKE 'seq_%';
text
  sequencename  | cache_size
----------------+------------
 seq_nocache    |          1
 seq_cached     |        100
(1 row)

在单个会话里,缓存序列看起来是连续的:

sql
SELECT nextval('seq_cached') AS a, nextval('seq_cached') AS b, nextval('seq_cached') AS c;
text
 a  | b  | c
----+----+----
  1 |  2 |  3
(1 row)

但另一个会话拿到的会是 101、102……而不是 4:

sql
-- 另开一个连接执行
SELECT nextval('seq_cached') AS other_session_value;
text
 other_session_value
---------------------
                 101
(1 row)

如果第一个会话在只用掉 3 个值时就退出(例如客户端断开、进程崩溃、事务回滚),剩余 97 个值就永久丢失,形成空洞。

空洞的四大来源:

来源说明是否可避免
事务回滚INSERT 后 ROLLBACK,序列不回收不可,属设计特性
唯一键冲突INSERT ... ON CONFLICT 失败的行也消耗序列值不可,属设计特性
序列缓存丢失进程崩溃导致预取值丢失可,用 CACHE 1 换性能
批量预分配SELECT nextval() FROM generate_series(1, n) 提前占号可,按需调用

关键结论:自增 id 不允许假设连续。 如果业务需要"流水号连续",必须用独立的计数器表 + 行锁实现,而不能依赖序列。

重置序列 ​

场景一:手工插入过大 id 后,序列落后,后续插入会主键冲突。

sql
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;
text
 current_max
-------------
        1001
(1 row)
sql
-- 重置序列到最大 id 之后
SELECT setval(
  pg_get_serial_sequence('invoices', 'id'),   -- 拿到序列名
  (SELECT max(id) FROM invoices),             -- 当前最大值
  true                                         -- 该值已被占用
) AS new_seq_value;
text
 new_seq_value
---------------
          1001
(1 row)
sql
-- 下一条自动插入从 1002 开始
INSERT INTO invoices (invoice_no) VALUES ('INV-AUTO') RETURNING id, invoice_no;
text
  id  | invoice_no
------+------------
 1002 | INV-AUTO
(1 row)

更简洁的写法(PostgreSQL 支持在 ALTER TABLE 里直接重置身份列):

sql
-- PostgreSQL 10+
ALTER TABLE invoices ALTER COLUMN id RESTART WITH 2000;

INSERT INTO invoices (invoice_no) VALUES ('INV-RESTART') RETURNING id;
text
  id  | invoice_no
------+-------------
 2000 | INV-RESTART
(1 row)

场景二:批量导入后统一校正所有序列(迁移项目时的排查脚本):

sql
-- 生成一段"校正所有 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';
text
                                          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)

序列参数调整 ​

生产环境常见需求:换更大容量的序列、调大缓存、或改成负向递减。

sql
-- 把 integer 序列升级为 bigint(SERIAL 遗留项目升级时用)
ALTER SEQUENCE products_id_seq AS BIGINT;

SELECT sequencename, data_type, max_value
FROM pg_sequences WHERE sequencename = 'products_id_seq';
text
    sequencename    | data_type |      max_value
--------------------+-----------+---------------------
 products_id_seq    | bigint    | 9223372036854775807
(1 row)
sql
-- 调整缓存与步长(业务分片时常用:不同实例用不同步长错开)
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;
text
 v1 | v2
----+-----
  0 |  -1
(1 row)

注意 ALTER SEQUENCE ... AS BIGINT 只改序列类型,不会改列的类型。SERIAL 列仍是 integer,需要单独改列:

sql
-- 遗留 SERIAL 项目扩容的完整步骤(大表上会重写,务必评估锁)
-- ALTER SEQUENCE products_id_seq AS BIGINT;
-- ALTER TABLE products ALTER COLUMN id TYPE BIGINT;

序列的并发安全性 ​

序列的递增是非事务性且原子的:多个会话并发调用 nextval() 不会拿到重复值,也不需要加锁。

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

因此序列不会成为写入瓶颈——这一点和 MySQL 的自增锁机制完全不同,PostgreSQL 的序列无需等待其他事务提交。

代价是:序列值不遵循事务可见性。会话 A 取到 5 并回滚,会话 B 仍会取到 6。

实战示例 ​

场景:为一个多租户 SaaS 的订单表设计自增主键,演示严格模式、序列校正、以及用序列实现"租户内连续单号"。

第 1 步:建表(严格模式)

sql
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)
);
text
CREATE TABLE

第 2 步:查看自动创建的序列

sql
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';
text
        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 步:正常插入

sql
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;
text
   id   | tenant_id | order_no
--------+-----------+----------
 100001 |         1 | SO-0001
 100002 |         1 | SO-0002
 100003 |         2 | SO-0001
(3 rows)

第 4 步:验证严格模式保护

sql
-- 试图手工写入 id,被拒绝
INSERT INTO orders (id, tenant_id, order_no, amount) VALUES (1, 3, 'SO-0001', 10.00);
text
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.
sql
-- 历史数据迁移需要保留原 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;
text
   id   | tenant_id
--------+-----------
 500000 |         9
(1 row)

第 5 步:迁移后校正序列(关键步骤)

sql
-- 迁移后序列仍停在 100003,不改会造成主键冲突
SELECT currval(pg_get_serial_sequence('orders', 'id')) AS before_fix;
text
 before_fix
------------
     100003
(1 row)
sql
-- 校正到当前最大值之后
SELECT setval(
  pg_get_serial_sequence('orders', 'id'),
  (SELECT max(id) FROM orders),
  true
) AS after_fix;
text
 after_fix
-----------
    500000
(1 row)
sql
-- 下一条自动插入不再冲突
INSERT INTO orders (tenant_id, order_no, amount) VALUES (9, 'SO-MIGRATE-2', 1.00)
RETURNING id;
text
   id
--------
 500001
(1 row)

第 6 步:用序列实现"租户内连续单号"

自增 id 不能保证连续,但业务常需要"每个租户从 0001 开始的单号"。用一张计数器表 + UPDATE ... RETURNING 实现(这在事务内可回滚,与序列语义相反):

sql
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;
text
 tenant_id | last_no
-----------+---------
         1 |       1
(1 row)
sql
-- 验证连续性:同一租户连续取号
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;
text
 last_no
---------
       2
(1 row)

 last_no
---------
       3
(1 row)

第 7 步:校验序列无重复

sql
SELECT
  count(*)              AS total_rows,
  count(DISTINCT id)    AS distinct_ids,
  min(id)               AS min_id,
  max(id)               AS max_id
FROM orders;
text
 total_rows | distinct_ids | min_id | max_id
------------+--------------+--------+--------
          5 |            5 | 100001 | 500001
(1 row)

第 8 步:清理

sql
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 亿上限在日志、订单、事件表上会真正触顶。
sql
-- 不推荐:列类型被固定为 integer
CREATE TABLE t_v1 (id SERIAL PRIMARY KEY);

-- 推荐:类型可控,序列强绑定
CREATE TABLE t_v2 (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY);
  • 自增 id 会有空洞,业务不能假设连续。回滚与冲突都会消耗序列值。
sql
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
text
 id
----
  2
(1 row)
  • BY DEFAULT 模式下手工插入 id 不会推进序列,必须手动 setval,否则会主键冲突。
sql
-- 错误!手工插入 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()。
sql
-- 错误!新连接直接调用会报错
-- SELECT currval('demo_seq');

-- 正确:先 nextval 再 currval,或用 last_value 直接读(有坑,见下条)
SELECT nextval('demo_seq'), currval('demo_seq');
  • 不要用 SELECT last_value FROM seq 读"下一个值"。last_value 是序列文件里的落盘值,CACHE > 1 时它停在缓存块的边界,会误导。
sql
-- 不推荐: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 的序列(若所有权绑定丢失),也不会因序列仍被引用而阻止删表。
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');
  • 不要在同一事务里依赖序列值做业务判断。序列值在事务提交前就对其他会话可见(不存在"隔离"),从序列取号不等于占位。

  • OVERRIDING SYSTEM VALUE 会绕过保护,只能用于数据迁移脚本,不能出现在日常业务代码里。

sql
-- 仅在数据迁移时使用
-- INSERT INTO orders (id, ...) OVERRIDING SYSTEM VALUE VALUES (...);
  • ALTER SEQUENCE ... AS BIGINT 不改列类型。SERIAL 列扩容必须同时改列,大表上是重写操作。
sql
-- SERIAL 项目扩容的两步(第二步在大表上代价高)
-- ALTER SEQUENCE products_id_seq AS BIGINT;
-- ALTER TABLE products ALTER COLUMN id TYPE BIGINT;
  • CACHE 越大,空洞越大。高并发短事务场景用大 CACHE(如 100~1000)能显著降低序列争用,但服务器崩溃时丢号更多。能接受空洞就用大 CACHE。

  • 不要在生成列或复杂表达式中混用 nextval()。一条 INSERT ... SELECT 里会为每一行调用一次,容易误伤。

sql
-- 危险:generate_series 里每行都会消耗一个序列值
-- INSERT INTO t (id) SELECT nextval('some_seq') FROM generate_series(1, 1000000);
  • restart_identity 慎用。TRUNCATE ... RESTART IDENTITY 会重置序列,若表被外键引用或已有数据关联,后果严重。
sql
-- 危险:级联清空并重置序列
-- 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 全局唯一;这是推荐做法,不要给每个分区单独建序列。

下一节

继续学习:精确数值(NUMERIC / DECIMAL)

参考链接 ​