标识列
标识列(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 或数据库驱动的生成键接口。
设计检查清单
- 标识列是否同时受到主键或唯一约束保护;
- 普通写入是否应被允许覆盖生成值;
- 类型容量是否足够覆盖预期生命周期;
- 是否错误依赖了连续编号或提交顺序;
- 历史数据导入后是否校准了生成器;
- 技术主键与业务唯一键是否分别建模;
- 应用是否通过可靠接口取得生成值。
标识列最适合承担“稳定且无业务含义的内部行标识”。把唯一性、业务规则和顺序语义交给各自对应的约束与列,表结构会更清晰。