相关子查询会导致N次全表扫描,因其每行都需重新执行子查询;应优先改用窗口函数AVG() OVER(PARTITION BY),或降级为派生表JOIN,避免NOT IN、ORDER BY/LIMIT滥用及列作用域混乱。

相关子查询导致N次全表扫描
只要子查询里引用了外层字段,比如 WHERE salary > (SELECT AVG(salary) FROM employees e2 WHERE e2.dept_id = e1.dept_id),MySQL 就不得不为 e1 的每一行重新执行一次子查询。如果主表返回 10 万行,子查询就执行 10 万次——哪怕它本身能走索引,总开销也爆炸。
常见翻车场景:按部门查“薪资高于本部门平均值”的员工、查“最新订单时间晚于用户注册时间”的用户。
- 优先改用窗口函数:
AVG(salary) OVER (PARTITION BY dept_id),MySQL 8.0+ 直接消除循环 - 降级方案用派生表 + JOIN:
SELECT e.* FROM employees e JOIN (SELECT dept_id, AVG(salary) avg_sal FROM employees GROUP BY dept_id) t ON e.dept_id = t.dept_id AND e.salary > t.avg_sal - 别在子查询里写
ORDER BY或LIMIT——它们会强制物化,让优化器彻底放弃下推条件
嵌套过深引发执行计划失控
视图或 CTE 嵌套超过 3 层,MySQL(尤其是 5.7)基本放弃代价估算,表现为 EXPLAIN 出现大量 MATERIALIZE 节点、同一 SQL 两次执行计划完全不同(比如 Hash Join 突然变 Nested Loop),且外层 WHERE 条件无法下推到最内层基表。
典型症状:加了 WHERE id = 123 后仍扫描百万行,EXPLAIN 显示 rows=1000000 而不是 1。
- CTE 不是“套一层就完事”,必须显式声明字段(禁用
SELECT *),否则外层过滤无法穿透 - 避免 CTE 交叉引用:
WITH a AS (...), b AS (SELECT * FROM a)会触发全物化 - 真需要复用中间结果且计算昂贵(比如含
GROUP BY+ 大表 JOIN),直接建临时表:CREATE TEMPORARY TABLE tmp_stats AS SELECT ...,再对关键字段建索引
NOT IN 遇 NULL 或子查询多行直接失效
NOT IN 只要子查询结果里有一个 NULL,整个条件就恒为 FALSE,结果集为空——这不是 bug,是三值逻辑的必然行为。另外,=、!= 等标量比较符遇到多行子查询会直接报错 Subquery returns more than 1 row。
常见误用:查“未下单的用户”写成 WHERE user_id NOT IN (SELECT user_id FROM orders),但 orders.user_id 允许为 NULL。
- 一律改用
NOT EXISTS:WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id),天然规避 NULL 问题 - 需要多值匹配时,明确用
IN;需要单值但不确定是否唯一,先加LIMIT 1或套MAX(),但得确认业务是否允许截断 - 检查子查询是否真该单行:跑
SELECT COUNT(*)看结果行数,比猜更可靠
列作用域混乱引发 Unknown column 错误
嵌套层级一深,MySQL 就找不到字段了,报错 Unknown column 'xxx' in 'WHERE clause'。根本原因不是表没这列,而是 MySQL 查找列名只认当前层级和紧邻上一层,跨两层以上就失效。
比如三层子查询里想引用最外层 users.id,但没加别名,内层根本看不见它。
- 所有被跨层引用的列,必须带表别名:
u.name而非裸写name - 外层和子查询里都用同名列(如
id)时,不加别名必歧义,MySQL 直接报错 - 子查询里别用
*,显式列出字段并重命名冲突项,比如u.id AS user_id
真正卡住性能的往往不是语法错,而是优化器在嵌套结构里彻底“迷路”——它不知道该先过滤还是先连接,也不知道哪些条件能下推。越早扁平化(用 CTE 或临时表),越容易掌控执行路径。


















