MySQL的INSERT ... ON DUPLICATE KEY UPDATE是实现UPSERT的核心方案,依赖主键或唯一索引触发原子性“存在即更新、不存在则插入”,避免竞态;若无唯一约束则退化为普通插入,不触发更新。

MySQL的INSERT ... ON DUPLICATE KEY UPDATE是核心方案
只要表有唯一索引(UNIQUE)或主键(PRIMARY KEY),就能用这条语句实现UPSERT。它不是“先查再插/更”的模拟逻辑,而是单条原子语句,避免竞态和重复插入错误。
常见错误是没建唯一约束就直接用——这时ON DUPLICATE KEY UPDATE完全不触发,只会静默插入重复行。
实操建议:
- 确保目标字段(如
email、user_id)上有UNIQUE索引,不是普通INDEX - 更新子句中可引用
VALUES(col_name)获取本次想插入的值,比如updated_at = VALUES(updated_at) - 若要更新计数器(如
clicks = clicks + 1),直接写表达式即可,无需VALUES() - 注意:如果唯一冲突来自联合唯一索引,必须所有列都参与冲突判断,不能只靠其中一列
用REPLACE INTO要格外小心
REPLACE INTO本质是“删+插”:先按唯一键匹配删除旧行,再插入新行。它看起来像UPSERT,但行为完全不同。
典型坑点:
- 自增ID会变化——即使内容没变,
id也可能被重分配,破坏外键引用或历史日志关联 - 触发器会执行两次(
DELETE和INSERT各一次),可能引发意料外副作用 - 如果有
ON DELETE CASCADE关联子表,REPLACE会真实删除再重建,开销大且危险 - 无法做条件更新(比如只在
new_value > old_value时才更新),它总是无脑覆盖
除非你明确需要“强制覆盖+重置ID”,否则别用REPLACE INTO替代UPSERT。
MySQL 8.0+ 可用INSERT ... ON CONFLICT DO UPDATE?不,那是PostgreSQL
MySQL至今(包括8.0和8.4)**不支持**标准SQL的ON CONFLICT语法。网上很多教程混用PostgreSQL写法,直接照搬会报错:ERROR 1064 (42000): You have an error in your SQL syntax。
别被ORM误导——有些ORM(如Django、SQLAlchemy)生成的“upsert”语句,在MySQL后端实际转成INSERT ... ON DUPLICATE KEY UPDATE;但如果你手写SQL,必须用MySQL原生语法,不能写ON CONFLICT。
实操提醒:
- 检查MySQL版本用
SELECT VERSION();,别凭印象判断 - 跨数据库迁移时,
ON DUPLICATE KEY UPDATE和ON CONFLICT不可互换,需单独适配 - 如果业务强依赖
ON CONFLICT语义(比如排除特定冲突列),只能靠应用层加锁或改用其他数据库
批量UPSERT性能与限制
一条INSERT ... ON DUPLICATE KEY UPDATE可带多行数据,比如INSERT INTO t(a,b) VALUES (1,2),(3,4) ON DUPLICATE KEY UPDATE b = VALUES(b);,这比循环单条快得多。
但要注意边界:
- 单次插入行数不宜超过1000行,否则可能触发
max_allowed_packet限制,报错Packets larger than max_allowed_packet are not allowed - 所有行共用同一个
UPDATE子句,不能为每行指定不同更新逻辑(比如第1行更新status,第2行更新score) - 如果某一行违反其他约束(如
NOT NULL、外键),整条语句失败,不会部分执行 - 涉及大字段(
TEXT/BLOB)时,批量反而可能因内存占用高而变慢
真正难处理的是“按不同条件更新不同字段”或“冲突时跳过而非更新”,这些得退回到应用层控制流,或者拆成INSERT IGNORE + 单独UPDATE两步——但必须加事务和适当锁。


















