一张千万级数据表上执行类似 SELECT user_id, SUM(amount) FROM order_detail GROUP BY user_id 的查询,如果耗时超过几秒,通常不用急着调整数据库参数。绝大多数情况下,慢的根因是这条 SQL 触发了大量回表、构造临时表或者对结果做了文件排序。优化的重点应当放在如何让 GROUP BY 利用索引的有序性,而不是把希望寄托在加大 tmp_table_size 上。

一、GROUP BY 的执行原理与性能瓶颈
MySQL 在执行 GROUP BY 时,通常有两种路径。第一种是借助索引的有序性直接顺序扫描,数据天然按照分组列排列,引擎可以在读取过程中逐组聚合,不需要额外的排序或临时表。第二种是当查询无法利用任何有序索引时,MySQL 会将匹配的行写入内部临时表,再根据分组列进行排序或哈希聚合。对于千万级数据,第二种路径非常昂贵,临时表很可能超过 tmp_table_size 后转成磁盘临时表,排序操作也会占用大量 CPU 和 I/O。因此,分析一条 GROUP BY SQL 时,首先要看执行计划里是否出现 Using temporary 和 Using filesort。
可以通过 EXPLAIN 观察这些信息。假设有一张订单明细表 order_detail,结构如下:
CREATE TABLE order_detail ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, status TINYINT NOT NULL, amount DECIMAL(10,2) NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; SELECT user_id, SUM(amount) AS total_amount FROM order_detail GROUP BY user_id;
此时虽然 user_id 列有索引,但 amount 不在索引中,引擎每读取一个索引条目都需要回表拿到 amount 值。更重要的是,即使走了 idx_user_id 索引,回表后的结果集仍然需要按照 user_id 汇总。优化器可能选择扫描主键全表,再对结果做临时表聚合。实际执行计划中你会看到 type 为 ALL 或 index,Extra 里同时出现 Using temporary 和 Using filesort,rows 接近千万。这种情况下的优化方向非常明确:给分组列和聚合列建立合适的联合索引,让查询变成覆盖索引扫描。
二、用覆盖索引消除临时表与排序
覆盖索引的意思是查询需要的所有列都能在一个索引中找到,不需要回表。对于前面的查询,我们可以创建一个同时包含 user_id 和 amount 的联合索引:
ALTER TABLE order_detail ADD INDEX idx_user_amount (user_id, amount);
添加之后再看执行计划:
EXPLAIN SELECT user_id, SUM(amount) AS total_amount FROM order_detail GROUP BY user_id;
执行计划中的 key 会变为 idx_user_amount,Extra 一栏只剩 Using index,表示覆盖索引扫描,Using temporary 和 Using filesort 都消失了。这是因为索引按照 user_id 排序,同一个用户的 amount 值在索引中连续存放,MySQL 可以沿着索引顺序扫描,每切换一个 user_id 就输出上一个分组的结果。整个过程中不需要创建临时表,也不需要额外排序。对于千万级数据,这个变化通常能把查询时间从十几秒降到几百毫秒甚至更低。
需要注意的是,这里使用的是紧凑索引扫描而不是松散索引扫描。松散索引扫描只对 GROUP BY 的每个分组读取一行,适合 COUNT(DISTINCT) 或 MIN/MAX 等场景。而 SUM(amount) 需要读取组内全部 amount 值,因此会扫描所有索引条目。即便如此,紧凑索引扫描仍然比临时表方案高效得多,因为数据是有序的,不需要额外的随机 I/O 和排序缓冲。
如果查询带有 WHERE 过滤条件,索引的顺序要按过滤字段、分组字段、聚合字段来设计。例如想统计某个时间段内每个用户的订单总额:
SELECT user_id, SUM(amount) AS total_amount FROM order_detail WHERE create_time >= '2025-01-01 00:00:00' AND create_time < '2025-02-01 00:00:00' GROUP BY user_id;
此时优先考虑创建 (create_time, user_id, amount) 这样的联合索引,让 WHERE 条件先通过索引缩小扫描范围,剩余数据在索引中仍然按 user_id 有序排列,聚合过程顺带完成。如果只给 user_id 建索引,过滤 create_time 后数据会被打散,临时表和排序仍会出现。联合索引的列顺序非常关键,必须把等值查询或范围查询的列放在前面,分组列放在中间,聚合列放在最后。最左前缀原则决定了如果跳过 create_time 直接使用 user_id,索引的利用效果会大打折扣。
三、执行计划关键字段与常见误判
优化 GROUP BY 时,一定要养成先看 EXPLAIN 的习惯。EXPLAIN 输出中最重要的几列是 type、key、rows 和 Extra。type 从 system、const、eq_ref、ref、range、index 到 ALL,性能通常由好到差。对千万级表来说,ALL 全表扫描和 index 全索引扫描都可能非常慢,尤其是当 Extra 中出现 Using filesort 时。Using index 代表覆盖索引,是比较理想的状态;Using temporary 表示需要创建临时表,它会拖慢查询且容易触发磁盘写。Using filesort 表示需要额外排序,数据量大时也会成为瓶颈。
常见的一个误区是认为只要分组列有索引,GROUP BY 就一定不会排序。实际上,如果索引只包含分组列,但查询还需要回表取其他列,优化器可能放弃索引,走全表扫描;或者即使走了索引,由于索引不同,分组列的顺序没有在索引中连续出现,仍然会触发临时表。另一个误区是看到 key 不为 NULL 就认为是好计划,但如果 key 是一个很长的二级索引且 Extra 中有 Using filesort,说明虽然用了索引,但排序成本依旧存在,整体性能可能还不如覆盖索引。
MySQL 8.0 的 EXPLAIN ANALYZE 能显示实际执行时间和各步骤成本,比传统 EXPLAIN 更直观。在优化千万级 GROUP BY 时,建议先执行 EXPLAIN FORMAT=JSON 查看是否有 grouping_operation 的临时表信息,再用 EXPLAIN ANALYZE 验证实际耗时。比如下面这条命令可以完整展示执行路径:
EXPLAIN ANALYZE SELECT user_id, SUM(amount) AS total_amount FROM order_detail GROUP BY user_id;
另一个容易被忽略的点是索引的区分度和列宽度。如果分组列的区分度很低,例如 status 只有 0 和 1,那即使加上索引,分组后数据量虽然小,但索引扫描的收益有限,可能更适合作汇总表。此外,索引列宽度越窄越好,过长字符串可以用前缀索引。千万级数据下,索引体积每缩小几十字节,扫描时的 I/O 成本都会成倍下降。
四、从慢查询到毫秒级:完整优化流程
假设读者手头已经有一条跑不动的 GROUP BY SQL,可以按下面的顺序操作。第一步,记录慢日志中的查询时间和执行频率;第二步,用 EXPLAIN 确认当前执行计划,重点看 Using temporary 和 Using filesort;第三步,检查表上现有索引,判断是否存在可以覆盖查询的联合索引;第四步,根据 WHERE 条件、GROUP BY 列和聚合列创建或调整索引;第五步,重新执行 EXPLAIN,确认两个 Using 消失;第六步,实际运行 SQL,对比响应时间和扫描行数。
这里给出一个完整的检查示例。先查看当前索引:
SHOW INDEX FROM order_detail;
然后创建针对查询的覆盖索引:
ALTER TABLE order_detail ADD INDEX idx_create_user_amount (create_time, user_id, amount);
再次执行 EXPLAIN 后,如果 Extra 中只有 Using index condition 或 Using index,且 rows 显著下降,说明索引生效。需要注意,如果用不到覆盖索引,可能仍然会有回表,但只要扫描行数足够少,性能也可以接受。对于确实无法通过索引优化的场景,比如分组字段来自函数计算或跨表连接,可以考虑在业务层做预聚合,比如每天凌晨生成一张汇总表,按 user_id 统计好 SUM(amount),查询时直接读取汇总结果,把千万级扫描变成几十万甚至几万行的精确查询。
总之,千万级数据的 GROUP BY 性能优化,核心不是增加内存或换硬件,而是通过索引设计让查询走有序扫描,避免临时表和文件排序。结合执行计划分析,可以准确判断当前瓶颈在哪里,再针对性地调整索引顺序或改写查询。很多慢 SQL 在加上一个看似简单的联合索引之后,性能就会有数量级的提升。
SQL GROUP BY优化索引执行计划修改时间:2026-10-02 21:42:18