EXPLAIN无法定位子查询性能瓶颈,因其仅显示DEPENDENT SUBQUERY等笼统标记,不反映调用次数与真实执行路径;应结合SHOW PROFILE、单独测试子查询、检查索引及利用EXPLAIN FORMAT=TREE验证semi-join优化。

为什么 EXPLAIN 看不出子查询卡在哪
因为 MySQL 的 EXPLAIN 对很多子查询(尤其是相关子查询)只显示 DEPENDENT SUBQUERY 或 UNCACHEABLE SUBQUERY 这类笼统标记,不展开内部执行路径。你看到一行 type=ALL + rows=10000,但实际这个子查询被外层 5 万行调用了 5 万次——EXPLAIN 不体现调用频次,只告诉你“单次预估”。
实操建议:
- 先用
SHOW PROFILE或performance_schema.events_statements_history_long查看真实执行耗时分布,确认是否真在子查询上卡住 - 把子查询单独拎出来跑一遍,加
SQL_NO_CACHE避免缓存干扰,观察单次耗时和扫描行数 - 对相关子查询(含外层表字段),检查是否缺失关联字段上的索引,比如
WHERE t2.user_id = t1.id中t2.user_id没索引,就会每次全表扫
把 IN (SELECT ...) 改成 JOIN 为什么不一定快
很多人一见 IN 子查询就条件反射改 JOIN,但 MySQL 8.0.19+ 对 IN 子查询做了 semi-join 优化,自动转成哈希连接或物化表;盲目改写反而可能绕过优化器判断,触发更差的执行路径。
实操建议:
- 先看
EXPLAIN FORMAT=TREE(MySQL 8.0+),确认当前是否已走-> Materialize或-> Hash Semi-Join - 如果子查询结果集小(IN + 物化通常比
JOIN更轻量;反之,外层小、子查询大,JOIN更稳 - 避免写成
LEFT JOIN ... ON ... WHERE right_col IS NULL替代NOT IN—— 若子查询含NULL,语义就错了,且容易误触发全表扫描
EXISTS 和 IN 在 NULL 处理上的坑
IN 遇到子查询返回 NULL 会整个表达式变 UNKNOWN,导致过滤失效;EXISTS 不受 NULL 影响,但若子查询里没写 WHERE 关联条件,会退化成常量真值,查出所有行。
实操建议:
- 用
IN时,子查询末尾加WHERE col IS NOT NULL显式过滤空值 - 用
EXISTS时,必须确保子查询WHERE条件里包含对外层表的引用,比如WHERE t2.ref_id = t1.id,否则就是无意义的全表扫描 - 别依赖
NOT IN做排除逻辑——只要子查询任意一行是NULL,整条记录就被跳过;改用NOT EXISTS更安全
重写子查询时最容易被忽略的执行计划陷阱
重写后 EXPLAIN 显示 type=ref、rows 很小,但线上依然慢——大概率是优化器选错了驱动表,或者统计信息过期导致行数估算严重偏差。
实操建议:
- 强制指定驱动表:用
STRAIGHT_JOIN(仅限JOIN场景),但要先验证小表真能走索引,否则更糟 - 更新统计信息:
ANALYZE TABLE t1, t2;,特别是子查询涉及的表数据变动频繁时 - 检查是否触发了临时表 + 文件排序:
Extra字段出现Using temporary; Using filesort,说明重写后的语句仍无法利用索引完成排序/去重 - 子查询里用了函数(如
DATE(created_at))或计算字段,会导致索引失效——提前在列上建函数索引(MySQL 8.0+)或冗余生成列
真正卡住系统的,往往不是语法多难,而是重写后执行计划看似变好,实则因统计不准或驱动顺序错位,让扫描行数从“可控”变成“爆炸”。盯着 rows 看不如盯着 filtered 和实际 EXPLAIN ANALYZE 输出的 actual rows。











