根本原因是MySQL未走索引分组而被迫全量扫描并使用临时表;需通过EXPLAIN中Extra列是否含Using temporary判断,复合索引须按WHERE→GROUP BY→SELECT顺序创建,并避免函数、覆盖不足及数据倾斜。

千万级表上 GROUP BY 一卡就是几秒甚至几十秒,根本原因不是语法写错了,而是 MySQL 没走索引分组,被迫全量读数据进临时表硬算——EXPLAIN 里只要出现 Using temporary 或 Using filesort,就等于确诊了。
怎么看是不是真走了索引分组?盯 EXTRA 列,不是 key 列
很多人只看 key 显示用了哪个索引,就以为优化成功。错的。哪怕 key 有值、type 是 ref,只要 Extra 里有 Using temporary,说明索引只帮了 WHERE 过滤,没帮上 GROUP BY 分组。
-
rows显示扫描行数远大于实际分组数(比如扫 300 万行,只出 1200 个分组),大概率是过滤没生效或索引顺序不对 -
type是ALL或index(非range/ref),基本等于没走有效索引 -
key_len异常大(比如VARCHAR(255)字段只查前 10 个字符,但key_len显示 765),说明索引定义和查询不匹配
复合索引怎么建才真正生效?必须按 WHERE → GROUP BY → SELECT 顺序
不是把字段堆进 CREATE INDEX 就行。MySQL 的 B+ 树只在“最左前缀匹配”且能按顺序输出分组键时,才支持松散索引扫描(Loose Index Scan)——这才是毫秒级的关键。
- 语句是:
SELECT user_id, COUNT(*) FROM orders WHERE status = 1 AND created_at > '2024-01-01' GROUP BY user_id, product_type;→ 索引应为INDEX(status, created_at, user_id, product_type) - 如果还要
SELECT MAX(amount),就把amount加到末尾,形成覆盖索引,避免回表 - 千万别写成
INDEX(user_id, product_type, status)——WHERE条件无法利用最左前缀,索引直接废掉 - 别在
GROUP BY里用函数:GROUP BY DATE(created_at)这种写法等于自废武功,索引完全失效
为什么加了索引还是慢?可能卡在聚合字段没覆盖或数据倾斜
索引建对了,EXPLAIN 也干净了,但查询仍慢,常见原因是聚合字段本身没被索引覆盖,或者分组键分布极不均匀。
-
SELECT user_id, SUM(amount) FROM orders GROUP BY user_id;只建INDEX(user_id)不够,必须建INDEX(user_id, amount),否则仍要回表取amount值 - 如果
user_id中有少数几个值占了 80% 数据(比如平台自营账号),就会触发严重数据倾斜:所有线程都得等那个最慢的分组归并完,反而比单线程还慢 - 快速识别倾斜键:
SELECT count(*), user_id FROM orders GROUP BY user_id ORDER BY count(*) DESC LIMIT 5; - 对已知热 key 单独处理:
SELECT ... WHERE user_id != 'hot_id' GROUP BY ... UNION ALL SELECT ... WHERE user_id = 'hot_id';
千万级之后光靠索引撑不住,必须引入预聚合
索引能减少扫描和排序开销,但改变不了“必须读取所有匹配行”的本质。比如统计某天全部订单金额总和,即使 created_at 有索引,MySQL 仍要遍历当天全部百万行去累加 amount。
- 高频固定维度(如按天/按用户/按商品类目):建预聚合表,每分钟或每小时跑一次
INSERT INTO agg_daily SELECT ..., time_bucket('1 day', pay_time), COUNT(*), SUM(price) FROM order_info WHERE pay_time >= ... GROUP BY ... - 低频灵活查询(如按小时分析某周内异常流量):先用范围条件大幅缩小数据量,再分组,例如
WHERE pay_time >= '2026-08-01' AND pay_time - 别在 OLTP 主库上直接建复杂聚合视图——
SELECT COUNT(*) FROM view_name很可能触发底层全表扫描,压垮主库











