mysql 5.7中update关联子查询会退化为o(n×m)嵌套循环,因无物化与semi-join优化;必须改用join或分批+临时表,并注意null语义一致性。

UPDATE里用关联子查询等于隐式嵌套循环
MySQL 5.7 及更早版本对 UPDATE 中的关联子查询几乎不做物化优化,执行器会为外层每行触发一次子查询——10 万行主表,就执行 10 万次子查询。这不是“慢一点”,而是复杂度从 O(N + M) 暴涨到 O(N × M)。
常见错误现象:EXPLAIN 看不到执行计划(UPDATE 不支持直接 EXPLAIN),但通过慢日志或 SHOW PROCESSLIST 能观察到长时间处于 Updating 状态;实际耗时远超预估,且 CPU/IO 持续拉满。
- 子查询中
WHERE条件列没索引 → 每次都全表扫描子表 - 子查询含聚合(如
COUNT(*))或ORDER BY + LIMIT→ 每次都重新排序、计算 - 主表本身无索引过滤条件(如
WHERE后没走索引)→ 先全表扫描主表,再逐行查子表
MySQL 5.7 的 UPDATE 关联子查询无法自动转 JOIN
和 SELECT 不同,MySQL 直到 8.0.19+ 才支持 UPDATE ... FROM 语法;5.7 对 UPDATE 中的关联子查询**完全不启用 semi-join 或物化优化**,哪怕子查询逻辑上等价于一个左连接。
这意味着你写:UPDATE orders SET status = (SELECT 'done' FROM shipments s WHERE s.order_id = orders.id),优化器不会把它重写成 UPDATE orders JOIN shipments...,而是老老实实循环执行。
- 即使子查询只返回单值,也逃不过逐行调用开销
-
EXISTS类子查询在UPDATE中同样不被优化,仍属DEPENDENT SUBQUERY - 别指望加
/*+ SEMIJOIN */这类 hint 生效——UPDATE语句不支持优化器提示
真正有效的替代方案只有两个
不是“能改就改”,而是必须选对路径:要么用 JOIN 改写(MySQL 5.7+ 可用),要么分批 + 临时表预聚合。硬套 SELECT 的优化经验到 UPDATE 上大概率翻车。
- 用
JOIN改写时,必须确保连接字段有索引,否则JOIN反而比子查询更慢 - 若子查询含聚合(如最新订单时间),不能直接
JOIN orders o2,得先建派生表:(SELECT user_id, MAX(created_at) AS max_at FROM orders GROUP BY user_id),再关联 - 分批更新时,WHERE 条件必须基于有索引的单调字段(如主键
id),避免LIMIT+ 无序扫描 - 临时表预聚合后,务必在关联字段上建索引(如
CREATE INDEX idx_tmp_uid ON tmp_agg(user_id)),否则 JOIN 仍走全表
NULL 和空结果的处理极易被忽略
关联子查询未匹配时返回 NULL,而 JOIN 默认丢弃无匹配行——这导致业务语义悄然改变。很多线上 bug 就出在这里。
- 原写法:
SET name = (SELECT c.name FROM customers c WHERE c.id = orders.cust_id)→ 无客户时设为NULL - 改成
JOIN后,若不加LEFT JOIN并用COALESCE(c.name, NULL),该行根本不会被更新 - 聚合子查询如
(SELECT SUM(val) FROM logs WHERE user_id = u.id),空组返回NULL;但LEFT JOIN后若没COALESCE(SUM(val), 0),SUM在空组仍为NULL,和原逻辑不一致
最危险的是没人测 NULL 场景,上线后统计数据突然变少、页面显示异常,问题很难归因到 SQL 改写上。










