应优先使用 ON DUPLICATE KEY UPDATE:它原地更新、不删不插,避免自增ID跳变、时间戳重置、触发器误执行、外键异常及并发数据丢失;REPLACE INTO 实质是 DELETE+INSERT,副作用大且隐蔽。

别用 REPLACE INTO 解决主键冲突——它不是“覆盖更新”,而是“删了重插”,副作用远大于便利性。
为什么 REPLACE INTO 会导致自增 ID 跳变和时间戳重置
MySQL 执行 REPLACE INTO 时,只要命中主键或任意唯一索引,就会先 DELETE 原记录、再 INSERT 新记录。这意味着:
- 原行的
AUTO_INCREMENTID 被释放,新行获得下一个自增值(哪怕你显式指定了旧 ID) -
created_at、updated_at等默认值字段会重设为当前时间或DEFAULT,无法保留原始创建时间 - 未在
VALUES中显式列出的字段(如status、retry_count)会被设为NULL或类型默认值(0、空字符串等) - 触发器会执行四次:
BEFORE DELETE→AFTER DELETE→BEFORE INSERT→AFTER INSERT
REPLACE INTO 在多唯一索引下可能误删多行
当表同时有主键和多个唯一索引(比如 email 和 phone 都是 UNIQUE),REPLACE INTO 可能匹配到不同行:
- 你插入
(email='a@b.com', phone='123'),但已有 A 行email='a@b.com'、B 行phone='123' - MySQL 会把 A 和 B 两行都
DELETE,再插入新行——这显然不是你想要的“更新某条用户记录” - 这种行为在 binlog 中表现为
DELETE+UPDATE混合事件,主从同步或回放时逻辑更难追溯
并发场景下 REPLACE INTO 会丢数据
两个线程同时对同一主键执行 REPLACE INTO,不加锁就极易覆盖彼此变更:
- 线程 A 想把
status = 'processing';线程 B 想把retry_count = retry_count + 1 - 两者都执行
REPLACE INTO users (id, status, retry_count) VALUES (123, 'processing', 1)(B 忘了带原retry_count值) - B 的语句最终生效,A 设置的
status被覆盖回旧值或默认值 - 这不是竞态条件下的“最终一致”,而是明确的数据丢失
真正该用什么替代 REPLACE INTO
99% 的主键冲突场景,应该用 INSERT ... ON DUPLICATE KEY UPDATE:
- 原地更新,不删不插,
AUTO_INCREMENT不变、时间戳可控、外键安全 - 支持表达式:
counter = counter + 1、updated_at = NOW()、name = IFNULL(VALUES(name), name) - 返回影响行数可区分行为:1=插入,2=更新,0=被 IGNORE(若搭配
INSERT IGNORE) - 高并发下配合
SELECT ... FOR UPDATE可实现强一致性读-改-写流程
例如:INSERT INTO users (id, username, email) VALUES (1, 'alice', 'a@b.com') ON DUPLICATE KEY UPDATE username = VALUES(username), email = VALUES(email), updated_at = NOW();
真正要警惕的不是语法选型,而是表结构本身——如果业务上本应靠自然键(如 email)做唯一约束,却硬塞了个无意义的自增 id 主键,那所有“冲突处理”都在掩盖设计缺陷。


















