UPDATE慢90%以上因WHERE未走索引导致全表扫描,应先用EXPLAIN分析type(ALL/index)、key(NULL)、rows(近总行数),排查隐式转换、OR条件退化等问题;子查询更新改JOIN并建复合索引;批量更新须分批加LIMIT且每批新事务;避免无效更新和CASE遗漏;临时禁用约束需谨慎;最终以实测为准。

WHERE 条件没走索引,先查执行计划
UPDATE 慢,90% 以上是因为 WHERE 条件没命中索引,导致全表扫描。别急着改 SQL,先用 EXPLAIN 看执行计划:
-
EXPLAIN UPDATE ...(MySQL 8.0+ 支持;低版本可用EXPLAIN SELECT模拟相同条件) - 重点看
type字段:要是ALL或index,基本就是全表或全索引扫描 - 再看
key是否为NULL,rows是否接近表总行数
常见坑:字段类型不一致(比如 device_id 是 varchar,但 WHERE 里传了数字),隐式类型转换会让索引失效;还有 OR 条件中部分字段无索引,整个条件直接退化成全表扫描。
子查询更新太慢,改用 JOIN 替代
像这种写法:UPDATE t1 SET col = (SELECT COUNT(*) FROM t2 WHERE t2.fk = t1.id),本质是“对 t1 每行都执行一次子查询”,数据量一大就崩。人大金仓那个案例里,6.9 万行 × 41 万行扫描,达 284 亿行 —— 不是慢,是灾难。
- 换成
JOIN写法,让数据库一次性做关联计算:UPDATE t1 JOIN (SELECT fk, COUNT(*) cnt FROM t2 WHERE state IN ('02','04') GROUP BY fk) t2 ON t1.id = t2.fk SET t1.port_count = t2.cnt - 务必确保
t2的关联字段(如fk)和过滤字段(如state)上有复合索引,例如:CREATE INDEX idx_fk_state ON t2(fk, state) - 如果
t2是分区表,确认分区裁剪生效(EXPLAIN中partitions只显示实际访问的分区)
批量更新卡住,必须分批加 LIMIT
一次性更新几十万行,不仅慢,还容易锁表、拖垮主从同步、触发 long transaction 告警。MySQL 默认事务下,UPDATE 会持有行锁直到事务结束。
- 用
LIMIT控制单次更新行数:UPDATE t SET status = 'done' WHERE status = 'pending' LIMIT 1000 - 配合循环(应用层或存储过程),每次更新后
SELECT ROW_COUNT()判断是否还有剩余 - 避免在事务里反复执行小
LIMIT更新 —— 每次都提交,否则锁持续累积;更稳妥的是每批开新事务 - 注意:不要在没有
ORDER BY的情况下用LIMIT,否则可能漏更新或重复更新(因 MySQL 不保证无序 LIMIT 的稳定性)
UPDATE 自身写法有冗余,检查是否真需要改
很多慢不是因为数据多,而是 UPDATE 本身在做无意义操作。比如把值设成和原来一样的内容,InnoDB 仍要写 redo、更新二级索引、触发触发器。
- 加显式判断,跳过无效更新:
UPDATE t SET status = 'active' WHERE status = 'pending' AND status != 'active' - 批量 CASE 更新时,确保所有分支覆盖完整,且
WHERE id IN (...)中的 ID 都真实存在,否则CASE默认为NULL,可能意外清空字段 - 临时禁用非必要约束可提速,但仅限维护窗口:
SET FOREIGN_KEY_CHECKS = 0、SET UNIQUE_CHECKS = 0,完事后记得恢复
真正难的不是知道该加索引或分批,而是判断哪条路径在当前数据分布、并发负载、硬件 IO 能力下最稳——比如 SSD 上批量 UPDATE 可能比 JOIN 更快,而 HDD 上恰恰相反。别抄方案,先测 EXPLAIN 和真实耗时。

















