优化GROUP BY需让分组直接走索引:索引顺序须严格匹配GROUP BY字段(如GROUP BY dept, role → INDEX(dept, role)),WHERE等值条件置最左,避免函数/表达式、SELECT *及非分组裸字段,并用ORDER BY NULL禁用隐式排序。

报表生成慢,八成卡在 GROUP BY 上——不是逻辑写错了,而是数据库被迫扫全表、建临时表、做文件排序。核心解法就一条:让分组动作直接走索引,不碰磁盘、不建临时表。
为什么EXPLAIN里总出现Using temporary和Using filesort
这两个提示是性能崩溃的明确信号:Using temporary 表示数据库不得不建临时表存中间分组结果;Using filesort 表示它还得额外排序才能完成分组。根本原因是索引没覆盖或没对上。
- 索引字段顺序必须严格匹配
GROUP BY子句顺序,比如GROUP BY dept, role,就得建(dept, role)联合索引,而非(role, dept)或单列索引 -
WHERE条件字段如果和分组字段强相关(如WHERE status = 1 GROUP BY user_id),索引应写成(status, user_id),让过滤和分组一步到位 - 带函数的分组(如
GROUP BY DATE(create_time))必然失效——BTREE 索引不支持表达式匹配,哪怕你给create_time加了索引也没用
SELECT * 和多余字段怎么拖慢GROUP BY
聚合查询里写 SELECT * 是隐形杀手:数据库即使能用索引分组,也得回表查所有字段,放大 I/O;更糟的是,一旦 SELECT 列超出索引覆盖范围,覆盖索引就失效,直接退化成全表扫描。
- 只选真正要的字段,例如统计部门人数,就写
SELECT dept, COUNT(*),别带name、email这类无关列 - 高频聚合查询可建覆盖索引,比如
SELECT dept, status, AVG(salary) FROM emp GROUP BY dept, status,对应索引设为(dept, status, salary) -
COUNT(*)比COUNT(col)更友好——前者不关心 NULL,更容易走索引;后者会跳过 NULL 值,可能触发额外判断逻辑
WHERE过滤比HAVING过滤快一个数量级
HAVING 是分组后才执行的,意味着数据库已经把几百万行分好组、算完聚合值,再拿结果去筛——而 WHERE 是在扫描阶段就砍掉 90% 的数据,两者成本天差地别。
- 错误写法:
GROUP BY user_id HAVING MAX(created_at) > '2024-01-01'—— 先分组再筛,慢 - 正确写法:
WHERE created_at > '2024-01-01' GROUP BY user_id—— 先筛再分组,快 - 对超大表做分页聚合时,别写
GROUP BY ... LIMIT 20 OFFSET 1000,应先用子查询锁定主键范围,再关联聚合,避免每次重算全量分组
松散索引扫描(Loose Index Scan)能省多少事
这是 MySQL 针对 MIN()/MAX() 类聚合的隐藏优化机制:只要索引有序且满足条件,它能跳过大量重复键值,直接定位每个分组的极值,完全绕过临时表。
- 前提很苛刻:必须是 BTREE 索引;分组字段是索引最左前缀;聚合函数只能是
MIN、MAX、COUNT(*)等简单函数;不能有ORDER BY或LIMIT干扰 - 例子:
SELECT c1, MIN(c2) FROM t GROUP BY c1,若索引是(c1, c2),就能触发松散扫描;换成SELECT c1, AVG(c2)就不行 - 别依赖它——它不稳定,版本、统计信息、数据分布稍变就退化回紧凑扫描。把它当红利,不作主路径
真正难的不是知道该建什么索引,而是判断哪些分组字段其实不该出现在报表里——每多一个分组维度,数据膨胀和索引维护成本都指数上升。先砍掉非必要维度,再谈索引,比啥都管用。

















