ON DUPLICATE KEY UPDATE 不触发 INSERT 触发器,仅执行 UPDATE 触发器;VALUES() 在触发器中无效,需显式传值;并发下 affected_rows=0 时触发器仍运行,须在触发器内判断 OLD!=NEW。

ON DUPLICATE KEY UPDATE 不触发 INSERT 触发器
它根本不会进入 INSERT 触发器逻辑——哪怕语句里写了 INSERT INTO。只要发生唯一键冲突并走更新分支,MySQL 就跳过所有 BEFORE INSERT 和 AFTER INSERT 触发器,只执行 BEFORE UPDATE 和 AFTER UPDATE 触发器。
常见错误现象:BEFORE INSERT 里写的自动生成 created_at 或校验逻辑完全没生效,但日志显示语句执行成功了;查数据库发现字段为空或校验被绕过。
- 这是设计行为,不是 bug:ODKU 的本质是“尝试插入 → 冲突则转为更新”,引擎层不认为这是一次 INSERT 操作
- 如果你依赖
INSERT触发器做初始化(比如生成 UUID、设置默认状态),必须把这部分逻辑复制到BEFORE UPDATE中,或统一改用存储过程封装 - 注意触发器顺序:如果同时存在
BEFORE UPDATE和AFTER UPDATE,ODKU 更新会按正常 UPDATE 流程触发它们,和普通 UPDATE 无异
VALUES() 函数在触发器里不可用
VALUES(col_name) 是 ODKU 语句级的语法糖,只在 ON DUPLICATE KEY UPDATE 子句中有效;一旦进入触发器上下文,它就失效了,直接报错 Unknown column 'VALUES(...)' in 'field list'。
使用场景:你想在 BEFORE UPDATE 里拿到“本该插入但被转成更新”的那个新值,比如审计字段或业务规则校验。
- 正确做法:在 ODKU 的
UPDATE子句中显式传入值,例如status = VALUES(status), updated_by = 'system',再在触发器里读NEW.status - 不能写
SET @new_status = VALUES(status)或在触发器里引用VALUES()—— 它不是变量,也不是函数调用,只是 INSERT 语句的特殊占位符 - 如果需要动态判断“这次是真更新还是假更新”(比如新旧值相同却仍触发了 ODKU),得靠
OLD.col != NEW.col在触发器里手动比对
复合唯一索引下触发器行为与锁范围强相关
当表有多个唯一索引(如 UNIQUE KEY (user_id, event_type))时,ODKU 可能命中任意一个冲突索引,而触发器看到的 OLD 和 NEW 值始终是那条被更新的物理行,但锁的范围取决于哪个索引触发了冲突。
容易踩的坑:两个事务同时执行 INSERT INTO t (user_id, event_type, score) VALUES (1001, 'login', 5) ON DUPLICATE KEY UPDATE score = score + VALUES(score),结果死锁或触发器读到不一致的中间状态。
- InnoDB 会为“引发冲突的那个唯一索引路径”加锁,不是固定锁主键;如果事务 A 因
user_id冲突加锁,事务 B 因event_type冲突加锁,可能形成锁等待环 -
BEFORE UPDATE触发时,行锁已持有,但间隙锁(gap lock)可能覆盖更大范围——别在触发器里再查其他表或调用耗时函数,否则放大锁等待时间 - 建议:只用单个高选择性唯一索引驱动 ODKU(比如邮箱、设备 ID),避免多唯一键共存;若必须用复合索引,确保所有 ODKU 语句都显式提供全部组合字段,减少歧义
并发下 affected_rows=0 时触发器仍会执行
当 ODKU 执行更新,但新值和旧值完全相等(如 name = VALUES(name) 且数据库里本来就是 'Alice'),MySQL 返回 affected_rows = 0,但 BEFORE UPDATE 和 AFTER UPDATE 触发器照常运行。
这意味着:你以为“没变就没触发”,其实触发器里的日志、计数器、外部调用全被执行了,可能造成误统计或资源浪费。
- 必须在触发器里显式判断:
IF OLD.col != NEW.col THEN ... END IF;,而不是依赖外部返回值做开关 - 尤其注意时间戳字段:如果写
update_time = NOW(),即使其他字段没变,update_time也会刷新,导致affected_rows = 2;但如果写成update_time = VALUES(update_time)且传入的值和旧值一样,就可能为 0 - MyBatis 等框架常忽略
affected_rows = 0的情况,直接走 success 回调——业务代码里别只看返回值,要结合触发器日志交叉验证
最易被忽略的一点:触发器看到的永远是“最终要写入的值”,但它无法区分这个值来自原始 INSERT VALUES 还是来自你硬编码的常量;ODKU 的原子性掩盖了数据来源路径,而触发器只能被动响应结果。


















