在数据库表设计中,主键如何取值一直是绕不开的话题。手工维护编号容易出现重复和并发冲突,而DB2提供的标识列IDENTITY机制可以把这件事交给数据库内核来完成:插入数据时由系统自动生成一个递增或递减的唯一数值,应用层无需关心具体的编号逻辑。这篇文章从建表语法讲起,逐步展开标识列的生成模式、参数控制、取值方法以及运维阶段的修改与重置,最后汇总几个高频踩坑点。

标识列的本质:内嵌在表定义里的序列机制
很多人把标识列理解为一种字段类型,其实它并不是独立的类型,而是附加在数值列上的一种属性。DB2允许在SMALLINT、INTEGER、BIGINT以及DECIMAL类型的列上声明IDENTITY属性,声明之后这一列的取值就不再由INSERT语句决定,而是由DB2内部维护的计数器分配。从实现层面看,每个标识列背后都对应一个内部序列对象,计数器的当前值、步长、缓存等状态都保存在这个序列里,所以标识列的很多行为特征和独立的SEQUENCE对象是一致的。
标识列最大的价值在于并发安全。多个会话同时向同一张表插入数据时,DB2会保证每次分配的编号互不重复,应用不需要加锁,也不需要先查询最大值再计算,避免了传统先SELECT MAX再加一的写法在高并发下产生的重复主键问题。同时,标识列的值具有单调递增的倾向(除非设置为递减或CYCLE循环),这对按时间顺序排列业务数据、配合聚簇索引提升范围查询性能都有实际帮助。
需要区分的一点是,标识列不等于主键。标识列只负责生成数值,是否唯一、是否作为主键,取决于列上的约束定义。实践中通常会把标识列同时声明为主键,但两者是独立的属性。另外一张表最多只能有一个标识列,这一点在表设计阶段就要规划好。
两种生成模式:GENERATED ALWAYS与GENERATED BY DEFAULT
声明标识列时必须选择一种生成模式,这也是初学者最容易混淆的地方。GENERATED ALWAYS表示该列的值完全由系统生成,INSERT语句不允许显式指定这一列的值,如果强行插入会直接报错,除非使用OVERRIDING SYSTEM VALUE子句声明覆盖。这种模式的优点是编号来源单一,可以杜绝人工或程序误写导致的编号混乱,适合对编号权威性要求较高的业务。
-- GENERATED ALWAYS 模式:编号完全由数据库生成
CREATE TABLE t_order (
order_id BIGINT NOT NULL
GENERATED ALWAYS AS IDENTITY
(START WITH 100000, INCREMENT BY 1),
cust_no VARCHAR(20),
amount DECIMAL(12,2),
create_time TIMESTAMP DEFAULT CURRENT TIMESTAMP,
PRIMARY KEY (order_id)
);
-- GENERATED BY DEFAULT 模式:未指定值时才自动生成
CREATE TABLE t_member (
member_id INTEGER NOT NULL
GENERATED BY DEFAULT AS IDENTITY
(START WITH 1, INCREMENT BY 1, NO CACHE),
member_name VARCHAR(50),
PRIMARY KEY (member_id)
);
GENERATED BY DEFAULT则相对宽松:如果INSERT语句没有给这一列赋值,系统自动生成;如果显式给了值,就使用给定的值。这种模式在数据迁移、批量导入场景下很实用,比如把旧系统的数据连同原始编号一起搬进新表。但它也埋下了隐患:一旦手工插入了一个比当前计数器更大的值,后续自动生成的编号不会感知这一点,继续按自己的节奏递增,直到撞上已存在的值触发唯一约束冲突。因此使用这种模式时,导入数据后应当及时用ALTER语句把计数器重置到安全位置,具体操作在后文展开。
选择建议很直接:纯新增型业务表优先用GENERATED ALWAYS,把编号的掌控权完全交给数据库;存在历史数据回填、多系统合并需求的表用GENERATED BY DEFAULT,但必须配套重置计数器的运维动作,否则迟早会遇到重复键错误。
核心参数详解:精确控制编号的起点、步长与缓存
IDENTITY子句支持一组参数,建表时没有显式指定的话都会取默认值。START WITH定义计数器起始值,比如订单号从100000开始,避免业务单号看起来太短;INCREMENT BY定义步长,正数递增、负数递减,某些分库分表架构会利用不同节点设置不同步长来错开编号区间。MINVALUE和MAXVALUE限定计数器边界,默认情况下最小值就是起始值,最大值取列类型的极限值。
-- 完整参数示例
CREATE TABLE t_invoice (
invoice_no BIGINT NOT NULL
GENERATED ALWAYS AS IDENTITY
(START WITH 5000
INCREMENT BY 10
MINVALUE 5000
MAXVALUE 9999990
NO CYCLE
CACHE 20),
cust_no VARCHAR(20),
invoice_amt DECIMAL(12,2),
PRIMARY KEY (invoice_no)
);
CACHE是影响编号连续性的关键参数。默认CACHE 20意味着DB2每次会把20个编号预先缓存在内存里,插入时直接从内存取值,避免每次都读写系统表,插入性能明显更好。代价是一旦数据库异常重启,缓存中尚未用掉的编号会整体丢弃,重启后从下一批编号开始分配,中间就出现一段空洞。NO CACHE则保证编号尽量连续,但事务回滚造成的空洞仍无法避免,而且插入性能会有折损。CYCLE与NO CYCLE决定计数器到达MAXVALUE之后是否回绕到MINVALUE重新开始,业务主键一般必须用NO CYCLE,否则回绕后会撞上历史数据。
还要注意ORDER参数。在并行插入的场景下,标识列默认不保证严格按插入时间先后发放编号,如果业务依赖编号的时间有序性,需要额外声明ORDER,但这同样会牺牲并发吞吐。绝大多数业务只要求编号唯一,不要求严格有序,保持默认即可。
如何拿到刚生成的编号值
插入数据后,应用经常需要知道数据库这次分配的编号是多少,比如插入订单主表后要把订单号写进明细表。DB2提供的标准函数是IDENTITY_VAL_LOCAL,它返回当前事务内最后一次单行INSERT所生成的标识值。这个函数的返回值在事务提交或回滚后会被清空,所以必须在同一个事务内、紧跟着插入语句去查询。
-- 插入一条会员记录
INSERT INTO t_member (member_name) VALUES ('张三');
-- 取回刚才生成的编号
SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1;
-- 典型用法:插入后立刻取值绑定到程序变量
INSERT INTO t_member (member_name) VALUES ('李四');
VALUES IDENTITY_VAL_LOCAL();
使用这个函数有几个限制必须清楚。第一,它只记录单行INSERT的结果,如果一条语句插入了多行,比如INSERT加SELECT的形式,函数返回的是最后一个值,前面的值无法通过它获取,这种场景应改用FINAL TABLE的写法。第二,如果插入时显式指定了标识列的值(GENERATED BY DEFAULT模式下),函数返回的是NULL而不是那个手工值。第三,不要用SELECT MAX取编号来替代这个函数,并发环境下两条会话可能拿到同一个最大值,反而制造主键冲突。
-- 多行插入时用 FINAL TABLE 直接取回所有生成的编号
SELECT order_id FROM FINAL TABLE (
INSERT INTO t_order (cust_no, amount)
SELECT cust_no, amount FROM t_order_stage
);
运维阶段如何修改与重置标识列
表建好之后,标识列的行为仍然可以调整。ALTER TABLE加ALTER COLUMN的组合支持RESTART WITH重置计数器到指定值、RESTART回到起始值、SET INCREMENT BY修改步长、SET CACHE调整缓存大小、SET GENERATED切换生成模式等操作。最常用的场景有两个:一是数据迁移完成后把计数器拨到最大编号之后,避免自动生成的值与导入数据冲突;二是测试环境清理数据后让编号从头开始。
-- 重置计数器到指定值(常用于数据导入后避开已占用的编号) ALTER TABLE t_member ALTER COLUMN member_id RESTART WITH 50001; -- 重置到建表时的 START WITH 或 MINVALUE ALTER TABLE t_member ALTER COLUMN member_id RESTART; -- 调整缓存大小 ALTER TABLE t_order ALTER COLUMN order_id SET CACHE 50; -- 修改步长 ALTER TABLE t_order ALTER COLUMN order_id SET INCREMENT BY 5; -- 切换生成模式 ALTER TABLE t_member ALTER COLUMN member_id SET GENERATED BY DEFAULT;
查看标识列的当前配置可以查询系统编目表SYSCAT.COLUMNS,其中identity字段标识该列是否为标识列,start、increment、cache等字段直接对应各个参数。排查编号跳号、确认缓存设置时,这张编目表是第一入口。另外要提醒一点,RESTART WITH设置的值如果小于表中已有数据的最大编号,后续插入会触发唯一约束冲突,重置前务必先确认数据现状,生产环境操作前最好锁定写入窗口。
-- 查询标识列配置信息
SELECT tabname, colname, identity, generated,
start, increment, minvalue, maxvalue,
cache, order, cycle
FROM SYSCAT.COLUMNS
WHERE identity = 'Y' AND tabname = 'T_ORDER';
常见问题与避坑指南
编号出现跳号是最常见的疑问。原因主要有三类:CACHE缓存因重启丢弃、事务回滚不返还编号、删除数据不回收编号。这三类都属于正常机制而非故障,标识列从设计上只承诺唯一性,不承诺无空洞。如果业务要求单号绝对连续,比如财务发票号,标识列并不合适,应当改用独立的编号表加锁生成,或者事后由业务程序补齐。
GENERATED ALWAYS模式下想手工指定编号,比如修复一条历史数据,需要OVERRIDING SYSTEM VALUE子句,否则会收到SQL0798N错误。而GENERATED BY DEFAULT模式下导入数据后忘记RESTART计数器,是导致SQL0803N重复键错误的典型原因,遇到这类错误时先检查计数器位置,再决定是否重置。
-- GENERATED ALWAYS 模式下强制写入指定编号 INSERT INTO t_order (order_id, cust_no, amount) OVERRIDING SYSTEM VALUE VALUES (100001, 'C001', 299.00);
最后对比一下标识列和独立SEQUENCE的取舍。标识列绑定在单表上,声明即用,适合一张表一个自增主键的经典结构;SEQUENCE是独立对象,可以被多张表共享、一个事务里可以取多个值、支持NEXT VALUE FOR灵活嵌入查询语句,适合多表统一编号、按需预取批量的场景。两者底层机制相通,理解了标识列的参数与行为,再去用SEQUENCE几乎不需要额外学习成本。实际项目中按编号的作用范围来选:只服务本表就用标识列,跨表共享就上SEQUENCE,不必为了形式统一而强行只用一种。