MySQL GROUP BY有两种聚合路径:streaming aggregation依赖索引有序性边读边算,需满足单表、分组列为驱动表索引最左前缀且无范围WHERE条件;否则退化为tmp table grouping,建临时表并用哈希索引加速分组。

MySQL执行器对GROUP BY的两种聚合路径:streaming vs tmp table
MySQL执行器处理GROUP BY时,根本不存在“边读边算”的理想流式聚合。所谓streaming aggregation,只是在特定索引条件下,让数据按分组键天然有序流入,从而避免显式构建临时表——但它仍依赖内部状态维护(如sum_field->aggregator),不是无状态的管道。
真正触发tmp table grouping的条件是:数据无法按分组键顺序到达,或优化器判断索引扫描成本高于建临时表。此时执行器会创建临时表,字段包含分组列+聚合列,并在分组列上建哈希索引加速去重与更新。
- streaming路径仅适用于单表、分组列是驱动表索引最左前缀、且无范围WHERE条件(如
WHERE a = 1 GROUP BY b可走loose index scan;WHERE a > 1 GROUP BY b则不行) - 一旦JOIN多表、或GROUP BY列不满足索引前缀连续性(如索引是
(a, b, c)但写GROUP BY b, c),立刻退化为tmp table路径 - MySQL 8.0+中
ORDERED_INDEX_GROUP_BY标志位决定是否启用streaming;可通过EXPLAIN FORMAT=TREE观察是否出现grouping_operation节点
为什么有索引还出现Using temporary?关键看索引列顺序和WHERE前置
常见误解是“只要GROUP BY字段上有索引就一定不走临时表”。实际中,即使category有单列索引,SELECT category, COUNT(*) FROM goods WHERE price > 100 GROUP BY category仍大概率触发Using temporary。
原因在于:price上的范围条件破坏了索引的有序性,MySQL无法保证扫描出来的category值是单调递增/递减的,也就无法流式分组。它必须先把所有满足price > 100的行捞出来,再统一按category归堆——这一步只能靠临时表。
- 正确做法是建联合索引
(price, category),把等值过滤列price = ?放最左,后续列才能用于分组排序 - 若WHERE中是
price BETWEEN 100 AND 500,仍属范围查询,联合索引中category无法被用于流式分组,但至少能减少扫描行数 -
ORDER BY NULL能禁用隐式排序,但不能消除临时表;它只跳过最后一步文件排序
内存临时表何时落地磁盘?别只盯tmp_table_size
内存临时表不是简单比对tmp_table_size阈值。MySQL 8.0+默认用TempTable引擎,它先按temptable_max_ram(默认1GB)分配内存块,当单个哈希桶膨胀或字符串列长度超限,就会触发磁盘落盘——哪怕总大小远低于tmp_table_size。
典型诱因包括:
- GROUP BY字段含长文本(如
VARCHAR(2000)),TempTable引擎对单字段长度敏感,易触发磁盘回退 - 分组键基数极高(如对
user_id分组千万级用户),哈希索引碎片化导致内存利用率骤降 - 聚合列含大对象(如
GROUP_CONCAT(content)),内容直接存入临时表而非指针,迅速撑爆内存
查证方式:SHOW STATUS LIKE 'Created_tmp%';关注Created_tmp_disk_tables是否增长;配合performance_schema.memory_summary_global_by_event_name定位TempTable内存分配峰值。
GROUP BY后ORDER BY的隐式耦合:排序字段必须出现在SELECT列表或满足函数依赖
很多人写SELECT dept, COUNT(*) FROM emp GROUP BY dept ORDER BY COUNT(*) DESC能跑通,但换成ORDER BY salary DESC就报错——这不是bug,而是MySQL对语义完整性的强制校验。
执行器在streaming路径下,要求ORDER BY字段要么是SELECT中的别名(如COUNT(*) AS c),要么其值能由GROUP BY列函数推导(即满足函数依赖)。否则,它必须先完成全部分组聚合,再对结果集二次排序,这就必然引入第二轮临时表或filesort。
- 在
only_full_group_by模式下,SELECT dept, COUNT(*) FROM emp GROUP BY dept ORDER BY dept合法,因为dept是分组键 - 但
SELECT dept, COUNT(*) FROM emp GROUP BY dept ORDER BY MAX(salary)不合法,因为MAX(salary)未出现在SELECT列表中,且非分组键函数依赖项 - 想绕过?把
MAX(salary)显式写进SELECT:SELECT dept, COUNT(*), MAX(salary) AS ms FROM emp GROUP BY dept ORDER BY ms DESC
最易被忽略的是:即使你没写ORDER BY,MySQL 5.7+默认对GROUP BY结果隐式排序,这步也会消耗资源。线上报表类查询务必显式加ORDER BY NULL,并确认执行计划中Using filesort已消失。


















