ON DUPLICATE KEY UPDATE 是 MySQL 唯一原生 UPSERT 操作,仅在主键或唯一索引冲突时触发更新,否则报错;支持复合唯一索引,但多唯一索引下按首个匹配索引判断,存在歧义与风险。

直接说结论:ON DUPLICATE KEY UPDATE 是 MySQL 唯一可用的原生“存在则更新、不存在则插入”原子操作,但它**只响应主键或唯一索引冲突**,不是万能 UPSERT,用错场景反而引发死锁、ID 跳变或误更新。
触发条件:必须有主键或唯一索引才能生效
这个语句根本不会执行,除非你表里明确定义了 PRIMARY KEY 或 UNIQUE 约束。没有约束,它就退化成普通 INSERT,冲突时直接报错 Duplicate entry。
- 复合唯一索引也支持,比如
UNIQUE KEY (user_id, date),只要VALUES中这两列组合重复,就会触发UPDATE - 多个唯一索引共存时行为危险:MySQL 会选**第一个匹配的唯一索引**做冲突判断,且
WHERE条件等效于WHERE a = ? OR b = ? LIMIT 1—— 可能只更新其中一行,而非全部匹配行 - 大小写敏感性由字段 collation 决定:
utf8mb4_0900_as_cs下'A'和'a'视为不同值,不触发更新
VALUES() 函数:避免硬编码,批量场景必备
在 ON DUPLICATE KEY UPDATE 子句里,VALUES(col_name) 表示“本次 INSERT 语句中该列要插入的原始值”,不是当前行旧值。这是批量写入时保持逻辑一致的关键。
- 累加场景必须用它:
score = score + VALUES(score),而不是score = score + 10(后者在批量插入多行时所有行都加同一个常量) - 不能跨行引用:
VALUES()只作用于当前正在冲突检测的那一条VALUES元组 - 如果
INSERT的列没出现在VALUES列表里(比如允许 NULL 或有 DEFAULT),VALUES(col)返回NULL,需配合IFNULL处理:name = IFNULL(VALUES(name), name)
影响行数与 LAST_INSERT_ID():别依赖返回值做业务判断
mysql_affected_rows() 返回值含义特殊,但容易误导:
-
1:新插入一行 -
2:更新了一行(哪怕新旧值完全一样,也返回 2) -
0:更新目标行,但所有被赋值字段的新值与旧值逐字节相等(注意:NULL 与 NULL 比较结果为 UNKNOWN,通常不等于) -
LAST_INSERT_ID()在插入时返回新生成的自增 ID;在更新时也返回该行当前的自增 ID —— 但**不是本次操作生成的**,是原有值,容易让人误以为“有新 ID 产生” - 高并发下,不要用返回值判断“是否首次插入”来发通知或初始化,默认值逻辑应放在应用层或触发器里
和 REPLACE INTO 的核心区别:别把 ID 重置当更新
REPLACE INTO 是先 DELETE 再 INSERT,而 ON DUPLICATE KEY UPDATE 是纯 UPDATE。这意味着:
- 自增 ID 必然跳变:
REPLACE会分配新 ID,老记录的id彻底丢失;ON DUPLICATE KEY UPDATE不动id,也不影响后续自增值(InnoDB 下仍会预分配) - 外键级联风险:
REPLACE的DELETE可能触发ON DELETE CASCADE,ON DUPLICATE KEY UPDATE完全不涉及删除 - 触发器行为不同:
REPLACE触发DELETE和INSERT触发器各一次;ON DUPLICATE KEY UPDATE只触发UPDATE触发器(但注意:可能被多次触发,比如批量插入时每行都触发) - 性能差异明显:对大表,
REPLACE的 delete+insert 开销远高于 update,尤其有二级索引时
真正需要“覆盖式刷新”的场景(比如缓存表全量同步),REPLACE 更干脆;但凡涉及数据连续性、审计追踪或外键完整性,ON DUPLICATE KEY UPDATE 是更安全的选择。最易被忽略的一点:它不保证你更新的字段一定来自你传入的那条记录——当存在多个唯一索引时,匹配逻辑本身就有歧义。


















