最稳妥的批量更新方式是insert ... on duplicate key update,前提是表有主键或唯一索引;否则优先选update ... join配合临时表,因其避免逐条update的高网络开销、频繁日志写入与行锁竞争,实测1000条数据耗时从800ms降至45ms,且能有效规避长事务与锁阻塞问题。

直接用 INSERT ... ON DUPLICATE KEY UPDATE 是最稳妥的批量更新方式,前提是表有主键或唯一索引;否则优先选 UPDATE ... JOIN 配合临时表。
为什么不能循环执行单条 UPDATE
每条 UPDATE 都是一次独立网络往返 + 一次事务日志写入 + 一次行锁获取。1000 条记录就是 1000 次 round-trip,哪怕在局域网也容易卡住。更关键的是:MySQL 默认会对被更新的每一行加 SELECT FOR UPDATE 类似锁,长时间未提交会阻塞其他读写。
- 实测:500 行循环更新耗时 ≈ 800ms;同一数据用
INSERT ... ON DUPLICATE KEY UPDATE耗时 ≈ 45ms - MyBatis 的
<foreach></foreach>标签生成多条UPDATE语句,本质仍是逐条发送,不是真批量 - JDBC 的
addBatch()+executeBatch()可缓解,但不如一条 SQL 原生高效
INSERT ... ON DUPLICATE KEY UPDATE 的坑与写法
这是有唯一约束场景下的首选,但它不是“纯更新”,而是“插入失败时更新”。必须确保冲突能真正触发,否则会静默插入新行。
MySQL 9.6.0是面向Linux平台的2026年创新版本,核心架构迎来重大革新。其将外键约束与级联操作从InnoDB引擎层上移至SQL层,确保所有数据变更均被完整记录至Binlog,彻底解决了CDC(变更数据捕获)与主从复制中的数据不一致难题。此外,该版本引入container_aware启动选项以原生适配容器环境,并对审计日志进行了组件化重构,为追求极致数据一致性与云原生体验的开发者提供了全新选择。
- 错误写法:
INSERT INTO users (id, name) VALUES (1,'a') ON DUPLICATE KEY UPDATE name='b'—— 如果id是主键但值 1 不存在,就会插入一行(1,'b'),而非你预期的“只更新” - 正确做法:确保传入的
id全部已存在,或加WHERE id IN (...)过滤(但注意该WHERE是作用于插入阶段,不是更新阶段) -
VALUES(col)引用的是当前VALUES子句里的值,不是原表字段值;想保留原值需显式写col = IFNULL(VALUES(col), col) - 单次建议不超过 1000 行,避免触发
max_allowed_packet限制或长事务锁表
没唯一键时用 UPDATE JOIN + 临时表
当表只有普通索引、或需要根据外部计算结果更新时,UPDATE ... JOIN 更可控,且不依赖冲突机制。
- 必须对 JOIN 条件列建索引,比如临时表
tmp_updates(id)—— 否则 MySQL 很可能全表扫描右表,性能暴跌 - 临时表要声明
PRIMARY KEY或UNIQUE,否则优化器可能放弃使用索引 - 权限注意:
CREATE TEMPORARY TABLE需要对应数据库用户有该权限,线上环境常被禁用 - 替代方案:用子查询代替临时表,但子查询若无别名或未加
FORCE INDEX,同样容易慢
超大表(千万级以上)必须分片提交
一次性更新 10 万行,事务日志、undo 空间、锁持有时间都会陡增。生产环境出问题往往不是语法错,而是没控制粒度。
- 推荐每批 500–5000 行,具体看单行数据大小和服务器配置;可用
LIMIT+WHERE id > ?分页推进 - 每次更新后
COMMIT,避免长事务拖垮主从同步或触发Lock wait timeout exceeded - 不要用
REPLACE INTO:它本质是DELETE + INSERT,缺失字段会被重置为默认值,极易误删数据 - 如果字段要全量覆盖(比如导入清洗后数据),先
CREATE TEMPORARY TABLE载入,再UPDATE ... JOIN,比反复解析字符串安全得多
最容易被忽略的是锁范围和事务边界——很多“慢更新”问题,根源不在 SQL 写法,而在没把大操作切成小事务,也没确认唯一索引是否真生效。










