唯一索引是INSERT ON DUPLICATE KEY UPDATE等机制生效的前提,缺失或错建将导致去重失效;需用SHOW CREATE TABLE确认约束,再通过ALTER TABLE ADD UNIQUE添加单列或复合唯一索引。

唯一索引是底线,不是可选项
没有唯一索引(UNIQUE KEY 或 PRIMARY KEY),任何 INSERT ON DUPLICATE KEY UPDATE、INSERT IGNORE 或乐观锁机制都无效。MySQL 只在检测到唯一约束冲突时才触发更新或忽略逻辑。
常见错误现象:明明写了 ON DUPLICATE KEY UPDATE,但还是插入了重复数据——八成是没建唯一索引,或者建在了错误字段上(比如只对 id 建了主键,但业务要求 phone_no 不能重复,却没给它加 UNIQUE)。
- 用
SHOW CREATE TABLE user;确认目标字段是否真有唯一约束 - 添加唯一索引语句示例:
ALTER TABLE user ADD UNIQUE (phone_no); - 复合唯一索引也有效:
ALTER TABLE order_item ADD UNIQUE (order_id, sku_id);
INSERT ON DUPLICATE KEY UPDATE 是高并发写入的主力方案
它把“查+插/更”压缩成一条原子 SQL,在 InnoDB 中由行级锁保障,比手动事务 + SELECT + IF 判断可靠得多。
性能和语义都优于逐条处理:1 万条数据耗时约 480ms,且天然幂等;而先 SELECT 再判断再 INSERT 的方式,在并发下必然漏判、双写。
- 必须确保
VALUES()引用的是待插入行的值,例如:login_count = login_count + VALUES(login_count) - 批量插入时,
VALUES是按行作用的,不是全局聚合值 - 批量大小控制在 500–1000 条以内,否则可能触发
max_allowed_packet限制或锁升级 - 事务中调用时,注意多行冲突可能引发间隙锁等待,尤其当唯一键非主键时
INSERT IGNORE 和 REPLACE INTO 的适用边界很窄
INSERT IGNORE 只沉默跳过冲突行,不提供更新能力;REPLACE INTO 实质是 DELETE + INSERT,会改变自增 ID、触发两次 binlog、破坏外键引用,且在有多个唯一索引时行为难预测。
它们适合“纯去重插入”且无需保留旧记录状态的场景,比如日志流水表、缓存预热表。但凡涉及计数累加、时间戳更新、状态变更,就该换用 ON DUPLICATE KEY UPDATE。
-
INSERT IGNORE返回的受影响行数包含被忽略的行(值为 0),需用ROW_COUNT()判断实际插入了几行 -
REPLACE INTO在主从复制中可能因DELETE顺序问题导致从库数据不一致 - 两者都不解决“读已提交后又被覆盖”的中间态问题,仅防重复,不保最终状态
乐观锁 version 字段只适用于低冲突率更新场景
乐观锁本质是应用层重试机制,依赖 UPDATE ... WHERE id = ? AND version = ? 影响行为 0 或 1 行来判断是否被抢先修改。它不适合高频写入,因为冲突后重试成本高,还可能无限循环。
真正需要它的,是“读多写少 + 写操作本身较重”的业务,比如用户资料编辑、订单状态推进。而大批量导入、埋点上报、计数器累加这类场景,用 ON DUPLICATE KEY UPDATE 直接原地更新更稳。
- version 字段必须为整型(避免浮点或字符串比较陷阱)
- 每次更新都要显式递增:
version = version + 1,不能靠应用层生成新值 - 首次插入时 version 应设为 1,不能为 NULL 或 0(否则 WHERE 条件失效)
- MyBatis 中需用
<selectKey>或数据库默认值保证 version 初始化
ON DUPLICATE KEY UPDATE 的组合看似简单,但容易在字段选错、批量过大、事务嵌套过深时失效;真正难的不是写对一句 SQL,而是确认那条唯一约束是否真的覆盖了你要防重的业务语义。

















