在业务开发中,"如果记录不存在就插入,存在就更新"是一个非常高频的需求,比如用户签到、库存扣减、埋点计数、配置同步等场景。如果用先SELECT再判断后INSERT或UPDATE的方式,不仅需要多次数据库交互,还可能在并发下出现竞态问题。MySQL提供的ON DUPLICATE KEY UPDATE语法可以让一条INSERT语句在遇到主键或唯一索引冲突时自动转为UPDATE,既简化了代码,又保证了原子性。本文将系统讲解这条语法的使用方法、执行原理和实战注意事项。

一、ON DUPLICATE KEY UPDATE的基本语法与执行原理
这条语法的完整形式是在普通INSERT语句后面加上ON DUPLICATE KEY UPDATE子句,其基本结构如下:
INSERT INTO t_user_sign (user_id, sign_date, sign_count) VALUES (1001, '2024-06-01', 1) ON DUPLICATE KEY UPDATE sign_count = sign_count + 1;
它的执行逻辑是:MySQL先尝试执行INSERT,如果插入的行与表中已有记录在主键或唯一索引上产生冲突,则放弃插入,转而对那条已存在的记录执行UPDATE操作;如果没有冲突,就正常插入新行。整个判断过程在存储引擎层完成,是原子操作,不存在"先查后插"的时间窗口,因此在并发场景下不会出现重复插入的问题。
需要特别注意的是,冲突的判定依据是主键和唯一索引,而不是任何业务字段。如果表上没有主键或唯一索引,这条语句永远只会执行插入。另外,如果一条INSERT批量插入了多行数据,并且这些行之间存在索引冲突,或者与表内已有数据冲突,MySQL会逐行处理,冲突的行转为更新,不冲突的行正常插入。
二、VALUES函数与批量写入的实战技巧
在更新时经常需要引用本次想要插入的值,MySQL提供了VALUES()函数来获取。例如更新用户昵称时,希望用本次插入的新昵称覆盖旧值:
INSERT INTO t_user (user_id, nickname, updated_at) VALUES (1001, '新昵称', NOW()) ON DUPLICATE KEY UPDATE nickname = VALUES(nickname), updated_at = NOW();
这里的VALUES(nickname)表示取本次INSERT中nickname字段对应的值,即字符串"新昵称"。如果插入时没有冲突,就直接插入这行;如果user_id=1001已经存在,则把nickname更新为"新昵称"。需要注意的是,从MySQL 8.0.20开始,VALUES()函数已被标记为废弃,官方推荐使用新的行别名语法:
INSERT INTO t_user (user_id, nickname, updated_at) VALUES (1001, '新昵称', NOW()) AS new ON DUPLICATE KEY UPDATE nickname = new.nickname, updated_at = NOW();
批量写入是这条语法最实用的场景。比如同步一批商品库存,可以把多条数据拼在一条INSERT里,冲突的自动更新,不冲突的自动插入,一次网络往返就完成全部操作:
INSERT INTO t_stock (sku_id, quantity) VALUES (1001, 50), (1002, 30), (1003, 20) ON DUPLICATE KEY UPDATE quantity = VALUES(quantity);
三、ON DUPLICATE KEY UPDATE与REPLACE INTO的区别
很多初学者容易把这两条语句混为一谈,实际上它们的底层机制完全不同。REPLACE INTO在遇到冲突时,会先DELETE掉旧记录,再INSERT新记录,行的物理数据被删除重建;而ON DUPLICATE KEY UPDATE是原地更新旧记录,不会删除再插入。
这个差异带来的影响主要有三点。第一,自增ID的处理不同:REPLACE INTO删除旧行后插入新行,自增主键会分配新值,导致ID不断变化;而ON DUPLICATE KEY UPDATE保留原有主键。第二,级联和触发器行为不同:REPLACE触发的DELETE操作可能引发外键级联删除或触发器执行,产生意外的副作用。第三,性能不同:REPLACE相当于一删一插两次操作,开销更大,且 binlog 记录的行数更多。
因此在绝大多数"存在即更新"的场景下,应优先使用ON DUPLICATE KEY UPDATE,只有在明确希望"完全替换旧记录、未列出的字段恢复默认值"时才考虑REPLACE INTO。
四、返回值规律与常见踩坑点
这条语句的affected rows返回值有特殊规律:插入新行返回1,更新已有行返回2,如果更新时发现新值与旧值完全相同则返回0。利用这个特性,程序可以判断出实际发生了插入还是更新。但也要小心,如果更新的值与原值相同,affected rows为0,某些框架会误以为操作失败。
使用时还有几个坑需要注意。首先是自增ID空洞问题:即使发生冲突转为更新,InnoDB的自增计数器仍然会先分配一个ID,导致AUTO_INCREMENT值持续增长,高并发下可能很快耗尽int类型的取值范围,建议用BIGINT。其次是死锁风险:批量插入多行时,不同事务加锁顺序不同可能引发死锁,建议保持批量数据的索引顺序一致,或减小批次大小。最后是幂等性问题:如果业务上希望"值没变就不更新",可以在UPDATE部分显式比较旧值,或依赖affected rows为0的特性做判断,避免无意义的写入放大。
掌握ON DUPLICATE KEY UPDATE语法后,配合合适的唯一索引设计,可以优雅地解决大部分"插入或更新"的业务需求,让代码更简洁,也让并发安全得到保障。
ON DUPLICATE KEY UPDATE插入冲突MySQL批量写入修改时间:2026-09-15 09:44:29