触发器中可向独立历史表INSERT,但必须用AFTER UPDATE且显式比较OLD/NEW值防NULL陷阱;需加索引、分区及reason字段,禁用DELETE LIMIT清理。

触发器里不能直接 INSERT INTO 历史表?先确认事务上下文
很多同学写完 BEFORE UPDATE 触发器,发现历史记录没存进去,或者报错 Can't update table 'products' in stored function/trigger。根本原因是:MySQL 不允许在触发器中修改当前正在被更新的表(即“递归修改”限制),但允许往其他表 INSERT。所以历史归档表必须是独立表(比如 product_price_history),且触发器类型得选 AFTER UPDATE——不是为了“等更新完成”,而是绕过该限制。
-
AFTER UPDATE是唯一安全选择;BEFORE UPDATE无法向历史表 INSERT(若历史表与主表有外键或级联,更易出错) - 触发器中避免调用存储函数或复杂子查询,否则可能拖慢主业务更新,尤其高并发改价场景
- 务必给历史表加索引:
CREATE INDEX idx_product_time ON product_price_history (product_id, updated_at);
怎么只归档“真正变化”的价格?别存冗余快照
如果每次 UPDATE 都无条件插入历史记录,哪怕 price 没变,也会产生脏数据。正确做法是在触发器里显式比较新旧值。MySQL 提供 OLD.price 和 NEW.price,但要注意 NULL 安全比较。
- 用
IF OLD.price NEW.price THEN ... END IF;判断是否真变化(处理 NULL 相等) - 别用
OLD.price != NEW.price,当任一为 NULL 时结果为 NULL,导致条件不成立 - 如果业务要求记录“调价意图”(比如运营手动点“保存”,即使价格未变也要留痕),那就去掉判断,直接归档
历史表结构设计:时间戳、操作人、变更原因怎么存?
单纯存 old_price/new_price 不够。真实场景需要知道谁、什么时候、为什么改价。但触发器本身拿不到应用层的用户 ID 或备注,得靠约定字段或会话变量。
- 在主表加一个可空的
updated_by字段(VARCHAR),应用层 UPDATE 时显式传入,触发器读NEW.updated_by - 用
CONNECTION_ID()或USER()作兜底(仅限调试,不可用于审计) - 历史表必加
reason VARCHAR(255)字段,对应主表的update_reason,避免后期补录困难 - 时间一律用
NOW(3)(毫秒级),别依赖应用层传时间,防止时钟不同步
归档量太大怎么办?分区或 TTL 清理要提前规划
价格变动频繁的商品(如秒杀品),一年可能生成上万条历史记录。不处理会导致查询变慢、备份膨胀。
- 按年分区:对
product_price_history表按updated_at做 RANGE 分区,例如PARTITION p2024 VALUES LESS THAN (UNIX_TIMESTAMP('2025-01-01')) - 禁止用
DELETE FROM ... LIMIT清理,容易锁表;改用ALTER TABLE ... DROP PARTITION p2022; - 如果数据库版本 product_price_history_2024),用视图聚合查询
UPDATE 语句——只要绕过 ORM 直接执行 SQL,就可能漏掉 updated_by 字段,导致历史记录丢失责任人。

















