GROUP BY 查询慢八成因未走索引或索引错误;EXPLAIN中出现Using temporary或Using filesort即确诊,根本原因是索引未覆盖WHERE、GROUP BY及SELECT字段全链路,或顺序/类型不匹配。

GROUP BY 查询慢,八成是没走索引或走了错误的索引;直接看 EXPLAIN 输出里有没有 Using temporary 或 Using filesort,有就是病根。
为什么加了索引 GROUP BY 还慢?
常见错觉:只要 GROUP BY 字段上有单列索引就万事大吉。实际不是。
- MySQL 无法用单列索引加速
SELECT user_id, SUM(amount) FROM t GROUP BY user_id—— 它仍要回表读amount值,触发随机 I/O - 真正起效的是覆盖索引:
INDEX (user_id, amount),让聚合在索引内完成,不查数据行 - 如果还有
WHERE status = 'paid',索引必须是(status, user_id, amount),且顺序不能乱(最左前缀原则) - 索引字段类型必须和查询中完全一致,比如
status是VARCHAR(20),但 WHERE 里传了'paid '(带空格),会隐式转换,索引失效
ORDER BY NULL 到底要不要加?
要加,除非你真需要排序结果。MySQL 默认对 GROUP BY 结果做隐式排序,哪怕你没写 ORDER BY。
- 加
ORDER BY NULL能跳过这步,避免Using filesort - 示例:
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id ORDER BY NULL - 注意:如果语句里同时有
ORDER BY user_id,这个排序无法被索引覆盖(除非索引是(user_id)且无其他条件),ORDER BY NULL就失去意义 - PostgreSQL 不默认排序,但加了
ORDER BY后又没索引匹配,一样会外排(Sort Method: external merge)
高基数分组(如 UUID、手机号)怎么救?
直接 GROUP BY phone 几乎必然触发磁盘临时表,tmp_table_size 再调大也治标不治本。
- 先归类降维:把手机号转成号段(
FLOOR(phone / 10000))或运营商(用字典表 JOIN 后取carrier_code) - 字符串别拼接分组:
GROUP BY CONCAT(first_name, ' ', last_name)既没法建索引,又吃 CPU - 用预计算列 + 索引替代:
ALTER TABLE user ADD COLUMN full_name_hash BIGINT AS (CRC32(CONCAT(first_name, last_name))) STORED,再对full_name_hash分组 - 去重统计优先用
COUNT(DISTINCT)而非SELECT COUNT(*) FROM (SELECT DISTINCT ...),前者可能走松散索引扫描,后者必全扫
亿级表还靠索引硬扛?别赌了
索引能减少扫描行数,但改不了“得把所有匹配行都读一遍”的事实。某天订单量涨到 5000 万,WHERE created_at > '2026-09-01' 即使走索引,仍要读几十万行再哈希分组。
- 分区表按天/月切分后,MySQL 可以只扫目标分区,比全表快 5–10 倍
- 物化汇总表(如每小时跑一次
INSERT INTO daily_order_stats SELECT DATE(created_at), user_id, COUNT(*), SUM(amount) FROM orders WHERE created_at >= ? GROUP BY ...)让查询退化为简单主键查找 - 实时性要求低的报表,直接读物化视图或 ClickHouse/StarRocks 这类 OLAP 引擎,别让 MySQL 干聚合的活
最容易被忽略的点:执行计划里 rows 和 filtered 的乘积是否接近真实扫描行数?如果不符,先 ANALYZE TABLE 更新统计信息——索引再好,优化器算错了基数,照样选错执行路径。


















