GROUP BY子查询慢主因是EXPLAIN出现Using temporary,表明走全表扫描+临时表路径;须建联合索引(WHERE字段前置、GROUP BY字段居中、非聚合字段覆盖),并避免函数分组或索引顺序错位。

GROUP BY子查询为什么慢?先看EXPLAIN里的Using temporary
只要EXPLAIN的Extra列出现Using temporary,基本就能断定这个子查询在走全表扫描+内存/磁盘临时表路径。尤其当子查询里还嵌套了GROUP BY,MySQL没法复用索引天然分组,只能边扫边建临时结构——数据量过十万,响应就明显卡顿。
常见诱因包括:
– 子查询的GROUP BY字段没索引,或索引不是联合索引
– 外层WHERE条件和子查询GROUP BY字段不共用同一索引
– 子查询SELECT里包含非分组字段,但这些字段没被索引覆盖
把子查询改写成JOIN关联,避免临时表生成
多数情况下,带GROUP BY的子查询(尤其是相关子查询)可以重构成JOIN,让优化器有机会走索引驱动表+顺序扫描分组。关键点是把子查询逻辑“外提”到主查询中,并确保驱动表有合适的过滤条件。
- 原写法(低效):
SELECT a.id, (SELECT COUNT(*) FROM orders o WHERE o.user_id = a.id GROUP BY o.user_id) cnt FROM users a; - 改写后(高效):
SELECT a.id, COALESCE(o.cnt, 0) cnt FROM users a LEFT JOIN (SELECT user_id, COUNT(*) cnt FROM orders GROUP BY user_id) o ON o.user_id = a.id; - 注意:子查询部分
GROUP BY user_id必须有索引(如INDEX idx_user_id (user_id)),否则内层依然会Using temporary - 如果外层
users表本身有时间范围限制(如WHERE created_at > '2025-01-01'),建议先用子查询过滤再JOIN,避免驱动过大
实在要保留子查询,必须给GROUP BY字段建联合索引
若业务逻辑强依赖子查询结构(比如权限校验、动态聚合),不能重写为JOIN,那就必须从索引入手。重点不是单列索引,而是按「WHERE条件字段 + GROUP BY字段 + SELECT非聚合字段」顺序建联合索引。
- 例如子查询为:
SELECT user_id, MAX(created_at), AVG(amount) FROM orders WHERE status = 'completed' GROUP BY user_id - 对应索引应为:
ALTER TABLE orders ADD INDEX idx_status_uid_ca_am (status, user_id, created_at, amount); - 索引顺序不能颠倒:
user_id必须紧跟status之后,否则无法用于GROUP BY user_id;created_at和amount必须包含,否则仍需回表,可能触发Using temporary - 避免在
GROUP BY字段上用函数:GROUP BY DATE(created_at)会让整个索引失效
临时表本身也要加索引,别只盯主表
当子查询结果集较大、MySQL被迫落地为磁盘临时表时(SHOW VARIABLES LIKE 'tmp_table_size'和max_heap_table_size太小),临时表默认无索引。这时即使主表索引再好,中间结果也白搭。
- 显式创建命名临时表并加索引:
CREATE TEMPORARY TABLE tmp_orders AS SELECT user_id, COUNT(*) cnt FROM orders WHERE status = 'completed' GROUP BY user_id; ALTER TABLE tmp_orders ADD INDEX idx_user_id (user_id); - 更稳妥的做法是:用
CREATE TABLE ... SELECT建普通表替代临时表,加上主键或唯一索引,再参与后续JOIN - 注意:
tmp_table_size调太高可能挤占其他连接内存,建议结合SHOW STATUS LIKE 'Created_tmp%'监控实际使用量再调整
最常被忽略的是索引顺序与GROUP BY字段顺序的严格一致性——差一个位置,索引就完全失效。不要假设“有索引就行”,得看它能不能让MySQL顺着B+树叶子节点天然分组。


















