跳到主要内容

标识列

标识列(identity column)是由数据库自动生成值的列,最常用于数字主键。应用只需插入业务数据,数据库负责在并发环境中分配编号。

标识列解决的是“如何生成值”,不等于主键,也不保证编号连续。若一列必须唯一且非空,仍应为它定义 PRIMARY KEY 或相应约束。

本文讲解 SQL 标准风格的标识列,示例以 PostgreSQL 为主。MySQL 的 AUTO_INCREMENT、SQL Server 的 IDENTITY 和 SQLite 的整数主键具有不同语法与边界。

创建标识列​

CREATE TABLE users (
user_id BIGINT GENERATED ALWAYS AS IDENTITY,
username TEXT NOT NULL,
CONSTRAINT pk_users PRIMARY KEY (user_id),
CONSTRAINT uq_users_username UNIQUE (username)
);

普通插入不需要提供 user_id:

INSERT INTO users (username)
VALUES ('alice')
RETURNING user_id;

数据库为新行生成编号,RETURNING 可以直接返回结果,避免通过 MAX(user_id) 猜测刚插入的值。

ALWAYS 与 BY DEFAULT​

标识列有两种生成策略。

GENERATED ALWAYS​

user_id BIGINT GENERATED ALWAYS AS IDENTITY

数据库要求普通写入使用生成值。显式提供编号通常会失败:

INSERT INTO users (user_id, username)
VALUES (100, 'bob');

数据迁移确实需要保留旧编号时,PostgreSQL 可明确覆盖系统值:

INSERT INTO users (user_id, username)
OVERRIDING SYSTEM VALUE
VALUES (100, 'bob');

ALWAYS 能更强地防止应用误写技术主键,适合绝大多数日常业务表。

GENERATED BY DEFAULT​

user_id BIGINT GENERATED BY DEFAULT AS IDENTITY

省略该列时由数据库生成,显式提供值时则采用调用方的值:

INSERT INTO imported_users (user_id, username)
VALUES (9001, 'legacy-user');

这种方式便于导入历史数据,但普通业务代码也更容易意外覆盖生成规则。除非存在明确需求,否则优先考虑 ALWAYS。

PostgreSQL 还支持 OVERRIDING USER VALUE,忽略输入中的显式标识值并强制重新生成,适合把数据复制到需要新编号的表。

自定义起点和步长​

可以在定义中传递序列参数:

CREATE TABLE tickets (
ticket_id BIGINT GENERATED ALWAYS AS IDENTITY
(START WITH 1000 INCREMENT BY 1),
subject TEXT NOT NULL,
CONSTRAINT pk_tickets PRIMARY KEY (ticket_id)
);

常见选项还包括最小值、最大值、循环和缓存。它们属于容量与性能配置,不应被用来编码易变的业务含义。

编号达到类型上限后,插入会失败;使用 BIGINT 可以显著扩大范围,但仍应监控增长速度。开启 CYCLE 会重新使用旧范围,若列受唯一约束保护,通常很快就会产生冲突,因此业务主键很少使用它。

标识列背后的序列​

在 PostgreSQL 中,标识列由隐式序列提供值。序列具有几个重要特征:

  • 多个会话可以安全地并发取值;
  • 取出的值通常不会因事务回滚而归还;
  • 缓存、失败插入和数据库重启都可能造成空号;
  • 序列当前值与表中的最大值不是同一个概念。

因此下面的假设都不可靠:

  • 相邻两行的编号一定相差 1;
  • 编号较大就一定更晚提交;
  • 删除最后一行后,下次会复用其编号;
  • 统计编号差值就能得到行数。

需要表达时间顺序时使用时间列,需要统计数量时使用 COUNT,不要从标识值反推。

导入后同步生成器​

显式导入较大的历史编号后,底层序列可能仍停留在旧位置,后续自动生成值就可能与已有行冲突。迁移流程应在导入完成后校准生成器。

PostgreSQL 可以使用 setval,但要先准确找到标识列关联的序列,并理解第三个参数对“下一次返回当前值还是后继值”的影响。生产迁移应封装为经过测试的脚本,不要仅凭表的最大值手工操作。

修改标识列​

可以给已有列添加标识属性:

ALTER TABLE users
ALTER COLUMN user_id ADD GENERATED ALWAYS AS IDENTITY;

前提是列类型与数据库要求兼容,已有数据也不会被自动改写或自动获得唯一约束。

切换生成策略:

ALTER TABLE users
ALTER COLUMN user_id SET GENERATED BY DEFAULT;

重启编号:

ALTER TABLE users
ALTER COLUMN user_id RESTART WITH 10000;

删除标识属性:

ALTER TABLE users
ALTER COLUMN user_id DROP IDENTITY;

这些操作可能影响依赖对象、权限和后续写入。重启编号前必须检查现有值,否则主键或唯一约束会阻止重复值,缺少约束时则可能悄悄产生重复标识。

标识列与主键​

下面两项职责不同:

机制职责
标识列在省略输入时生成一个列值
主键约束保证值非空且唯一,并标识一行

推荐把二者明确组合:

CREATE TABLE products (
product_id BIGINT GENERATED ALWAYS AS IDENTITY,
sku TEXT NOT NULL,
product_name TEXT NOT NULL,
CONSTRAINT pk_products PRIMARY KEY (product_id),
CONSTRAINT uq_products_sku UNIQUE (sku)
);

product_id 是稳定的技术标识,sku 是业务唯一键。业务编号可能改变格式或被人工分配,不宜因为“看起来唯一”就承担所有内部引用职责。

标识列与 SERIAL​

PostgreSQL 的 SERIAL、BIGSERIAL 是历史上常见的便捷写法,本质上会创建整数列、序列和默认值。标识列更接近 SQL 标准,并能用 ALWAYS / BY DEFAULT 明确是否允许覆盖。

维护旧表时无需仅为追求新语法而仓促迁移;创建新表时通常优先选择标识列。迁移 SERIAL 时要检查序列所有权、默认值、权限和依赖,而不是只替换列定义文本。

标识列与 UUID​

标识列不是唯一的主键方案。UUID 适合离线生成、跨系统合并或不希望暴露递增规模的场景;递增整数通常更紧凑,B-tree 索引局部性也更好。

选择时应考虑:

  • 是否需要在写入数据库前生成 ID;
  • 是否跨多个独立系统分配;
  • 索引大小和写入局部性;
  • URL 等外部接口是否允许暴露递增编号;
  • 调试、日志和人工沟通的便利性。

无论选择哪一种,都应由唯一约束或主键约束保证完整性。

常见误区​

标识列天然唯一​

不一定。生成器通常避免自己产生重复值,但显式覆盖、重启或错误校准都可能冲突。用 PRIMARY KEY 或 UNIQUE 做最终保证。

编号可以作为连续票据号​

不适合。标识列允许出现空号。要求严格连续的编号需要专门的事务设计,而且通常会牺牲并发性能。

通过 MAX(id) + 1 生成编号​

并发会话可能计算出相同结果。应让标识列或序列完成原子分配。

插入后查询最大 ID​

并发写入时可能读到别人的记录。使用 RETURNING 或数据库驱动的生成键接口。

设计检查清单​

  1. 标识列是否同时受到主键或唯一约束保护;
  2. 普通写入是否应被允许覆盖生成值;
  3. 类型容量是否足够覆盖预期生命周期;
  4. 是否错误依赖了连续编号或提交顺序;
  5. 历史数据导入后是否校准了生成器;
  6. 技术主键与业务唯一键是否分别建模;
  7. 应用是否通过可靠接口取得生成值。

标识列最适合承担“稳定且无业务含义的内部行标识”。把唯一性、业务规则和顺序语义交给各自对应的约束与列,表结构会更清晰。