告别触发器!Oracle 12c 身份列自增实战与大表添加主键的“避坑指南”
在日常的数据库开发和运维中,实现主键自增是一个极其普遍的需求。如果你是从 MySQL 转投 Oracle 的开发者,一定会对 Oracle 早期版本中缺乏原生的自增主键支持感到不适。过去,我们不得不依赖“序列+ 触发器”的组合拳来实现,这不仅增加了代码的复杂度,还带来了额外的性能开销。
然而,从 Oracle 12c 开始,官方引入了 ANSI SQL 标准的 IDENTITY 列特性,彻底改变了这一局面
。今天,我们就来深入实战 Oracle 的身份列特性,并重点探讨一个在生产环境中极易踩坑的运维场景:如何为已有大数据量的表安全地添加自增主键?
一、 重新定义自增:Oracle 12c IDENTITY 列详解
Oracle 12c 引入的 IDENTITY 列,本质上是通过内部隐式创建一个序列生成器来实现的,它将序列与表列深度绑定,极大地简化了开发流程
。
根据业务需求的不同,IDENTITY 列提供了三种生成模式,它们在灵活性与严格性上有着显著的差异
:
1. GENERATED ALWAYS AS IDENTITY:最严格的“守护者”
使用 ALWAYS 关键字时,该列的值完全由数据库内部序列控制。用户在任何情况下都不能在 INSERT 语句中为该列显式指定值,也无法通过 UPDATE 语句修改该列的值,甚至插入 NULL 值也会直接报错(ORA-32795)
。
- 适用场景:适用于对数据一致性要求极高、绝对不允许人工干预或覆盖自增ID的业务表。
2. GENERATED BY DEFAULT AS IDENTITY:灵活的“妥协者”
使用 BY DEFAULT 时,默认情况下系统会使用序列生成器赋值,但允许用户在 INSERT 时显式指定一个具体的值。不过,如果你尝试插入 NULL 值,将会违反 NOT NULL 约束而报错(ORA-01400)
。同时,该列允许被 UPDATE 修改(但不能改为 NULL)。
- 适用场景:适用于数据迁移、历史数据合并等需要保留旧表原ID,同时新数据自增的场景。
3. GENERATED BY DEFAULT ON NULL AS IDENTITY:终极的“兜底王”
这是 BY DEFAULT 模式的增强版。它的行为与 BY DEFAULT 几乎一致,唯一的区别在于:当用户显式插入 NULL 值时,系统不会报错,而是会自动拦截并使用序列生成器为其分配一个有效值
。
- 适用场景:提供了最大的灵活性,尤其适合应用层代码不严谨、可能偶尔传入 NULL 的场景,确保插入操作不会因为主键为空而失败。
二、 性能优化必修课:不可忽视的序列缓存
无论是哪种模式,IDENTITY 列的底层都是序列。在创建时,我们可以通过 identity_options 来配置底层序列的参数,其中最影响高并发性能的就是 CACHE 设置
。
默认情况下,如果不指定缓存大小,Oracle 每次获取序列的 NEXTVAL 都可能产生对数据字典的物理 I/O 和锁争用。在高并发插入场景下,这会引发严重的 row cache lock 等待。因此,在创建 IDENTITY 列时,务必根据并发量设置合理的缓存大小(如 CACHE 1000),以大幅提升性能
。
sql
-- 推荐的建表语句示例:指定缓存大小
CREATE TABLE orders (
order_id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY (CACHE 1000),
customer_name VARCHAR2(100)
);
三、 生产级避坑:大表添加自增主键的“暗黑陷阱”
了解了 IDENTITY 的便捷后,我们来看一个让无数 DBA 深夜惊魂的运维场景:为一张已有数千万数据的生产大表,补加一个自增主键列。
很多开发者想当然地认为,一条 DDL 就能搞定:
sql
ALTER TABLE big_table ADD id NUMBER GENERATED BY DEFAULT AS IDENTITY INCREMENT BY 1 START WITH 1 CACHE 100 NOT NULL;
千万别直接在生产环境执行这条语句! 这是一个极其危险的操作,原因如下:
- 这不是元数据操作,而是全表数据更新:由于该语句不仅新增了列,还要求列
NOT NULL且由 Identity 自动生成值,Oracle 必须为表中的每一行现有记录逐行生成并写入一个唯一的序列值。数据量越大,修改的数据块越多,执行时间呈线性增长。 - 海量的 Undo 与 Redo 风暴:逐行更新海量数据会产生庞大的重做日志和撤销记录,极易导致 Undo 表空间爆满或归档日志快速填满磁盘,甚至撑挂整个数据库。
- 长时间锁表阻断业务:在执行此 DDL 期间,Oracle 会对表加排他锁,阻塞所有业务对该表的 DML 操作。对于千万级大表,这可能导致业务长时间中断。
正确的“分步走”破局方案
对于非空的大数据量表,必须采用分步操作,将大事务拆解,以最小化对生产环境的影响:
第一步:极速添加可为空的普通列(仅修改数据字典,瞬间完成)
sql
ALTER TABLE big_table ADD (id NUMBER NULL);
第二步:在业务低峰期,分批更新历史数据
使用 PL/SQL 脚本,按 Rowid 分批提交,避免长事务和 Undo 暴涨,为现有行填充 ID 值。
第三步:修改列为 NOT NULL 并添加主键约束
待所有历史数据均有值后,再添加约束。
sql
ALTER TABLE big_table MODIFY id NOT NULL;
ALTER TABLE big_table ADD CONSTRAINT pk_big_table PRIMARY KEY (id);
第四步:将列修改为 IDENTITY 属性(使用 START WITH LIMIT VALUE)
为了让后续新插入的数据自动生成 ID,我们将列属性变更为 IDENTITY。这里有一个关键参数:START WITH LIMIT VALUE。它的作用是让 Oracle 自动查找表中现有 ID 的最大值,并将序列的起始值设定为该最大值之后,从而完美避免主键冲突
。
sql
ALTER TABLE big_table MODIFY id GENERATED BY DEFAULT AS IDENTITY (START WITH LIMIT VALUE CACHE 1000);
四、 结语
Oracle 12c 的 IDENTITY 列无疑是开发者的福音,它让我们彻底告别了繁琐的触发器,代码更简洁,维护更方便
。但在享受便利的同时,我们更需敬畏数据库的底层逻辑——DDL 并非总是轻量级操作,尤其是在面对海量数据时。
理解 ALWAYS 与 BY DEFAULT 的差异,合理配置 CACHE 参数,掌握大表变更的分步拆解艺术,才是一个成熟数据库从业者应有的素养。希望这篇避坑指南,能在你的下一个生产变更中,为你保驾护航。
更多推荐




所有评论(0)