大表 group by 慢的根本原因是执行路径低效,explain 的 extra 列出现 using temporary 或 using filesort 即表明索引未参与分组;需按 where→group by→select 顺序设计联合索引,避免函数、前缀索引不匹配及统计信息过期等问题。

大表 GROUP BY 慢,不是因为 SQL 写得不够“优雅”,而是数据库被迫全量读、排序、建临时表——EXPLAIN 里一旦出现 Using temporary 或 Using filesort,就说明索引根本没参与分组过程,优化必须从执行路径下手。
看懂 EXPLAIN 的 Extra 列才是关键
别只盯着 key 是否有值,Extra 才是判决书:Using temporary = MySQL 拿不到有序分组键,只能把所有匹配行捞进内存/磁盘临时表再硬分;Using filesort = 分组字段没被索引天然排序,还得额外排序。哪怕 key 显示用了索引、type 是 range,只要 Extra 有这两个词,性能就已失控。
-
rows显示扫描 50 万行,但GROUP BY后只返回 200 行?说明过滤太晚,数据在分组前没被有效收缩 -
key_len异常大(比如VARCHAR(255)字段只查前缀却显示 765)→ 索引定义和查询条件不匹配,可能用了前缀索引但 WHERE 没走最左前缀 - 统计信息过期也会误导优化器,
ANALYZE TABLE orders是低成本必做项
联合索引顺序必须严格匹配 WHERE → GROUP BY → SELECT
索引不是字段堆砌,MySQL 的松散索引扫描(Loose Index Scan)只在 B+ 树能按顺序输出分组键时生效。错误示例:INDEX(user_id, status) 配合 WHERE status = 1 GROUP BY user_id —— status 不是最左,索引直接失效。
- 正确顺序:高区分度
WHERE字段放最左(如status),范围条件居中(如created_at),GROUP BY字段紧随(如user_id, product_type) - 若
SELECT包含MAX(amount),把amount加到索引末尾,形成覆盖索引,避免回表 - 多列分组时,
GROUP BY字段顺序必须和索引定义完全一致(GROUP BY region, city就不能靠INDEX(city, region))
别在 GROUP BY 或 WHERE 里用函数
GROUP BY DATE(created_at) 这种写法等于主动放弃索引——函数会让所有索引失效,MySQL 只能全表扫描后逐行计算日期再分组。这不是“可能慢”,是“必然触发 Using temporary”。
- 高频固定粒度(如按天):加冗余字段
created_date DATE,建索引INDEX(created_date, user_id),查询改用WHERE created_date = '2024-06-01' GROUP BY created_date - 低频灵活需求(如按小时):先用范围缩小数据量,
WHERE created_at >= '2024-06-01' AND created_at ,再 <code>GROUP BY HOUR(created_at),至少砍掉 90% 数据 - MySQL 5.7 不支持函数索引,
INDEX(DATE(created_at))完全无效;8.0+ 虽支持,但DATE()索引仍无法用于GROUP BY场景
当索引也救不了时,换数据组织方式
千万级以后,单靠索引收效递减。此时真正有效的不是“怎么写 SQL”,而是“让 SQL 不用算”。预聚合、分区裁剪、CTE 提前收口,本质都是把计算压力从查询时转移到写入或后台。
- 按天/小时建汇总宽表(如
orders_daily_summary),写入端异步更新,查询直读,性能提升百倍以上 - 时间分区表 +
WHERE dt BETWEEN→ 引擎自动裁剪分区,GROUP BY只在几个 GB 小分区上跑,而非几十 TB 全表 - 跨表关联大明细(如
ordersJOINorder_items):用 CTE 先在order_items上GROUP BY order_id聚出sum_amount,主查询再 JOIN,避免笛卡尔膨胀
最容易被忽略的点:GROUP BY 基数(分组数量)过高会直接压垮内存,哪怕索引全命中。业务上提前排除 NULL、测试数据、低频值,比调优 SQL 更立竿见影。










