GROUP BY 在千万级表上变慢主因是执行计划选择 Hash Match Aggregate 或全表扫描,导致 tempdb 落盘;需确保索引字段顺序与分组列一致、避免 SELECT *、处理隐式转换、利用分区剪枝及预聚合。

为什么 GROUP BY 在千万级表上会突然变慢
不是语法写错了,而是执行计划里悄悄走了 Hash Match Aggregate 或全表扫描。当数据量超过 1000 万,GROUP BY 没走索引、没利用排序优势、又没覆盖查询列时,SQL Server 很可能放弃内存聚合,转而把中间结果写入 tempdb —— 这一落盘,耗时就从毫秒级跳到秒级甚至分钟级。
必须让 GROUP BY 走索引扫描(而不是查找)
关键不是“有没有索引”,而是“索引是否支持有序聚合”。GROUP BY 要高效,索引字段顺序必须和分组字段完全一致,且不能有跳过前导列的情况。
- 错误示例:
CREATE INDEX IX_Odd ON Orders(CustomerID, OrderDate, Amount),但查询是GROUP BY OrderDate, CustomerID→ 因为顺序不匹配,索引大概率被忽略 - 正确做法:若常按
OrderDate, CustomerID分组,建索引就得是CREATE INDEX IX_DateCust ON Orders(OrderDate, CustomerID) INCLUDE(Amount) - 聚集索引更优:如果业务天然按时间分组(如日/月汇总),直接把
fariqi或CreateDate设为聚集索引,GROUP BY fariqi就能走顺序扫描,避免排序开销 - 注意 NULL 值:含大量 NULL 的列参与分组时,SQL Server 可能拒绝使用索引;可提前用
WHERE fariqi IS NOT NULL过滤,或用计算列+索引固化非空值
避免 SELECT * + GROUP BY 的组合陷阱
这是最隐蔽的性能杀手。SELECT *, COUNT(*) FROM T GROUP BY A 看似合理,实际会强制回表 + 排序 + 大量逻辑读 —— 即使 A 上有索引也救不了。
- 立刻改写:只选真正需要的列,例如
SELECT A, COUNT(*), SUM(B), MAX(C) FROM T GROUP BY A - 用
INCLUDE补齐聚合所需字段,比如CREATE INDEX IX_A ON T(A) INCLUDE(B, C),这样索引页里就能拿到全部数据,不用再查数据页 - 警惕隐式转换:
GROUP BY SUBSTRING(Name, 1, 3)或YEAR(CreateDate)会导致索引失效;应改用计算列+持久化索引:ALTER TABLE T ADD NamePrefix AS SUBSTRING(Name, 1, 3) PERSISTED,再在该列建索引
分区表 + 聚合下推能绕过很多瓶颈
当单表超 2000 万且按时间分组占主流(如每天统计销售),硬扛不如拆解。SQL Server 的分区不是噱头,配合 GROUP BY 能做到物理剪枝。
- 按月分区后,
GROUP BY OrderDate且WHERE OrderDate >= '2026-09-01'时,SQL Server 只扫 9 月那个分区文件,IO 直接降一个数量级 - 别忘了对每个分区单独维护统计信息:
UPDATE STATISTICS T ON [PartitionScheme] WITH RESAMPLE,否则优化器可能误判行数,选错执行计划 - 慎用
SWITCH归档旧分区:归档后记得重建目标表上的对应分区索引,否则新分区的聚合性能会断崖下跌
最容易被忽略的一点:GROUP BY 的聚合结果集大小,比原始表还大 —— 比如按秒级时间戳分组,千万行可能产出百万个分组键。这时候再好的索引也扛不住内存压力,得靠业务层预聚合或引入物化视图(SQL Server 2016+ 的 INDEXED VIEW)来兜底。


















