GROUP BY 查询慢主因是未触发松散索引扫描(Loose Index Scan),导致全表扫描并创建临时表;需满足单表、GROUP BY字段为同一BTREE索引最左前缀、无范围条件干扰、聚合函数限于MIN/MAX等严格条件,方可跳过组内冗余行、仅读每组首键高效分组。

GROUP BY 查询慢,八成不是写法问题,而是没让 MySQL 走索引分组——直接全表扫描建临时表,数据一过百万,查询就卡在 Creating tmp table 状态。
为什么 GROUP BY 会变慢
MySQL 执行 GROUP BY 时,默认策略是:先读全表,再往内存或磁盘临时表里按分组键“归堆”,最后逐组算 COUNT()、SUM()。这个过程不走索引时,EXPLAIN 里会出现 Using temporary; Using filesort,意味着性能瓶颈已出现。
- 临时表体积爆炸:SELECT 中带
TEXT、BLOB或 JOIN 大表的非必要字段,会让每组中间结果膨胀数倍 - 隐式排序失效:MySQL 8.0+ 已取消
GROUP BY自动排序行为,但很多人还依赖它,结果加了ORDER BY又多一次排序 - WHERE 条件没前置:比如
WHERE created_at >= '2024-01-01' AND status = 1 GROUP BY user_id,若没索引覆盖全部条件,MySQL 可能先分组再过滤,白算
用 Loose Index Scan 避免临时表
核心目标是让 MySQL 直接通过 BTREE 索引“跳着读”分组键,而不是把所有行拉出来再归堆。这叫 Loose Index Scan,只读取每个分组的第一个索引项,效率提升明显。
- 必须满足:所有
GROUP BY字段都来自同一个 BTREE 索引,且顺序一致(如GROUP BY a, b需索引(a, b),不能是(b, a)) - WHERE 条件要是该索引的最左前缀范围查询(如索引
(region, city, created_at),则WHERE region = 'CN' AND city LIKE 'Sh%'可触发 Loose Scan;但WHERE city = 'Shanghai'就不行) -
EXPLAIN中看到type: index且没有Using temporary,说明生效了
示例:对订单表按用户分组统计,添加复合索引:ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, created_at);,再执行 SELECT user_id, COUNT(*) FROM orders WHERE status = 1 AND created_at > '2025-01-01' GROUP BY user_id;,大概率走 Loose Scan。
HAVING 不要当 WHERE 用
HAVING 是在分组完成之后才执行的,无法利用索引加速,且必须等所有组计算完才能过滤。把它当 WHERE 用,等于把本可提前筛掉的百万行,硬拖进分组流程。
- 错误写法:
SELECT user_id, SUM(amount) FROM orders GROUP BY user_id HAVING created_at > '2025-01-01'——created_at是行级字段,HAVING阶段已不可见,语法报错 - 更隐蔽的错误:
SELECT user_id, COUNT(*) c FROM orders GROUP BY user_id HAVING c > 100看似合理,但如果 90% 用户订单数 ≤ 100,那 MySQL 还是得为这 90% 分组并计数,纯浪费 - 正确做法:把能下推到行级的条件全塞进
WHERE,比如时间范围、状态码、ID 区间等;HAVING只留真正需要聚合后判断的逻辑,如HAVING AVG(price) > 500
别在 SELECT 列表里塞大字段
一旦用了 GROUP BY,MySQL 必须为每组暂存完整行数据(哪怕你只想要 COUNT(*)),如果 SELECT 里有 TEXT 字段或 JOIN 了用户表带头像字段,临时表体积会指数级增长,极易触发 Created_tmp_disk_tables。
- 检查
SHOW STATUS LIKE 'Created_tmp%';,若Created_tmp_disk_tables持续上升,就是临时表撑爆内存了 - 避免
SELECT *或SELECT u.*, COUNT(o.id)这类写法;只选分组键和聚合结果,如SELECT user_id, COUNT(*) FROM orders GROUP BY user_id - 真要关联其他字段(如用户名),用子查询或
JOIN后再GROUP BY,但确保JOIN条件能走索引,且关联表不引入重复行
最常被忽略的一点:GROUP BY 的字段顺序,决定了索引是否能复用、结果是否天然有序。如果业务要求按 department, job_title 分组,却建了 (job_title, department) 索引,Loose Scan 就失效了——顺序错,一切白搭。


















