MySQL的UPDATE语句必须用带索引字段的WHERE条件限定范围,否则触发全表扫描和整表锁;避免函数操作索引字段,应改用范围查询。

用 WHERE 条件限定更新范围,否则就是全表扫描
MySQL 的 UPDATE 语句如果没有 WHERE 子句,或 WHERE 中的字段没索引,就会触发全表扫描——每行都读出来判断是否匹配,再逐行更新。这不仅慢,还容易锁住整张表(尤其在 READ-COMMITTED 或更低隔离级别下可能引发间隙锁)。
实操建议:
- 确认
WHERE字段已建索引,比如UPDATE users SET status = 1 WHERE id = 123要求id是主键或有索引; - 避免用函数包裹索引字段,如
WHERE YEAR(created_at) = 2024会让索引失效,改用范围查询:WHERE created_at >= '2024-01-01' AND created_at ; - 批量更新时别用单条
UPDATE循环,改用INSERT ... ON DUPLICATE KEY UPDATE或临时表 JOIN 更新。
慎用 UPDATE 没有主键/唯一键的表
如果表没有主键或唯一索引,MySQL 在执行 UPDATE 时无法高效定位记录,优化器可能退化为全表扫描+临时表排序,甚至在某些版本中强制使用 ALL 访问类型。这种情况在 ALTER TABLE 后忘记补主键、或日志类宽表上特别常见。
实操建议:
- 用
EXPLAIN FORMAT=TRADITIONAL UPDATE ...查看执行计划,确认key列是否为NULL或显示ALL; - 对无主键表,优先添加自增
id或业务唯一键(如order_no+tenant_id组合); - 实在不能加索引的场景(如归档历史表),考虑先
CREATE TEMPORARY TABLE导出目标行,再用主键 JOIN 更新原表。
避免在 UPDATE 中调用子查询或函数导致重复计算
像 UPDATE t1 SET col = (SELECT MAX(x) FROM t2 WHERE t2.id = t1.id) 这类写法,MySQL 可能对每一行都重新执行一次子查询,性能随数据量指数下降。同样,UPDATE t SET ts = NOW() 看似简单,但若表大、并发高,时间函数本身不是瓶颈,锁竞争才是。
实操建议:
- 把子查询结果先存入临时表,再用
JOIN更新:CREATE TEMPORARY TABLE tmp_max AS SELECT id, MAX(x) mx FROM t2 GROUP BY id;
UPDATE t1 JOIN tmp_max USING(id) SET t1.col = tmp_max.mx; - 用常量代替每次求值的函数,如提前算好时间戳:
SET @now := NOW(); UPDATE t SET ts = @now;; - 注意
UPDATE ... LIMIT N能缓解锁等待,但不解决扫描开销——它只是限制更新行数,不代表扫描行数减少。
批量更新时控制事务大小和锁粒度
一次更新几百万行,即使有索引,也可能因事务过长导致 undo log 膨胀、主从延迟、锁等待超时(Lock wait timeout exceeded)。InnoDB 行锁在没有索引时会升级为表锁,而大事务还会阻塞 DDL。
实操建议:
- 按主键分段更新,例如:
UPDATE t SET status=1 WHERE id BETWEEN 10000 AND 19999 AND status=0;,每次 1w 行,循环执行; - 显式加
COMMIT,避免隐式事务累积; - 监控
INFORMATION_SCHEMA.INNODB_TRX中的trx_rows_locked和trx_lock_structs,判断是否锁了过多行; - 低峰期操作,或设置
innodb_lock_wait_timeout为更短值(如 10 秒),让失败更快暴露。











