
简介一份面向Oracle数据库开发与运维人员的PL/SQL触发器编程专题文档系统讲解触发器如何弥补数据库完整性约束的不足实现复杂业务规则、操作监控与审计功能。内容详细覆盖DML触发器、INSTEAD OF触发器、系统触发器的适用场景系统梳理触发事件、WHEN条件、触发对象、BEFORE与AFTER时机、行级与语句级等核心概念并通过CREATE TRIGGER创建、自动执行、DROP TRIGGER删除的完整流程演示配合教师表防重名、操作日志记录等示例代码方便读者直接对照练习。资源为单个PDF文件整包约39KB轻量易读。目前已有262人学习下载。对于准备Oracle认证、从事数据库开发或需要完善数据审计机制的开发者这份材料能提供清晰的知识框架与可直接套用的SQL模板既可作为入门导读也能作为日常参考手册有助于快速上手触发器编程。1. 触发器不是存储过程的附属品为什么 ORACLE PL/SQL 触发器编程要单独讲白天一切正常凌晨批量更新一跑业务表被锁了半小时。这种事故我在数据库故障里见过不少最后揪出来的原因往往不是存储过程而是表上一个不起眼的触发器。ORACLE PL/SQL 触发器编程本质上是学一种由数据库自动调度、不需要应用层显式调用的代码执行机制。它解决的问题非常具体当数据发生变更时你希望自动做点什么——写审计日志、维护冗余字段、拦截脏数据——但又不想在每条业务代码里手动调用一段存储过程。这篇文章适合刚接手 Oracle 数据库开发的工程师也适合从 MySQL 转过来、想重新理解触发器这个老概念的开发者。2. 触发器的执行机制先搞懂执行次数和触发点再写代码写触发器最大的误区是一上来就 CREATE TRIGGER结果行为和自己想的不一样。触发器不是存储过程的简化版它有自己独立的执行语义。你至少要搞清楚两个问题一条 SQL 进来它下面挂的触发器会被执行几次触发器执行的时候数据到底处于什么状态。2.1 语句级与行级同样一条 UPDATE触发次数差了一个数量级Oracle 触发器按粒度分成语句级和行级两类。不带 FOR EACH ROW 的是语句级触发器一条 DML 语句不管影响多少行它只执行一次带 FOR EACH ROW 的是行级触发器DML 语句影响的每一行都会执行一次。这个差别在批量场景下是数量级的差距直接决定性能表现。你可以用一段很短的脚本在本地验证这个机制-- 准备一张三行的测试表 CREATE TABLE t_trigger_demo ( id NUMBER PRIMARY KEY, val NUMBER ); INSERT INTO t_trigger_demo VALUES (1, 100); INSERT INTO t_trigger_demo VALUES (2, 200); INSERT INTO t_trigger_demo VALUES (3, 300); -- 语句级触发器一条 SQL 只触发一次 CREATE TABLE stmt_log (cnt NUMBER); CREATE OR REPLACE TRIGGER trg_stmt_demo AFTER UPDATE ON t_trigger_demo BEGIN INSERT INTO stmt_log VALUES (1); END; / -- 行级触发器每一行触发一次 CREATE TABLE row_log (cnt NUMBER); CREATE OR REPLACE TRIGGER trg_row_demo AFTER UPDATE ON t_trigger_demo FOR EACH ROW BEGIN INSERT INTO row_log VALUES (1); END; / -- 一条 UPDATE 影响三行 UPDATE t_trigger_demo SET val val 1; -- 对比两种触发器的执行次数 SELECT stmt AS trigger_type, COUNT(*) AS fire_cnt FROM stmt_log UNION ALL SELECT row, COUNT(*) FROM row_log;这段代码里你在一张表上并排放了两个触发器它们互不干扰。执行 UPDATE 后stmt_log 里只会有 1 条记录而 row_log 里会有 3 条。这直接证明了语句级和行级触发器的本质区别。这里的关键参数是 FOR EACH ROW。带上它触发器按行执行不带按语句执行。还有一个容易忽略的细节如果一条 UPDATE 语句实际匹配了 0 行语句级触发器不执行行级触发器当然也不会执行但如果一个触发器是行级的语句执行过程中每碰一行都会触发一次无论这个行最终是否真的被修改了值。2.2 BEFORE 与 AFTER数据落盘前你能改落盘后你只能看触发时点决定了你在这个触发器里能对数据做什么操作。BEFORE 触发器在数据写入磁盘之前执行你可以通过 :NEW.字段 直接修改将要落库的值这个修改会真正生效。AFTER 触发器在行写入完成之后执行此时数据已经定了你再改 :NEW 已经没有意义它不会影响最终落盘的结果。看一个最常见的 BEFORE 用法给字段兜底赋值CREATE OR REPLACE TRIGGER trg_before_insert_default BEFORE INSERT ON t_trigger_demo FOR EACH ROW BEGIN -- 应用层漏传时这里兜底加 1 :NEW.val : COALESCE(:NEW.val, 0) 1; END; /这段触发器在每次 INSERT 前执行。COALESCE 处理空值如果业务传进来的 val 是 NULL它会先取 0 再 1如果业务传了 200就变成 201。因为这是 BEFORE INSERT所以 :NEW.val 的赋值会直接写在最终插入的行上。AFTER 触发器的典型场景是审计。行已经写完逻辑上不可能再回退到修改前所以你要把 :OLD 和 :NEW 的对比值记下来作为变更前后的快照。记住一个选型原则要改数据用 BEFORE只要记录不改数据用 AFTER。审计逻辑放在 AFTER 里能避免你顺手把业务数据也改掉让触发器职责更干净。2.3 触发事件组合与 WHEN 过滤用最少代码控制触发范围Oracle 允许一个触发器同时监听多个事件写法是 INSERT OR UPDATE OR DELETE也可以单独监听 UPDATE OF 某列。配合 WHEN 子句可以在行级触发时再做一层条件过滤只有满足条件的行才执行触发器体。看这段限制负薪资的示例CREATE OR REPLACE TRIGGER trg_guard_salary BEFORE INSERT OR UPDATE OF salary ON emp FOR EACH ROW WHEN (NEW.salary 0) BEGIN RAISE_APPLICATION_ERROR(-20001, 薪资不能为负数); END; /WHEN 子句在触发体执行之前做判断条件不满足时整个触发器体被跳过。注意这里写的是 NEW.salary不需要加冒号WHEN 子句里的语法和使用 :NEW 的 PL/SQL 块不同。而且 WHEN 里只能引用 :NEW 和 :OLD 的字段不能调用函数、不能写子查询这是 Oracle 的限制。这段代码里 BEFORE INSERT OR UPDATE OF salary 表示插入时不管 salary 是什么都做检查更新时只有 salary 出现在 SET 列表里才做检查。这里的出现在 SET 列表很关键SET salary salary 即使值没变触发器一样会执行。我见过不少人在这个细节上翻车后面避坑章节会再展开。3. 用 PL/SQL 在 Oracle 里跑通第一个触发器建表、编译、验证的完整过程理解了机制就可以动手写一个真正能落地的触发器了。这一章用一个审计示例带你走完整流程建业务表、建日志表、创建触发器、执行 DML、查询验证。整个过程在任何安装好的 Oracle 数据库上都能复现。3.1 最小落地建日志表、建触发器、执行验证最常见的触发器入门需求是审计谁在什么时候把某张表的哪几个字段改成了什么值。下面这套脚本是一个完整的落地案例。-- 1. 业务表员工表 CREATE TABLE emp ( emp_id NUMBER PRIMARY KEY, emp_name VARCHAR2(50), salary NUMBER ); -- 2. 审计日志表 CREATE TABLE emp_audit_log ( log_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, emp_id NUMBER, old_salary NUMBER, new_salary NUMBER, op_type VARCHAR2(10), change_by VARCHAR2(60), change_at TIMESTAMP DEFAULT SYSTIMESTAMP ); -- 3. 审计触发器薪资变更或删除时记录日志 CREATE OR REPLACE TRIGGER trg_emp_audit AFTER UPDATE OF salary OR DELETE ON emp FOR EACH ROW BEGIN IF UPDATING THEN INSERT INTO emp_audit_log (emp_id, old_salary, new_salary, op_type, change_by) VALUES (:OLD.emp_id, :OLD.salary, :NEW.salary, UPDATE, SYS_CONTEXT(USERENV, SESSION_USER)); ELSIF DELETING THEN INSERT INTO emp_audit_log (emp_id, old_salary, new_salary, op_type, change_by) VALUES (:OLD.emp_id, :OLD.salary, NULL, DELETE, SYS_CONTEXT(USERENV, SESSION_USER)); END IF; END; / -- 4. 执行一条 UPDATE UPDATE emp SET salary salary * 1.1 WHERE emp_id 1; -- 5. 查看审计结果 SELECT * FROM emp_audit_log;这段代码的几条逻辑说明UPDATING 和 DELETING 是 Oracle 提供的条件谓词用来判断当前触发的是哪个事件。因为触发器同时监听了 UPDATE 和 DELETE所以在触发体里必须靠它们分流。日志表里记录 :OLD.emp_id 和 :OLD.salary取的是变更前的值这是审计的关键——如果都用 :NEW你就查不到原来是多少。SYS_CONTEXT(USERENV, SESSION_USER) 返回当前会话用户比直接拼 USER 变量更规范也方便以后接 LDAP 等外部认证时读取真实登录身份。参数说明log_id 列用了 GENERATED ALWAYS AS IDENTITY这是 Oracle 12c 以后的写法自动生成序列值。如果你的库还是 11g需要改成 SEQUENCE 默认值或者触发器生成主键。AFTER UPDATE OF salary 表示只有 salary 列被更新时才触发审计如果 UPDATE 只改了 emp_name这个触发器不会被触发。执行完第 4 步如果 emp 表里连一行 emp_id1 的数据都没有日志表自然为空这不是触发器没生效而是 UPDATE 匹配 0 行。验证触发器是否真的编译成功先看第 5 步的查询结果同时可以查 USER_TRIGGERS 确认状态。3.2 触发器管理查询状态、禁用、启用、删除触发器不是建完就一劳永逸。做数据修复、批量导数据时触发器反而会成为累赘这时需要临时禁用。Oracle 的管理命令很直接全在 DDL 层面。-- 查看当前用户下所有触发器及状态 SELECT trigger_name, table_name, status, trigger_type, triggering_event FROM user_triggers ORDER BY table_name, trigger_name; -- 禁用与启用 ALTER TRIGGER trg_emp_audit DISABLE; ALTER TRIGGER trg_emp_audit ENABLE; -- 删除触发器 DROP TRIGGER trg_emp_audit; -- 查看触发器源码排查历史改动 SELECT line, text FROM user_source WHERE name TRG_EMP_AUDIT ORDER BY line;USER_TRIGGERS 视图里的 STATUS 字段是判断触发器有没有生效的第一入口。很多生产事故的根源就是某次维护窗口里有人执行了 ALTER TRIGGER ... DISABLE做完批量更新忘了 ENABLE之后业务逻辑悄悄变了。所以我每次接手一个库第一件事就是按表名扫一眼这张视图把长期 DISABLED 的触发器全部找出来问一遍。USER_SOURCE 里存的是触发器源码文本按行号排列。这里的 NAME 是触发器的名字注意它是大写查询时要写 TRG_EMP_AUDIT 而不是小写。3.3 在 PL/SQL Developer 和 SQL*Plus 里执行注意编译结束符和错误定位同一个创建触发器的脚本在 SQLPlus 和 PL/SQL Developer 两个环境里的执行习惯不一样。SQLPlus 里CREATE OR REPLACE TRIGGER 是一个 PL/SQL 块结尾必须单独写一行斜杠 /斜杠表示把缓冲区里的内容提交给数据库编译。漏写斜杠是新手在命令行环境里最常见的编译失败原因命令看着输入完了但什么都没发生。在 PL/SQL Developer 里你大可以不写末尾的斜杠直接选中整个脚本按 F8 执行。编译结果会在 Messages 面板里显示常见的提示是 Trigger created with compilation errors。这时候别急着改先点开 View 菜单下的 Compile 信息或者直接执行SELECT name, type, line, position, text FROM user_errors WHERE name TRG_EMP_AUDIT ORDER BY line;USER_ERRORS 会告诉你错误在哪一行哪一列错误码是什么省去逐行猜的时间。这是调试触发器最基本的手段比看 Messages 面板里的提示更准确。4. 触发器能落地的三类业务场景审计、默认值与汇总维护搞清楚怎么建触发器下一步是回答我的项目里到底该不该用它。触发器不是万能的它适合三种场景审计追踪、默认值填充与字段联动、汇总数据维护。每种都有明确的收益和代价。4.1 审计追踪把变更前后值和操作人落库审计是触发器最经典的应用场景没有之一。应用层的操作日志可以被用户篡改、可以被日志框架截断但数据库触发器记录的审计信息独立于应用代码只要数据库账号权限不被滥用这些记录基本可信。前面 3.1 节的例子已经覆盖了基本审计这里再加一层常见优化只在值真正变化时记录。CREATE OR REPLACE TRIGGER trg_emp_audit_salary_change AFTER UPDATE OF salary ON emp FOR EACH ROW WHEN (OLD.salary NEW.salary) BEGIN INSERT INTO emp_audit_log (emp_id, old_salary, new_salary, op_type, change_by) VALUES (:OLD.emp_id, :OLD.salary, :NEW.salary, SALARY_CHANGE, SYS_CONTEXT(USERENV, SESSION_USER)); END; /WHEN (OLD.salary NEW.salary) 把值没有变化的更新过滤掉。比如应用层一次性把整行字段都 SET 了一遍即使 salary 还是原值没有这个条件也会产生一条无意义的审计记录。加了之后日志量能显著下降审计表查询也更快。审计触发器的坑在于它让每次 DML 都多一次 INSERT写放大明显。如果一张热表的更新频率很高审计日志表会膨胀得很快。我的处理习惯是给审计日志表做按月分区并且只审计真正关心的字段而不是把整行的旧值新值全记下来。4.2 默认值填充与字段联动把应用层重复代码下沉到数据库很多表的字段有默认值逻辑比如创建时间、创建人、编号。如果在应用层写每个入口都要重复一遍漏一个口子数据就脏了。用 BEFORE INSERT 触发器统一兜底是数据一致性最高、重复代码最少的方式。CREATE OR REPLACE TRIGGER trg_emp_bi_defaults BEFORE INSERT ON emp FOR EACH ROW BEGIN -- 应用层没传员工编号时从序列取值 IF :NEW.emp_id IS NULL THEN SELECT seq_emp_id.NEXTVAL INTO :NEW.emp_id FROM dual; END IF; -- 员工姓名统一去首尾空格避免前端不做 trim :NEW.emp_name : TRIM(:NEW.emp_name); END; /这段代码的执行逻辑很清晰依次检查 :NEW 里的字段为空则补默认值非空则做规范化。注意这里用的是 IF 而不是无条件赋值因为如果应用层已经传了业务编号触发器不应该覆盖它。字段联动也是同样的思路。比如订单表插入时把订单金额带回到客户表或者更新商品库存时同步更新销售数量。放在触发器里应用层完全无感知逻辑不会因为某个接口忘记调而失效。但这种联动的代价在后面会提到它把性能损耗藏在了每一笔 DML 后面。4.3 汇总数据维护触发器的实时性与维护成本第三种常被提起的场景是维护汇总数据。订单插入后实时更新客户累计消费金额避免查询时临时做 SUM 聚合。CREATE OR REPLACE TRIGGER trg_order_after_ins AFTER INSERT ON orders FOR EACH ROW BEGIN UPDATE customer_summary c SET c.total_amount c.total_amount :NEW.amount WHERE c.customer_id :NEW.customer_id; END; /逻辑上没有任何问题事务回滚时汇总表的更新也会跟着回滚数据不会不一致。但实际跑起来这个触发器会让订单表成为性能瓶颈高频插入场景下customer_summary 这一行会被反复 UPDATE行锁竞争激烈并发一高就会出现锁等待。所以我现在的倾向是小数据量、低并发、实时性要求高的系统触发器维护汇总没问题中大型系统我更推荐把汇总逻辑挪到应用层的批量任务或者物化视图里让触发器只做审计和轻量校验。做方案时不要一听触发器能实时维护汇总就上先想清楚你的写入量级。5. 触发器避坑指南五个翻车现场与排查方法触发器出问题比存储过程出问题更隐蔽。存储过程至少是显式调用的报错能找到调用点触发器是数据库自动触发的一条 UPDATE 进来背后可能串了一整条触发器链。下面五个坑每个都是真实生产环境里踩出来的。5.1 ORA-04091触发器读了自己正在变的表现象在 emp 表上写了个行级触发器里面去 SELECT AVG(salary) FROM emp执行 UPDATE emp SET salary salary * 1.1 时报错ORA-04091: table EMP is mutating, trigger/function may not see it原因行级触发器执行时emp 表正处于被修改的变异状态。Oracle 不允许在行级触发器代码里直接读同一张表因为此时表里的数据处于不确定状态读了可能会得到错误结果。解决不要把查询放在行级触发器里。常见做法是先把行级需要的数据收集到包变量等语句完成后再由语句级触发器统一读取。如果你用的是 Oracle 12c 及以上版本可以改用复合触发器Compound Trigger在行级阶段只收集数据在 AFTER STATEMENT 阶段再查询并处理结构更清晰。详见这段收集型写法-- 包存放会话级的累计变量 CREATE OR REPLACE PACKAGE emp_stat_pkg AS g_total_salary NUMBER : 0; END; / -- 行级触发器只收集旧值不查表 CREATE OR REPLACE TRIGGER trg_emp_collect AFTER UPDATE OF salary ON emp FOR EACH ROW BEGIN emp_stat_pkg.g_total_salary : emp_stat_pkg.g_total_salary :OLD.salary; END; / -- 语句级触发器表已经不处于变异状态可以安全使用 CREATE OR REPLACE TRIGGER trg_emp_after_stmt AFTER UPDATE OF salary ON emp BEGIN DBMS_OUTPUT.PUT_LINE(Total old salary: || emp_stat_pkg.g_total_salary); emp_stat_pkg.g_total_salary : 0; -- 用后复位避免下次累计错误 END; /这种写法避开了 ORA-04091但也引入了一个新的责任包变量是会话级的如果你在同一个会话里连续执行两条 UPDATE 但第二条没触发语句级触发器变量不会自动清零所以用后手动复位是必须的。注意遇到 ORA-04091 时不要用自治事务绕过它。自治事务虽然能躲开变异表检查但会引入数据一致性和事务隔离问题治标不治本。5.2 触发器里的 COMMIT把外层事务切成两半现象应用层用存储过程包了一个事务过程里更新 emp 后又更新 dept中途出错回滚。但实际执行后发现 emp 的更新没有回滚数据已经提交了。查遍应用代码最后在触发器的 EXCEPTION 块里发现了 COMMIT。原因Oracle 默认情况下触发器参与调用者的事务。一旦在触发器里写了 COMMIT 或 ROLLBACK就会切断当前事务事务边界就被拆碎了。应用层后续的 ROLLBACK 只能回滚到触发器 COMMIT 之后的部分。解决触发器代码里严格禁止写事务控制语句。如果确实需要独立提交日志——比如审计操作即使主事务失败也要记录——可以声明自治事务CREATE OR REPLACE TRIGGER trg_emp_audit_autonomous AFTER UPDATE ON emp FOR EACH ROW DECLARE PRAGMA AUTONOMOUS_TRANSACTION; BEGIN INSERT INTO emp_audit_log (emp_id, old_salary, new_salary, op_type) VALUES (:OLD.emp_id, :OLD.salary, :NEW.salary, UPDATE); COMMIT; -- 自治事务内允许提交不影响主事务 END; /但自治事务要慎用。它提交的数据不会随主事务回滚也就是说可能记录到一笔最终被取消的变更这在合规审计里是有误导性的。我一般只在确有必要的时候才用而且会在代码里写清楚为什么需要自治。5.3 递归触发写了个会调用自己的触发器现象执行一条 INSERT INTO a触发了一个往表 b 插入数据的触发器而 b 的触发器又往表 a 插入数据形成触发器级联。执行一段时间后报错ORA-00036: maximum number of recursive SQL levels (50) exceeded原因触发器链式触发了超过了 Oracle 允许的递归深度通常是表结构设计时没考虑触发器之间互相调用。解决第一种方案是切断循环链让其中一层不再使用触发器改成存储过程由业务显式调用第二种方案是在包变量里加一个逻辑开关第一次触发后把开关置 FALSE后续即使再次进入触发器直接跳过。CREATE OR REPLACE PACKAGE trg_guard AS g_running BOOLEAN : FALSE; END; / CREATE OR REPLACE TRIGGER trg_a_bi BEFORE INSERT ON a FOR EACH ROW BEGIN IF NOT trg_guard.g_running THEN trg_guard.g_running : TRUE; INSERT INTO b(id) VALUES (:NEW.id); trg_guard.g_running : FALSE; -- 确保下一次正常触发 END IF; END; /注意这个布尔开关是会话级的同一会话里多个语句共享。如果在递归链中间发生异常g_running 可能没有被复位后续所有操作都会被跳过所以异常处理里也要复位。更稳妥的做法是对表名加判断条件而不是单纯一个全局开关。5.4 异常没有处理一条脏数据让整批业务崩掉现象触发器里写了一个 SELECT INTO想把某个字段的值读到变量里。某天出现一条异常数据SELECT INTO 查不到结果抛 ORA-01403 NO_DATA_FOUND结果主表的 UPDATE 直接被这个触发器拖垮整个批量任务失败。原因触发器内的异常如果没有 EXCEPTION 块捕获会向上传播到发起 DML 的会话错误被当成这条 SQL 的错误返回给应用。触发器是隐式执行的应用层根本看不到触发器代码排错时很容易先怀疑自己的 SQL 写错了。解决对于可能查不到数据的逻辑显式处理空结果CREATE OR REPLACE TRIGGER trg_emp_check_mgr BEFORE INSERT OR UPDATE ON emp FOR EACH ROW DECLARE v_salary NUMBER; BEGIN -- 预期可能查不到数据时用嵌套块捕获 BEGIN SELECT salary INTO v_salary FROM emp WHERE emp_id :NEW.manager_id; IF v_salary :NEW.salary THEN RAISE_APPLICATION_ERROR(-20002, 上级薪资不能低于下级); END IF; EXCEPTION WHEN NO_DATA_FOUND THEN NULL; -- 没有上级就不校验 END; END; /这里的重点是把 SHOW 业务逻辑的异常单独嵌套在一个子块里NO_DATA_FOUND 时静默处理不影响主流程而真正需要抛错给业务的情况用 RAISE_APPLICATION_ERROR 显式抛出。触发器里写任何查询前先问自己一句查不到怎么办想好再写。5.5 排查触发器问题的三个入口数据字典、日志表、PL/SQL Developer 调试现象生产环境一个审计触发器既不报错也没写入数据另一个触发器偶尔报错但不知道触发了多少次。这种问题靠猜是猜不出来的。解决按下面三个入口逐个排查。第一个入口是数据字典。SELECT trigger_name, status, table_name FROM user_triggers 先看触发器的启用状态。我遇到过的触发器没生效案例里有一半是之前被 DISABLE 了没恢复。第二个入口是临时调试日志。在触发器里加一行写入调试表比如每次触发都记录一条 log_time 和触发事件复现一次后看日志就能确认触发器到底有没有被触发。生产环境不建议用 DBMS_OUTPUT因为除非显式设置否则用户根本看不到输出而日志表是落地的随时可以查。第三个入口是用 PL/SQL Developer 的调试功能。选中触发器名右键选择 View在打开的编辑窗口里设置断点然后执行一条会触发它的 DML。调试器会在断点停下你可以查看 :NEW、:OLD 的每个字段值。这一步能非常直观地看到问题。调试完注意回滚 DML别把调试数据留在业务表里。6. 触发器的进阶用法条件触发、性能验证与调试习惯6.1 WHEN 子句里不能放的东西WHEN 子句虽然灵活但有明确边界。它只能引用 :NEW 和 :OLD 的字段不能调用你自己的自定义函数也不能读序列。WHEN (NEW.salary get_min_salary()) 这种写法会被 Oracle 直接拒绝因为 WHEN 条件会被翻译成底层表达式函数调用在每条记录判断时都不可预测。复杂判断请挪到触发体里用 PL/SQL 写。6.2 批量操作下测量触发器的性能成本判断一个触发器该不该留最直接的方法是量化它对 DML 的影响。做法很简单先禁用触发器跑一遍批量插入记录耗时再启用触发器用同样数量的数据跑一遍对比差值就是触发器的成本。SET TIMING ON BEGIN FOR i IN 1..1000 LOOP INSERT INTO emp(emp_id, emp_name, salary) VALUES (i, name || i, 1000); END LOOP; END; /我一般会在新写的触发器上线前做一次这种对比如果批量 1000 行耗时从 200 毫秒涨到 2 秒就得考虑是不是触发器里做了太多事情。性能不达标的触发器通常会改成语句级或者把一部分逻辑挪到应用层的批量任务里。6.3 触发器代码里的调试钩子我习惯在每个生产触发器的异常处理里把错误信息、触发事件、:OLD/:NEW 关键字段写进一张独立的错误日志表并用一个包变量控制是否启用。平时关闭排障时打开不影响线上性能。这样触发器出了问题不用再登到库上看半天一张SELECT * FROM trg_err_log就能定位问题出在哪张表、哪个触发器、哪条数据上。触发器是数据库的主动行为比存储过程更隐蔽。它一旦上线影响的是所有经过该表的 DML。写之前想清楚场景写之后做好验证和日志兜底。我自己的教训是接手任何数据库第一件事先查一遍 USER_TRIGGERS把没有文档说明的触发器全摸清楚再动业务表——被一张没人知道是谁建的触发器坑过一次之后这个习惯就再也丢不掉了。希望帮到你。本文还有配套的精品资源点击获取