触发器中应避免SELECT *、多表JOIN、标量函数及全表扫描,优先使用INSERTED/DELETED表、EXISTS、带索引WHERE条件;耗时操作须移出触发器,改用队列或CDC;禁用比删除更安全但需防范隐式依赖风险。

触发器里别写 SELECT * 或 JOIN 多张表
绝大多数高资源消耗的触发器,根源在于在 AFTER INSERT 或 INSTEAD OF UPDATE 里执行了全表扫描或跨表关联。比如用 SELECT * FROM orders JOIN customers ON ... 去查上下文,哪怕只插一行,也会拖慢整个事务。
实操建议:
- 只查真正需要的字段,且必须带
WHERE条件,条件字段要有索引(如WHERE id = @inserted.id) - 避免在触发器中调用标量函数(
dbo.GetUserName()),它们无法并行、易阻塞 - 如果要查历史数据,优先用
EXISTS而非SELECT COUNT(*),前者可短路
用 INSERTED/DELETED 代替子查询查原表
常见错误是:在 UPDATE 触发器里写 SELECT * FROM products WHERE id IN (SELECT id FROM inserted) —— 这会引发二次读取,还可能读到未提交的脏数据(取决于隔离级别)。
正确做法是直接从 INSERTED 和 DELETED 临时表取值:
UPDATE p SET p.last_modified = GETDATE(), p.version = p.version + 1 FROM products p INNER JOIN inserted i ON p.id = i.id;
注意:
-
INSERTED和DELETED是内存表,无锁、无日志、零IO - 批量操作时它们可能含多行,别假设只有一条(
@@ROWCOUNT要检查) - 不能在
INSTEAD OF触发器里直接 UPDATE 原表,否则会递归触发自己
把耗时逻辑移出触发器,改用 Service Broker 或队列
发邮件、调外部 API、写日志表、生成报表缓存……这些操作不该出现在触发器里。它们会让事务变长、锁住主表、拖垮并发吞吐。
可行替代方案:
- 在触发器里只写一条记录到轻量级队列表(
audit_queue),带event_type、target_id、created_at - 用 SQL Agent Job 每 5 秒轮询一次该表,或用
WAITFOR (RECEIVE ...)接收 Service Broker 消息 - 业务层改用 CDC(变更数据捕获)+ 应用监听,比触发器更可控、更易监控
禁用触发器比删掉更安全,但要注意隐式依赖
线上临时禁用触发器(DISABLE TRIGGER tr_log_changes ON orders)比删掉再重建快得多,也不会影响依赖它的存储过程或视图绑定。
但容易踩的坑:
- 禁用后,应用可能因缺失审计/校验逻辑而写入非法数据(比如绕过金额正负校验)
- 某些 ORM(如 Entity Framework)会自动检测触发器存在,禁用后可能报错或跳过预期逻辑
-
ALTER TABLE ... DISABLE TRIGGER ALL会一并禁用所有,包括你不知道谁写的系统级触发器
真正难的不是写触发器,而是判断“这一行变更,到底有没有必要立刻响应”。多数时候,延迟几秒再处理,系统反而更稳。










