应使用自增主键+业务字段组合唯一索引,并采用微秒级时间戳(DATETIME(6))避免冲突;触发器须分AFTER INSERT/UPDATE/DELETE单独定义,严禁在触发器内查询被操作表以防ERROR 1442。

触发器写入历史表时,主键冲突或时间戳重复怎么办
直接在 AFTER UPDATE 触发器里往历史表插入原记录,最容易踩的坑是:没处理主键冲突,或者用 NOW() 作为版本时间导致同一事务内多条更新产生相同时间戳,后续查版本时无法区分顺序。
正确做法是让历史表主键脱离业务主键约束,改用自增 id + 业务字段组合唯一索引;时间戳统一用 SYSUTCDATE() 或加上微秒级精度(MySQL 5.6.4+ 支持 DATETIME(6)):
CREATE TABLE user_history (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
name VARCHAR(100),
email VARCHAR(255),
version_at DATETIME(6) NOT NULL,
op_type ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL,
UNIQUE KEY uk_user_version (user_id, version_at)
);
- 别依赖业务主键做历史表主键,否则
UPDATE多次会因重复user_id冲突 -
version_at必须带微秒精度,否则并发更新可能写入相同时间戳 - 加
UNIQUE KEY (user_id, version_at)是为了防止误操作重复写入,不是主键替代方案
INSERT/UPDATE/DELETE 触发器必须分开定义,不能合并
MySQL 不支持一个触发器响应多个事件类型,BEFORE INSERT, UPDATE 这种语法是非法的。每个操作都得单独建触发器,且注意 OLD 和 NEW 的可用性差异。
例如记录用户变更,要建三个触发器:
CREATE TRIGGER user_insert_history AFTER INSERT ON users FOR EACH ROW INSERT INTO user_history (user_id, name, email, version_at, op_type) VALUES (NEW.id, NEW.name, NEW.email, SYSUTCDATE(6), 'INSERT'); CREATE TRIGGER user_update_history AFTER UPDATE ON users FOR EACH ROW INSERT INTO user_history (user_id, name, email, version_at, op_type) VALUES (OLD.id, OLD.name, OLD.email, SYSUTCDATE(6), 'UPDATE'); CREATE TRIGGER user_delete_history AFTER DELETE ON users FOR EACH ROW INSERT INTO user_history (user_id, name, email, version_at, op_type) VALUES (OLD.id, OLD.name, OLD.email, SYSUTCDATE(6), 'DELETE');
-
INSERT只能用NEW,DELETE只能用OLD,UPDATE两者都能用 —— 但历史快照应存变更前状态,所以用OLD - 所有触发器必须用
AFTER,否则事务回滚时历史记录已写入,造成数据不一致 - 别在触发器里做复杂逻辑(比如关联查询、调用函数),容易锁表或拖慢主表写入
触发器中访问被修改表会报错 ERROR 1442
如果在 user_update_history 里试图查 users 表(比如想补全某个字段),MySQL 会直接报 ERROR 1442: Can't update table 'users' in stored function/trigger because it is already used by statement which invoked this stored function/trigger。
这是 MySQL 的硬限制:触发器不能读写当前正在被 DML 操作的表。绕过方法只有两个:
- 把需要的字段全部从
OLD/NEW显式取出来,不要现场查 —— 这是最常用也最安全的方式 - 改用应用层双写:先查出完整旧数据,再执行
UPDATE,最后异步写历史表(放弃触发器,换消息队列或事务日志解析)
没有第三种办法。哪怕用临时表、子查询、变量赋值,只要最终访问了 users,就触发这个错误。
历史表膨胀后查询慢,怎么避免全表扫描
版本表数据只增不删,几个月后动辄千万行,按 user_id 查最新 3 条版本,没索引就是 type: ALL。
关键索引不是简单加 INDEX(user_id),而是复合索引覆盖查询模式:
ALTER TABLE user_history ADD INDEX idx_user_version (user_id, version_at DESC);
- 必须把
version_at DESC写进索引定义,否则ORDER BY version_at DESC LIMIT 3仍可能失效 - 如果常按时间范围查(比如“某天所有变更”),再加一个
INDEX(version_at)独立索引 - 定期归档老数据(如保留 180 天),用
DELETE FROM user_history WHERE version_at ,但注意加 <code>LIMIT 10000分批删,避免长事务锁表
触发器本身不解决性能问题,它只负责“写对”,查得快得靠索引设计和归档策略——这点最容易被忽略,等慢了才想起加索引,但大表重建索引又卡住线上服务。


















