必须用update(salary)判断字段是否变更,因其能准确识别null参与的修改;错误做法包括手动取值比较或用isnull转换后比较,正确方式是先用update(salary)守门,再从inserted/deleted表读值。

只在UPDATE中检查salary字段变更
触发器必须聚焦于salary本身的变动,而不是整行更新。SQL Server 提供UPDATE()函数来精准判断字段是否被修改:IF UPDATE(salary)比OLD.salary != NEW.salary更可靠——后者在NULL参与比较时会返回UNKNOWN,导致校验失效。
常见错误是写成IF @old_salary @new_salary并手动取值,这既冗余又易错;正确做法是直接用UPDATE(salary)做前置守门,再从inserted和deleted表里读具体数值。
- 必须用
UPDATE(salary)判断,不能依赖ISNULL(NEW.salary, 0) != ISNULL(OLD.salary, 0) - 多行更新时,
inserted和deleted都是表,不是标量,不能用SELECT TOP 1随意取值 - 若业务允许“调低但需审批”,可在触发器里加
IF NOT EXISTS (SELECT 1 FROM inserted i JOIN approvals a ON i.id = a.emp_id WHERE a.status = 'approved')这类关联校验
用THROW拦截非法调低,且保留原始事务语义
一旦发现NEW.salary ,必须立刻<code>THROW(SQL Server 2012+)或RAISERROR(旧版本),让整个UPDATE语句连同事务一起回滚。任何试图用RETURN、PRINT或静默跳过的方式,都会导致数据已改但没报错——这是最危险的“半成功”状态。
注意:THROW不接受变量作为错误消息,所以不能写THROW 50001, @msg, 1;得拼成常量字符串,或改用RAISERROR配合sp_executesql动态构造(不推荐)。
- 合法写法:
THROW 50001, 'Salary cannot be decreased without approval', 1; - 错误写法:
THROW 50001, CONCAT('Salary drop detected: ', @old, '→', @new), 1;(CONCAT在THROW中不支持) - 别在触发器里写
ROLLBACK TRAN——AFTER触发器中执行它会导致“事务已提交后再回滚”的语法错误
避免被高权限账号绕过:禁用+权限回收双保险
DBA或拥有ALTER TABLE权限的用户,一行DISABLE TRIGGER tr_salary_protect ON employees就能让防护彻底失效。仅靠触发器逻辑严密没用。
真正有效的防线是:先用REVOKE UPDATE(salary) ON employees FROM [app_user]收回对salary字段的直写权限,再把更新操作收口到存储过程中(该过程内部调用触发器)。这样即使触发器被禁,用户也无法直接UPDATE salary。
- 检查触发器是否启用:
SELECT name, is_disabled FROM sys.triggers WHERE parent_id = OBJECT_ID('employees'); - 禁止用户直接改salary:
REVOKE UPDATE(salary) ON employees FROM PUBLIC;(再单独授给运维角色) - 别把触发器建在
db_owner账号下——该角色天然能DISABLE TRIGGER
时间窗口与终态冻结要分开处理
防止薪资调低和防止非工作时间修改是两件事,不能混在一个触发器里判。前者是业务规则(金额方向),后者是操作策略(时间范围)。混写会导致逻辑膨胀、难测试、易漏判。
比如周五17:58发的调薪单,如果触发器同时检查时间和金额,可能因时间条件误拦合法调高;而真正的风险点是“终态冻结”——当员工状态为'terminated'或'archived'时,salary应完全不可改,此时时间判断反而多余。
- 终态冻结优先级更高:先查
OLD.status IN ('terminated', 'archived'),满足则无条件拒绝所有salary变更 - 时间限制建议另建一个DDL或登录触发器监控
DISABLE TRIGGER行为,而非塞进DML触发器 - 若真要加时间控制,小时判断必须用
DATEPART(HOUR, GETDATE()) ,不能用<code>BETWEEN 9 AND 17——后者在17:59:59.997之后仍为true
approval_ref字段,并在触发器里校验该字段非空——而不是指望触发器去查另一张表反向验证。










