
从一个报错说起给表加了唯一索引之后数据库就开始帮你挡重复数据。但挡归挡问题是它挡人的方式很直接——INSERT 执行到一半撞上冲突直接抛错ERROR 1062 (23000): Duplicate entry testexample.com for key uk_email刚入门时我的第一反应是插入前先查一遍。SELECT一下这条 email 存不存在不存在再INSERT。单线程跑没问题但只要两个请求并发进来就可能同时通过 SELECT 检查、再同时 INSERT——还是炸。先查后插本质上是把检查和写入拆成了两步中间的空隙就是隐患。其实 MySQL 自己就提供了两种撞上冲突怎么办的语法区别只在于冲突发生的那一刻你希望数据库做什么什么都不做跳过这一行 →INSERT IGNORE更新一下已有的那行 →ON DUPLICATE KEY UPDATE下面用一个例子把两种都过一遍。准备一张会冲突的表CREATETABLEusers(idBIGINTAUTO_INCREMENTPRIMARYKEY,emailVARCHAR(64)NOTNULL,nameVARCHAR(64)NOTNULL,last_loginDATETIME,UNIQUEKEYuk_email(email));email 上建了唯一索引顺带一提MySQL 的唯一索引允许多个 NULL 共存所以这里 email 要 NOT NULL否则空值能无限重复插入。INSERT IGNORE撞了就跳过机制正常尝试插入如果因为唯一索引冲突失败就静默跳过这一行——不报错、不插入只是受影响行数为 0。-- 如果 email testexample.com 已存在这条语句静默跳过无任何报错INSERTIGNOREINTOusers(email,name)VALUES(testexample.com,John);关键点必须检查受影响行数。这是判断到底插没插进去的唯一方式。用 Java 写就是看update()的返回值introwsjdbcTemplate.update(INSERT IGNORE INTO users (email, name) VALUES (?, ?),testexample.com,John);if(rows0){// 冲突了email 已存在。记日志或走别的分支log.warn(email 已存在跳过插入);}平时写 CRUD 很少有人看executeUpdate()的返回值用了INSERT IGNORE之后它就是关键情报1 表示插入成功0 表示撞了冲突。一个容易踩的坑IGNORE忽略的不只是重复键错误。字段超长被截断、类型不匹配这类本来会报错的情况也会被降级成警告静默放行。所以它比名字听起来更宽容数据质量要求高的场景要想清楚再用。适用场景幂等写入——批量导入去重、初始化数据、日志记录。这类场景的特点是重复了就算了我不需要知道细节。ON DUPLICATE KEY UPDATE撞了就更新最推荐机制先尝试插入如果冲突就转去 UPDATE 已有的那一行。更新哪些字段完全由你指定——所以这种方式也叫 Upsert存在就更新不存在就插入。-- email 存在则更新 name 和 last_login不存在则插入新记录INSERTINTOusers(email,name,last_login)VALUES(testexample.com,John,NOW())ONDUPLICATEKEYUPDATEnameVALUES(name),last_loginVALUES(last_login);VALUES(name)是个便捷函数代表这条 INSERT 语句里 name 列的值避免把同一个值写两遍。受影响行数1 表示执行了插入2 表示执行了更新。有个细节要知道——如果更新后的值和原来一模一样受影响行数是 0。所以拿返回值判断操作类型时别漏了这种情况。一个VALUES()覆盖不了的场景——自增计数-- 每次执行浏览量 1不存在则插入并置 1存在则 1INSERTINTOarticle_stats(article_id,view_count)VALUES(1001,1)ONDUPLICATEKEYUPDATEview_countview_count1;view_count view_count 1引用的是表里已有的旧值这是VALUES()做不到的计数器类需求只能这么写。想要完全覆盖旧数据怎么办不需要什么特殊语法——把所有字段都写进 UPDATE 子句就行INSERTINTOusers(email,name,last_login)VALUES(testexample.com,Jane,NOW())ONDUPLICATEKEYUPDATEnameVALUES(name),last_loginVALUES(last_login);这样旧行除了唯一键和主键之外的字段全部被新值覆盖效果就是替换而且主键 id 不变、其他表的外键引用不受影响。比物理删除再插入的做法可控得多。版本提醒MySQL 8.0.20 起官方把VALUES(col)标记为废弃推荐用行别名写法INSERTINTOusers(email,name,last_login)VALUES(testexample.com,John,NOW())ASnewONDUPLICATEKEYUPDATEnamenew.name,last_loginnew.last_login;两种写法目前都能跑老项目里VALUES()还随处可见看新教程时别被两套写法搞懵。适用场景用户登录刷新时间、库存计数、配置项更新、覆盖式写入——凡是有则更新、无则插入的需求首选它。两种方案对比方案核心机制冲突时行为受影响行数典型场景关键注意事项INSERT IGNORE尝试插入静默跳过冲突行不插入、不报错0冲突/ 1成功幂等写入、批量导入去重只能靠行数判断结果其他错误也会被静默ON DUPLICATE KEY UPDATE尝试插入更新冲突行指定字段更新指定字段保留其他字段1插入/ 2更新Upsert刷新时间、计数器、覆盖写入需显式指定更新字段灵活性最高怎么选决策指南拿业务需求对着这张决策树走一遍基本就能定插入撞上唯一索引冲突你希望数据库做什么 │ ├─ 冲突了就算了什么都不用做 │ └─ INSERT IGNORE批量导入、初始化数据去重 │ └─ 冲突了要更新数据部分字段或全部覆盖 └─ ON DUPLICATE KEY UPDATE绝大多数 Upsert 场景首选两条核心原则首选ON DUPLICATE KEY UPDATE精确控制更新哪些字段从只更新一个时间戳到覆盖所有字段都能胜任覆盖绝大多数业务场景。需要保证不重复而不需要更新时用INSERT IGNORE它最简单但代价是静默——记得检查受影响行数别让冲突悄悄溜过去。写在最后整理完这两种语法有几点体会受影响行数是个宝。写 CRUD 的时候update()返回值从来没人看但这两种语法全靠它反馈结果。数据库给了你返回值就看你要不要用。唯一索引才是真正挡重复的那道门。两种语法都靠唯一索引触发——没有唯一索引INSERT IGNORE只是普通插入ON DUPLICATE KEY UPDATE的 UPDATE 分支永远不会走到。先想清楚什么算重复哪个字段建唯一索引再想冲突了怎么办选哪种语法顺序不能反。先查后插不是不行是要加锁或者靠唯一索引兜底。并发场景下检查写入必须是一个原子操作才算安全。这两种语法本质上是把冲突处理下沉到数据库层让存储引擎替你做原子性保证——这也是我不再执着于先 SELECT 再 INSERT 的原因。