聚合下推是优化器在满足条件时自动选择的策略,核心前提是输入数据按分组字段物理有序,这几乎完全依赖索引;若WHERE中对分组字段用函数、分组字段不在索引最左前缀、JOIN后GROUP BY且未走Merge/Index Seek,或外层ORDER BY非分组字段,均会破坏排序导致无法启用Stream Aggregate。

聚合下推不是手动“开启”的功能,而是优化器在满足特定条件时自动选择的执行策略;它能否生效,取决于你写的 GROUP BY 是否能被驱动成 Stream Aggregate,核心前提是输入数据已按分组字段物理有序——这几乎全靠索引决定。
为什么你的 GROUP BY 没走 Stream Aggregate?
SQL Server 2022 默认倾向用 Hash Match (Aggregate),但它的代价是内存哈希建表+重散列,数据量一大就慢。而 Stream Aggregate 是流式累加,近乎 O(n),但要求上游节点输出已按分组列排序。常见破坏排序的写法:
- WHERE 中对分组字段用了函数,比如
WHERE YEAR(order_time) = 2024→ 索引失效,无法保证 order_time 有序 - 分组字段不在索引最左前缀,比如索引是
(user_id, order_time),却写GROUP BY order_time - JOIN 后再 GROUP BY,且 JOIN 没走 Merge 或 Index Seek(比如用了 Nested Loops + 表扫描)
- 外层有 ORDER BY 非分组字段,迫使优化器放弃流式路径
如何让优化器选 Stream Aggregate?
目标不是“提示它用”,而是“让它不得不选”——通过索引和写法把排序成本前置到索引扫描阶段:
- 为分组字段建覆盖索引,且把分组列放在最左:例如
GROUP BY user_id, status,就建CREATE NONCLUSTERED INDEX IX_orders_group ON orders(user_id, status) INCLUDE(order_amount, created_at) - WHERE 条件必须可走索引 Seek:时间范围写成
created_at >= '2024-01-01' AND created_at ,别用DATEPART(YEAR, created_at) = 2024 - 避免在 GROUP BY 前引入无序操作:比如不要在子查询里先
UNION ALL再分组,也不要在 CTE 里做未排序的SELECT * - 如果必须 JOIN,确保连接方式能保留顺序:用
INNER JOIN ... ON a.id = b.a_id并给b.a_id建索引,比LEFT JOIN更易触发 Merge Join
替代 GROUP BY 的轻量方案:DISTINCT 能快一个数量级
如果你的语句只是 SELECT col1, col2 FROM t GROUP BY col1, col2(无任何聚合函数),那它语义上等价于 SELECT DISTINCT col1, col2 FROM t,但执行路径完全不同:
-
DISTINCT在 SQL Server 2022 中更倾向走Sort + Top N Sort或直接利用索引顺序去重,尤其当有合适索引时,可能跳过排序 -
GROUP BY即使没聚合函数,优化器仍按聚合流程处理,大概率触发Hash Match - 实测中,百万行去重场景,
DISTINCT耗时常为GROUP BY的 1/10~1/5
注意:仅当不依赖 COUNT(*)、SUM() 等聚合结果时才可替换;一旦加了聚合,DISTINCT 就不再合法。
验证是否真正下推成功
别只看执行时间,重点盯执行计划里的三个信号:
- 找到
Stream Aggregate节点,确认其直接上游是Index Seek或Clustered Index Scan(且该索引键顺序匹配 GROUP BY 列) - 检查该
Index Seek的Actual Number of Rows是否接近最终输出行数——若差两个数量级,说明中间膨胀严重,下推实际失败 - 右键节点 → Properties → 查看
Ordered = True和EstimateIsPotentiallyUnordered = False
真正的聚合下推,是让数据库把“分组”这件事从内存计算变成磁盘顺序读取的副产品;最容易被忽略的是:它不依赖任何新语法或 hint,只依赖你有没有把索引建对、把 WHERE 写干净。










