group by字段必须位于索引最左侧,且非聚合select字段需包含在索引中;等值条件应置于范围条件之前以避免索引截断;count(*)可用窄索引优化,count(字段)则要求该字段在索引中且为not null。

GROUP BY 字段必须出现在索引最左侧
如果查询写成 SELECT user_id, COUNT(*) FROM orders GROUP BY user_id,但表上只有 created_at 单列索引,MySQL 就无法跳过排序直接利用索引完成分组。因为 GROUP BY 依赖字段顺序——索引必须以 user_id 开头,否则优化器大概率放弃使用索引做分组,转而走临时表 + filesort。
实操建议:
- 把
GROUP BY中的字段放在复合索引最左边,哪怕它不是查询中过滤条件的主字段 - 若同时有
WHERE user_id = ? AND status = 'paid',索引应定义为(user_id, status),而不是(status, user_id) - 多个
GROUP BY字段(如GROUP BY region, product_type)需严格按该顺序建索引:(region, product_type)
SELECT 列中的非聚合字段必须包含在索引中
当写 SELECT region, MAX(created_at), COUNT(*) FROM sales GROUP BY region 时,MAX(created_at) 要求引擎能从索引里直接拿到值,否则仍需回表。如果索引只有 (region),created_at 就不在索引覆盖范围内,回表开销显著。
实操建议:
- 把所有非聚合的
SELECT字段加到索引后缀位置,例如(region, created_at) - 注意字段顺序:
GROUP BY字段在前,后续是用于MAX/MIN/AVG的字段,最后才是其他被选中的非聚合列 - 避免把大字段(如
TEXT、长VARCHAR)加进索引——可用STORED GENERATED COLUMN提取关键值再建索引
WHERE 条件和 GROUP BY 共用索引时要注意范围查询截断
WHERE created_at > '2024-01-01' AND status = 'shipped' GROUP BY user_id 这类查询容易误以为建 (status, created_at, user_id) 就行。但 created_at > ? 是范围查询,会中断索引匹配,导致 user_id 部分失效,分组仍要额外排序。
实操建议:
- 把等值条件(
=、IN)放索引前面,范围条件(>、BETWEEN)尽量靠后 - 如果必须用范围条件筛选后再分组,考虑改写:先用子查询或 CTE 过滤出小结果集,再对结果分组
- 检查
EXPLAIN输出里的key_len和Extra字段——若出现Using temporary; Using filesort,说明索引没生效
COUNT(*) 和 COUNT(字段) 的索引策略不同
COUNT(*) 只统计行数,只要索引能提供“行存在性”即可,哪怕是最左前缀匹配;但 COUNT(order_no) 要排除 NULL 值,就必须确保 order_no 在索引中且不允许为 NULL,否则仍需回表判断。
实操建议:
- 对
COUNT(*),一个窄索引如(user_id)就足够支撑分组计数 - 对
COUNT(col),索引必须包含该col,且该列定义为NOT NULL,否则优化器不敢跳过回表 - 如果业务允许,优先用
COUNT(*)—— 它更易被索引覆盖,也更稳定











