子查询在GROUP BY后执行慢是因为MySQL 5.7及更早版本常将其误判为相关子查询反复执行,且聚合子查询易禁用物化而退化为嵌套循环;改用LEFT JOIN+IS NULL需子查询显式DISTINCT或GROUP BY去重,并对JOIN别名字段判NULL;含聚合的子查询须将COUNT等逻辑前置到子查询中;WHERE条件必须下推至子查询内以避免全量计算,实测可从8.2秒降至0.3秒;HAVING条件必须保留在子查询内,不可移至外层WHERE。

子查询在 GROUP BY 后面执行太慢,为什么?
因为 MySQL(尤其是 5.7 及更早版本)对 SELECT ... FROM t1 WHERE x IN (SELECT y FROM t2 GROUP BY z) 这类写法,常把子查询当成“相关子查询”反复执行,哪怕逻辑上可以一次性算完。更糟的是,如果子查询里带 GROUP BY 或聚合函数,优化器大概率放弃物化(materialization),改用嵌套循环,数据量一过万,响应直接卡住。
关键不是“能不能用子查询”,而是“优化器认不认得清依赖关系”。你写的语义是“先分组出结果,再过滤主表”,但数据库看到的是“对主表每一行,都去跑一遍子查询”。
把 IN 子查询改成 LEFT JOIN + IS NULL 怎么写?
适用于“查主表中那些在子查询分组结果里不存在的记录”这类场景,比如:查所有没产生过订单的用户。
- 原写法:
SELECT u.id FROM user u WHERE u.id NOT IN (SELECT user_id FROM order GROUP BY user_id) - 改写后:
SELECT u.id FROM user u LEFT JOIN (SELECT DISTINCT user_id FROM order) o ON u.id = o.user_id WHERE o.user_id IS NULL
注意两点:子查询必须用 DISTINCT 或 GROUP BY 显式去重,否则 LEFT JOIN 会放大主表行数;IS NULL 判断必须落在 JOIN 后的别名字段上,不能写成 o.* IS NULL —— 那语法都不合法。
GROUP BY 前要聚合,JOIN 后怎么保留分组逻辑?
当子查询本身有聚合(如 COUNT(*)、MAX(created_at)),且你需要把这些聚合结果和主表关联时,不能只靠 DISTINCT,得把聚合逻辑提前到子查询里完成。
- 错误示范:
SELECT u.name, o.cnt FROM user u JOIN (SELECT user_id FROM order GROUP BY user_id) o ON u.id = o.user_id—— 这里丢掉了计数信息 - 正确写法:
SELECT u.name, o.cnt FROM user u JOIN (SELECT user_id, COUNT(*) AS cnt FROM order GROUP BY user_id) o ON u.id = o.user_id
这个子查询会被 MySQL 8.0+ 自动物化,执行计划里能看到 Materialize;但在 5.7 上建议给 order(user_id) 加联合索引,否则 GROUP BY 会触发临时表 + filesort。
WHERE 条件下推到子查询里,能快多少?
如果主表或子查询表本身有筛选条件(比如只看“最近30天”的订单),一定要把 WHERE 写进子查询内部,而不是留在外层。否则优化器可能先算全量分组,再过滤,白白浪费 I/O 和内存。
- 低效:
SELECT u.id FROM user u WHERE u.id IN (SELECT user_id FROM order GROUP BY user_id) AND u.status = 'active' - 高效:
SELECT u.id FROM user u WHERE u.id IN (SELECT user_id FROM order WHERE created_at > NOW() - INTERVAL 30 DAY GROUP BY user_id) AND u.status = 'active'
实测过一个 200 万行的订单表:不下推时执行 8.2 秒,下推后降到 0.3 秒。不是所有情况都这么明显,但只要子查询数据量超过主表 10%,就值得检查是否漏了条件下推。
最常被忽略的一点:JOIN 改写后,原来子查询里的 HAVING 条件(比如 HAVING COUNT(*) > 5)必须保留在子查询内,不能挪到外层 WHERE —— 那会改变语义,也可能让索引失效。










