应使用 insert ... on duplicate key update 而非 select + insert,因其依赖唯一约束实现原子性操作,避免并发幻读与重复键错误,且无 replace into 的自增id消耗、双触发器等副作用。

MySQL 唯一性检查为什么别总用 SELECT + INSERT
直接查再插(SELECT ... WHERE unique_col = ? 判断是否存在,不存在才 INSERT)在并发场景下必然踩幻读或重复键错误——因为两次操作之间有时间窗口,其他事务可能已插入同值。这不是程序逻辑能靠“加锁”简单规避的,尤其在高并发写入时,性能和正确性都不可靠。
真正高效的做法是:把唯一性检查和写入合并成原子操作,让数据库自己判、自己处理。
INSERT ... ON DUPLICATE KEY UPDATE 是怎么工作的
它依赖表上已有的唯一约束(UNIQUE 索引或 PRIMARY KEY),不是靠语句里写条件。MySQL 在执行 INSERT 时,一旦发现违反唯一约束,就自动转为 UPDATE,整个过程在一行内完成,无竞态。
- 必须确保目标列上有
UNIQUE或PRIMARY KEY约束,否则不会触发ON DUPLICATE KEY UPDATE -
UPDATE部分只能更新当前行的字段,不能引用子查询或跨表数据 - 如果主键/唯一索引由多列组成,只要其中任意一个冲突组合命中,就会触发更新
- 执行后返回的
affected rows:1 表示新插入,2 表示更新(注意:不是所有客户端都一致,PHP mysqli 默认开启MYSQLI_CLIENT_FOUND_ROWS才返回 2;Python MySQLdb / PyMySQL 默认返回 2)
示例:
INSERT INTO user_stats (user_id, login_count, last_login) VALUES (123, 1, NOW()) ON DUPLICATE KEY UPDATE login_count = login_count + 1, last_login = NOW();
ON DUPLICATE KEY UPDATE 和 REPLACE INTO 的关键区别
很多人误以为 REPLACE INTO 更简单,但它本质是 DELETE + INSERT,会引发额外开销和风险:
- 自增 ID 会被消耗(即使最终是“更新”,ID 也已+1)
- 触发器会执行两次(
DELETE和INSERT各一次) - 外键级联操作可能被意外触发
- 如果有多个唯一索引,
REPLACE可能因匹配到不同索引而删除“不该删”的行(比如 A/B 两列各自唯一,REPLACE可能因 B 冲突删掉 A 不同的旧记录)
ON DUPLICATE KEY UPDATE 没有这些副作用:不改变主键、不触发 DELETE 相关逻辑、只更新匹配到的那一行。
实战中容易忽略的细节
这个语句看着简单,但线上出问题往往卡在边界上:
- 唯一索引必须包含所有用于判断“是否重复”的字段——比如你希望按
(user_id, date)组合去重,就必须建联合唯一索引:UNIQUE KEY idx_user_date (user_id, date),只建单列索引没用 - 如果表用的是
utf8mb4_unicode_ci排序规则,字符串比较默认不区分大小写,'ABC'和'abc'会被视为重复,需确认业务是否接受 -
UPDATE子句里慎用函数依赖当前时间(如NOW()),若语句重试(如网络超时后客户端重发),可能导致时间戳被反复覆盖 - 无法用它实现“存在则跳过,不存在才插入”的纯幂等插入(即不更新任何字段),此时应改用
INSERT IGNORE,但要注意它静默失败、不报错也不返回影响行数
唯一性检查这件事,数据库比应用层更懂怎么原子、高效、安全地做——前提是索引建对、语句写准、边界想全。











