mysql触发器中old.col != new.col会漏判null变化,因null != null结果为unknown而非true;应改用空安全比较、显式类型转换、字段级条件判断及before update时机,并规范日志存储与索引。

MySQL触发器里为什么OLD.col != NEW.col会漏判NULL变化
因为SQL中NULL != NULL结果是UNKNOWN,不是TRUE或FALSE,而IF条件只响应TRUE。哪怕字段从NULL变成'abc',用!=也跳过逻辑。
必须改用空安全等于运算符:OLD.email NEW.email返回0才表示真变了;返回1说明值一致(含两个NULL)。
- 别写
OLD.phone IS NULL AND NEW.phone IS NOT NULL这种长条件——易漏、难维护、不覆盖双向变化 - 字符串区分大小写需显式加
BINARY:BINARY OLD.name BINARY NEW.name - 数值字段注意隐式类型转换:如果
OLD.amount是DECIMAL而NEW.amount来自字符串参数,比较前先CAST(NEW.amount AS DECIMAL(10,2))
如何只记录真正变更的字段,避免日志爆炸
触发器默认每行UPDATE都触发一次,但多数字段根本没变。不加判断就全量插入日志,user表更新一次name,email、phone、updated_at全记一遍,查起来慢,磁盘涨得快。
正确做法是为每个待审计字段单独写判断分支,用UNION ALL合并结果再插入:
IF OLD.email NEW.email THEN
INSERT INTO audit_log (table_name, row_id, column_name, old_value, new_value)
VALUES ('users', OLD.id, 'email', OLD.email, NEW.email);
END IF;
- 不要试图用
OLD.* NEW.*——MySQL语法不支持通配符比较 - 新增字段(如加了
avatar_url)必须手动补上对应IF块,没有自动适配机制 - 大字段(如
TEXT或JSON)建议存哈希值而非原文:SHA2(OLD.bio, 256),节省空间且避免日志表撑爆
BEFORE UPDATE还是AFTER UPDATE?选错时机直接丢数据
要用OLD和NEW做比对,只能在BEFORE UPDATE触发器里——这时新旧值都已就位,且还能在语句执行前拦截或修正。
AFTER UPDATE虽然也能读OLD/NEW,但语句已提交,若日志插入失败(比如磁盘满、权限不足),主事务不会回滚,造成数据与日志不一致。
-
BEFORE UPDATE中可安全修改NEW.col(如自动更新updated_at),不影响比对逻辑 - 禁止在
BEFORE里做SELECT ... INTO或调用含查询的存储函数——会拖慢主DML,高并发下锁等待飙升 - 日志表必须与原表同库、同引擎(推荐
InnoDB),否则跨库写入在事务中可能出错且无法回滚
审计字段值怎么存才方便后续查和还原
存原始值看似直观,但TEXT字段超长、JSON格式不统一、datetime带毫秒精度,会导致日志表膨胀且难以精确匹配还原。
关键不是“存得全”,而是“存得准、查得快、还原稳”:
-
old_value/new_value字段类型必须和原字段严格一致(比如原email是VARCHAR(255),日志列也得是VARCHAR(255),否则截断无声失败) - 时间类字段建议归一化到秒:
DATE_FORMAT(OLD.updated_at, '%Y-%m-%d %H:%i:%s'),避免毫秒差异误判为变更 - 联合索引至少要有
(table_name, row_id, column_name, change_time),不然按用户查历史变更就是全表扫 -
changed_by别依赖USER()——它返回的是数据库连接账户(如'app@10.0.1.5'),应用层应提前设SET @current_user = 'u123'再读取
字段级差异判断本身开销极小,但一旦混进子查询、函数调用或跨表操作,就从透明钩子变成性能瓶颈。最常被忽略的是:日志表没索引、TEXT字段没截断、changed_by写成固定字符串,上线后查三天前某条记录要等十几秒。











