业务系统中经常需要自动记录数据变化、校验字段合法性或同步派生字段,DB2触发器正是实现这类逻辑的数据库端工具。触发器不是被应用程序调用,而是当指定表上发生INSERT、UPDATE、DELETE操作时由数据库引擎自动执行,因此适合审计、数据校验和轻量级派生计算。不过编写触发器与写普通存储过程有所不同,它的执行时机、过渡变量以及禁用方式都有明确规则。本文将围绕DB2 for LUW环境,从基础语法、行级与语句级差异、临时禁用技巧和常见限制几个方面展开。

一、DB2触发器核心语法与创建示例
在DB2中创建触发器使用CREATE TRIGGER语句,触发动作分为BEFORE、AFTER和INSTEAD OF。BEFORE适合在写入前修改数据,AFTER适合记录审计日志,INSTEAD OF常用于视图上的操作重定向。一个典型的行级更新触发器会使用REFERENCING子句定义OLD和NEW别名,OLD表示修改前的行镜像,NEW表示修改后的行镜像。如果只关心某些字段变化,可以在UPDATE后面的OF子句中列出字段名,避免无关字段触发。
下面这个示例在员工表工资被更新后写入审计表。OLD和NEW在FOR EACH ROW触发器中可以直接访问,SQL PL的SET、IF、INSERT等语句包裹在BEGIN ATOMIC与END之间。
CREATE TABLE employee_audit (
emp_id INT,
old_salary DECIMAL(10,2),
new_salary DECIMAL(10,2),
change_time TIMESTAMP
);
CREATE TRIGGER trg_emp_update
AFTER UPDATE OF salary ON employee
REFERENCING OLD AS o NEW AS n
FOR EACH ROW
WHEN (n.salary <> o.salary)
BEGIN ATOMIC
INSERT INTO employee_audit(emp_id, old_salary, new_salary, change_time)
VALUES (o.emp_id, o.salary, n.salary, CURRENT TIMESTAMP);
END
如果希望在插入员工记录时自动生成主键,可以采用BEFORE INSERT触发器。这类触发器在数据写入前执行,可以在NEW字段上执行SET赋值。通过WHEN条件判断主键是否为空,避免应用已经提供主键时被覆盖。
CREATE SEQUENCE emp_seq START WITH 1 INCREMENT BY 1;
CREATE TRIGGER trg_emp_before_insert
BEFORE INSERT ON employee
REFERENCING NEW AS n
FOR EACH ROW
WHEN (n.emp_id IS NULL)
BEGIN ATOMIC
SET n.emp_id = NEXT VALUE FOR emp_seq;
END
INSTEAD OF触发器则用于视图上的写入操作。当视图本身不可直接更新时,可以通过INSTEAD OF触发器将INSERT、UPDATE或DELETE重定向到基础表,从而让应用层继续使用视图接口,而实际数据仍由后台表维护。这种用法在封装复杂查询视图时非常实用。
二、行级触发器与语句级触发器的差异及性能影响
FOR EACH ROW与FOR EACH STATEMENT决定了触发器按行还是按语句执行。行级触发器在受影响结果集的每一行上执行一次,适合需要读取或修改单行字段值的场景;语句级触发器在一次INSERT、UPDATE或DELETE操作中只执行一次,适合汇总类审计、统计变更行数等需求。
性能差异非常明显。假设一条UPDATE语句影响10万行,行级触发器主体会被执行10万次,任何内部INSERT都会累积成10万次单行写入;而语句级触发器主体只执行1次,它可以通过过渡表访问整个受影响集合,一次批量写入完成记录。因此如果只需要统计变化总行数,不应使用行级触发器。
| 比较维度 | FOR EACH ROW | FOR EACH STATEMENT |
|---|---|---|
| 执行次数 | 每行一次 | 每条触发语句一次 |
| 可访问的单行新旧值 | 可以 | 不可以,需使用过渡表 |
| 适合场景 | 审计单行变化、字段校验 | 汇总统计、批量变化记录 |
| 批量操作性能 | 通常较慢 | 通常更快 |
语句级触发器不能直接引用OLD和NEW单行别名,而要通过NEW_TABLE和OLD_TABLE过渡表访问受影响行集合。下面示例统计一次更新涉及的总行数,并写入汇总表。
CREATE TABLE emp_change_summary (
change_count INT,
summary_time TIMESTAMP
);
CREATE TRIGGER trg_emp_update_stmt
AFTER UPDATE ON employee
REFERENCING NEW_TABLE AS new_rows OLD_TABLE AS old_rows
FOR EACH STATEMENT
BEGIN ATOMIC
INSERT INTO emp_change_summary(change_count, summary_time)
SELECT COUNT(*), CURRENT TIMESTAMP FROM new_rows;
END
三、如何禁用或临时停用DB2触发器
需要先明确一点:DB2 for LUW没有MySQL那样的ALTER TRIGGER DISABLE语法,也没有现成的启用或禁用开关。若想停止触发器逻辑,只能根据实际情况选择删除、重建或设计内部开关。把这个限制提前考虑清楚,生产环境里才不会在紧急操作时手忙脚乱。
最直接的方法是DROP TRIGGER。执行前建议使用系统目录或数据库工具导出触发器DDL脚本,这样恢复时可以直接重建。对于生产环境,删除操作要在维护窗口执行,避免业务依赖触发逻辑时产生数据不一致。
-- 导出或保存DDL后,再执行删除 DROP TRIGGER trg_emp_update;
如果希望保留触发器定义,只做临时停用,可以用一个全局变量作为总开关。触发器主体内先判断开关值,只有为1时才执行核心逻辑。DB2支持CREATE VARIABLE语句,这种变量存储在数据库中,重启后依然保留。相比查询控制表,访问全局变量开销更小,适合高频表上的触发器。
CREATE VARIABLE aud_switch SMALLINT DEFAULT 1;
CREATE TRIGGER trg_emp_update_switch
AFTER UPDATE OF salary ON employee
REFERENCING OLD AS o NEW AS n
FOR EACH ROW
WHEN (n.salary <> o.salary)
BEGIN ATOMIC
IF aud_switch = 1 THEN
INSERT INTO employee_audit(emp_id, old_salary, new_salary, change_time)
VALUES (o.emp_id, o.salary, n.salary, CURRENT TIMESTAMP);
END IF;
END
另一种方案是使用控制表。当多个触发器需要统一管理时,配置表更直观,触发器查询当前开关状态。缺点是每次触发都会访问配置表,高频写入下可能产生锁竞争或额外I/O。可以在控制表上建立合适索引,并考虑使用全局变量缓存状态。根据实际需求权衡,没有唯一正确答案。
四、编写触发器时的常见限制与调试建议
DB2触发器主体内不允许执行COMMIT、ROLLBACK等事务控制语句,因为触发器运行在触发语句所在的事务中,提交或回滚会影响整个事务的原子性。同样地,不应该在触发器中调用包含事务控制的存储过程,否则会报错或破坏数据一致性。触发器主体通常应保持轻量,避免在行级触发器中执行大量复杂查询或远程访问,否则会严重拖慢主表写入。
递归触发需要特别小心。如果触发器在INSERT到表A时又向表B写入,而表B上的触发器又操作表A,可能形成循环。DB2对递归深度有内部限制,超过限制会返回SQLSTATE错误。设计时应画出触发器链路,必要时在触发器中设置应用级标记或使用表字段区分来源,避免递归。
调试触发器比调试存储过程更难,因为触发器由数据库隐式调用。可以采用以下方式:在触发器主体中加入独立的日志表,记录关键变量值和异常信息;使用SIGNAL SQLSTATE主动抛出错误并附带描述,方便定位数据问题;对复杂逻辑先在普通存储过程中完成验证,再改写为触发器。测试时一定要覆盖批量操作场景,因为行级触发器在单行测试时可能表现正常,而在批量更新时出现性能或递归问题。
DB2触发器适合审计、数据校验等场景,但编写时需要明确触发时机、行级还是语句级,以及是否真的需要触发器。临时停用要提前设计开关,而不是依赖数据库提供禁止语法。掌握过渡变量、限制和调试方法后,才能在生产环境中稳定使用。