mysql用before update触发器+ signal 阻断敏感字段修改,需显式处理null;postgresql用raise exception+is distinct from;sql server用instead of触发器;sqlite用raise(abort);所有方案均需配合权限控制。

MySQL 中用 SIGNAL 拦截字段修改
唯一能真正阻止敏感字段被改的方式,是在 BEFORE UPDATE 触发器里比对 OLD.field 和 NEW.field,不一致就用 SIGNAL SQLSTATE '45000' 主动报错中断事务。别指望“悄悄还原”或“日志告警”——那改已经发生了。
常见错误是写成 IF OLD.salary != NEW.salary THEN SIGNAL ...,但字段允许 NULL 时会失效:因为 NULL != NULL 返回 UNKNOWN,条件不成立。必须显式处理空值:
(OLD.salary != NEW.salary) OR (OLD.salary IS NULL) != (NEW.salary IS NULL)- JSON 字段建议用
JSON_EQUAL(OLD.field, NEW.field)(MySQL 8.0.26+),避免因格式空格/键序差异误判 -
SIGNAL仅 MySQL 5.5+ 支持;低版本只能靠插入不存在表等 hack 方式,但错误信息模糊、难定位
PostgreSQL 中用 RAISE + IS DISTINCT FROM 安全判断
PostgreSQL 的触发器不能直接阻断操作,但 BEFORE UPDATE 里配合 RAISE EXCEPTION 可中止整个事务。关键在于字段比较要用 IS DISTINCT FROM,它天然处理 NULL,比 != 可靠得多。
例如保护已审批订单的金额:
IF OLD.status IN ('paid', 'closed') AND NEW.amount IS DISTINCT FROM OLD.amount THEN RAISE EXCEPTION 'amount locked in final state';-
IS DISTINCT FROM是逻辑判断,不能用于索引,但正适合这种校验场景 - 注意:PostgreSQL 触发器本身不拦权限,必须先
REVOKE UPDATE ON table FROM user,再只授非敏感字段权限,否则用户可绕过触发器直连表
SQL Server 用 INSTEAD OF 触发器替代写入
SQL Server 不支持在触发器里用 SIGNAL 或 RAISE 中断,正确做法是建 INSTEAD OF UPDATE 触发器,不执行原语句,而是直接报错并回滚。
典型写法:
CREATE TRIGGER trReadOnly_UserSSN ON users INSTEAD OF UPDATE AS BEGIN RAISERROR('SSN is immutable', 16, 1); ROLLBACK; END- 别用
AFTER UPDATE—— 修改已发生,回滚只影响触发器内部,原 UPDATE 依然生效 - 主键或
IDENTITY列根本走不到触发器,引擎会在解析阶段就报错,所以只保护业务字段(如ssn、account_number) - 禁止在触发器里更新同表(哪怕只是
updated_at),会引发嵌套触发或死锁
SQLite 用 RAISE(ABORT) + PRAGMA 控制维度表只读
SQLite 的 BEFORE UPDATE 触发器可用 RAISE(ABORT, 'msg') 中止修改,但它默认不启用递归触发,必须提前设 PRAGMA recursive_triggers = ON,否则触发器可能不执行。
适用于防止误批量更新维度表:
CREATE TRIGGER lock_dim_region BEFORE UPDATE ON dim_region BEGIN SELECT RAISE(ABORT, 'dim_region is read-only'); END;-
AFTER触发器完全无效——改完了才触发,拦不住 - 如果表只是偶尔需更新(比如新增国家),触发器里要加条件判断,比如只拦
WHERE status = 'active'这类范围更新,放行按主键的单条UPDATE ... WHERE id = 123
所有方案里最容易被忽略的一点:触发器只管语句执行路径,不管权限。如果用户本身有表级 UPDATE 权限,他可以绕过触发器直接发语句——真正的防线永远是权限收窄,触发器只是最后一道校验。











