GROUP BY索引优化:慢查询卡在分组上?试试这几种建索引方法

线上报表页面加载要等七八秒,后台日志里反复刷着一条 SQL:SELECT user_id, COUNT(*) FROM order_log WHERE create_time > '2024-01-01' GROUP BY user_id。查了执行计划,type 是 ALL,rows 扫了 200 多万行——明明只想要几百个用户的汇总结果,却把整张表翻了个底朝天。

为什么 GROUP BY 容易变慢?

MySQL 在执行 GROUP BY 时,如果找不到合适索引,会先排序再分组(Using filesort),或者建临时表(Using temporary)。这两步都是磁盘 IO 密集型操作,数据量一上来,速度就肉眼可见地拖住。

索引怎么建才管用?

关键不是“有没有索引”,而是“索引字段顺序是否匹配 GROUP BY + WHERE 的实际路径”。举个真实例子:

SELECT city, AVG(price) FROM products WHERE status = 1 GROUP BY city;

这张表有 30 万条商品记录,没加索引前执行要 2.8 秒。加了 INDEX(status) 没用,加了 INDEX(city) 也没用。真正起效的是这个组合索引:

ALTER TABLE products ADD INDEX idx_status_city (status, city);

执行时间直接降到 0.09 秒。原因很简单:WHERE 先过滤 status=1,再按 city 分组,索引的最左前缀正好覆盖这两个动作,扫描范围大幅缩小。

再看一个带聚合函数的场景

用户想看每个省份近 30 天的订单金额总和:

SELECT province, SUM(amount) FROM orders WHERE order_time > DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY province;

这时候别急着建 (order_time, province)。因为 order_time 是范围查询(>),它后面的字段 province 在 B+ 树中无法被用于快速定位分组键。更优解是:(province, order_time) —— 先按 province 排好,再在每个 province 内部按时间筛选,GROUP BY 直接利用索引有序性完成,连排序都省了。

别忽略 NULL 和隐式类型转换

有次帮朋友调一个统计接口,GROUP BY 字段是 VARCHAR 类型的 category_code,但 WHERE 条件里写成了 WHERE category_code = 123(没加引号)。MySQL 自动转成数字比较,导致索引失效,分组又回到全表扫描。加上引号、确保类型一致后,响应从 5 秒压到 0.12 秒。

另外,如果 GROUP BY 字段允许 NULL,而业务上绝大多数值非空,建议加个条件排除掉 NULL(比如 WHERE category_code IS NOT NULL),避免索引中大量 NULL 值干扰范围扫描效率。

小技巧:用覆盖索引减少回表

如果 SELECT 的字段都能被索引包含,MySQL 就不用回主键索引捞数据。比如:

SELECT user_id, COUNT(*) FROM logs WHERE log_type = 'login' GROUP BY user_id;

建索引时可以多带上 COUNT 依赖的字段(虽然这里只是计数,但 user_id 本身必须在索引里):

ALTER TABLE logs ADD INDEX idx_type_user (log_type, user_id);

这样整个分组过程都在二级索引里完成,不访问聚簇索引,IO 更少,尤其在大表上效果明显。