用 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;<br>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 秒),让失败更快暴露。 真正卡住性能的往往不是 SQL 写法本身,而是没意识到“看似只改一行”的语句背后触发了多少行扫描、多少行加锁、多少次磁盘 IO。索引是否覆盖、事务是否可控、执行计划是否被误判——这些细节比语法糖重要得多。
