真正决定性能的是执行计划中的rows和Extra字段;中间结果集是否膨胀、索引是否生效、select_type是否为DEPENDENT SUBQUERY,比子查询或JOIN语法重要十倍。

别纠结“子查询慢还是JOIN快”,真正决定性能的是执行计划里那几行rows和Extra字段——中间结果集膨胀了没?索引用上了没?select_type是不是DEPENDENT SUBQUERY?这些比语法写法重要十倍。
看到DEPENDENT SUBQUERY就该警觉
这是相关子查询的明确信号,意味着外层每行都会触发一次内层查询。10万用户 × 每次查订单表 → 实际执行10万次子查询,哪怕单次只要5ms,总耗时也逼近500秒。
- 典型写法:
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)或WHERE u.id IN (SELECT user_id FROM orders WHERE o.user_id = u.id) - EXPLAIN里若
id为2、select_type为DEPENDENT SUBQUERY,且table列显示大表(如orders),基本就是性能瓶颈所在 - 修复方向:优先加索引——
orders(user_id)必须存在;若仍卡顿,改用INNER JOIN+DISTINCT或EXISTS(注意EXISTS内层字段也要有索引)
IN子查询在MySQL 5.6+常被优化为semi-join
只要子查询满足“非相关、无GROUP BY、无LIMIT、无ORDER BY”,优化器大概率把它转成半连接,执行路径和INNER JOIN几乎一致,EXPLAIN里能看到<semi-join></semi-join>或FirstMatch。
- 高效前提:
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE status = 'paid'),要求orders.user_id有索引,且status字段也有索引(复合索引更佳) - 一旦子查询带
SELECT DISTINCT或GROUP BY user_id,优化器直接放弃semi-join,退化为逐行物化,性能断崖下跌 - 别盲目替换成JOIN——如果原
IN已走type: range或index_subquery,改完反而可能因驱动表选错变慢
LEFT JOIN后COUNT(*)把NULL也计进去了
想统计每个用户的订单数,写LEFT JOIN orders ON u.id = o.user_id GROUP BY u.id后用COUNT(*),结果所有用户都返回1(含无订单用户),因为COUNT(*)统计的是分组后的行数,不是o.id非空个数。
- 正确写法是
COUNT(o.id),它只统计非NULL值 - 更轻量的替代方案:
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id)—— 这是标量子查询,不膨胀中间结果,适合小结果集场景 - 但注意:如果
orders表极大且user_id无索引,标量子查询会变成嵌套循环全表扫描,比LEFT JOIN还糟
NOT IN遇到NULL直接失效
WHERE id NOT IN (SELECT user_id FROM banned_users)只要子查询结果里有一个NULL,整条语句返回空集——这是SQL标准行为,但90%的人第一次踩坑时都懵了。
- 安全写法只有两个:
LEFT JOIN ... WHERE b.user_id IS NULL或NOT EXISTS (SELECT 1 FROM banned_users b WHERE b.user_id = u.id) -
NOT EXISTS比LEFT JOIN更省内存,因为不生成右表匹配行;但若banned_users.user_id没索引,NOT EXISTS会逐行全表扫 - 千万别用
WHERE id NOT IN (SELECT user_id FROM banned_users WHERE user_id IS NOT NULL)来绕开——WHERE过滤发生在子查询执行之后,IS NOT NULL无法阻止NULL参与NOT IN逻辑
最容易被忽略的不是语法选择,而是EXPLAIN输出里的rows预估是否严重偏离实际——比如预估100行,实际扫描10万行,那无论你写IN、EXISTS还是JOIN,都救不回来。先看执行计划,再调索引,最后才动语法。


















