INSERT ... ON DUPLICATE KEY UPDATE 是 MySQL 批量更新首选,需确保唯一索引存在且正确使用 VALUES();大批量宜用 UPDATE JOIN 临时表;REPLACE INTO 有隐式删插风险;百万级必须分批+事务控制。

MySQL 用 INSERT ... ON DUPLICATE KEY UPDATE 做集合更新最稳妥
当你要根据主键或唯一索引批量更新一批记录(比如同步用户状态、刷新商品库存),INSERT ... ON DUPLICATE KEY UPDATE 是 MySQL 原生支持、原子性强、性能好的首选。它本质是“有则更新,无则插入”,但只要确保目标表的 WHERE 条件能命中唯一约束(如 id 或 UNIQUE KEY),就能当纯更新用。
常见错误是没建好唯一索引,导致语句变成批量插入而非更新;或者把 ON DUPLICATE KEY 写成 ON CONFLICT(那是 PostgreSQL 的语法)。
- 必须提前在要匹配的字段上建
UNIQUE或PRIMARY KEY,否则不触发更新逻辑 - 更新字段不能写成
SET col = VALUES(col)就完事——VALUES(col)指的是本次INSERT尝试插入的值,不是原值 - 如果只想更新、不想插入新行,可在
INSERT部分故意让非唯一字段违反约束(比如设一个不可能的status = -1),但这属于 hack,不推荐
INSERT INTO user_status (id, last_login, status)
VALUES (101, '2024-06-01 10:00:00', 1),
(102, '2024-06-01 10:05:00', 0),
(103, '2024-06-01 10:10:00', 1)
ON DUPLICATE KEY UPDATE
last_login = VALUES(last_login),
status = VALUES(status);
大批量更新别硬扛,用 UPDATE ... JOIN + 临时表拆解
当要更新几万甚至几十万行,且条件复杂(比如按另一个表的聚合结果更新),直接写 UPDATE ... WHERE id IN (...) 容易触发锁表、超时或内存溢出。UPDATE ... JOIN 配合临时表是更可控的方式。
关键点在于:临时表必须有索引(尤其是被 JOIN 的字段),否则性能会断崖式下跌;临时表用 CREATE TEMPORARY TABLE,会话结束自动清理,比普通表安全。
- 不要在
UPDATE中嵌套子查询返回大量数据,MySQL 5.7 及以前版本可能生成派生表并全表扫描 -
JOIN的顺序影响执行计划——把小结果集(如临时表)放左边,大表放右边,优化器更容易走索引 - 更新前先
SELECT验证临时表数据是否符合预期,避免误更新
CREATE TEMPORARY TABLE tmp_update AS SELECT user_id, MAX(login_time) as latest_login FROM login_log WHERE log_date >= '2024-06-01' GROUP BY user_id; ALTER TABLE tmp_update ADD PRIMARY KEY (user_id); UPDATE users u JOIN tmp_update t ON u.id = t.user_id SET u.last_active = t.latest_login;
REPLACE INTO 看似简单,但会隐式删再插,慎用
REPLACE INTO 在遇到唯一键冲突时,会先 DELETE 原行再 INSERT 新行。这会导致自增 ID 跳变、触发器重复执行、外键级联行为异常,还可能放大 binlog 体积。
除非你明确需要重置整行(包括未在语句中指定的字段为默认值),否则它不是 ON DUPLICATE KEY UPDATE 的替代方案,而是不同语义的操作。
- 如果表有
AUTO_INCREMENT主键,REPLACE后该 ID 一定会变,下游依赖主键的缓存或日志可能失效 - 如果行上有
ON DELETE CASCADE的外键子表,REPLACE会触发一次删除+插入,子表对应行被删又重建 - 无法只更新部分字段——没出现在
REPLACE语句里的字段会被设为默认值或NULL
真正的大批量(百万+),考虑分批次 + LIMIT + 事务控制
无论用哪种 SQL 方式,单次更新超过 10 万行都容易触发 lock_wait_timeout、撑爆 innodb_buffer_pool 或阻塞复制。必须主动切片。
核心不是“怎么写一条牛逼 SQL”,而是“怎么让每条 SQL 都轻量、可中断、可重试”。用 LIMIT 分批是底线,配合 WHERE id > ? 游标推进比 OFFSET 更稳定。
- 每次更新后加
SELECT ROW_COUNT()判断是否还有数据,避免死循环 - 每批用独立事务,失败只回滚当前批,不影响整体进度
- 应用层控制间隔(比如
SLEEP(0.1)),减轻主库压力,也方便运维随时暂停
START TRANSACTION; UPDATE orders SET status = 3 WHERE id BETWEEN 100001 AND 101000 AND status = 1; COMMIT;MySQL 批量更新真正的难点不在语法,而在对唯一约束的依赖是否清晰、对锁范围和事务粒度是否有预判。写完语句后,务必用
EXPLAIN 看执行计划,确认走了索引;上线前在从库或测试环境压测锁等待时间。










