sql server 的 group by 不走 index seek 是因为其索引使用严格遵循最左前缀原则,where 条件未包含索引首列(如索引为(itemcode, avg, time)但查询仅用time过滤),导致无法触发index seek,只能回退至index scan或clustered index scan,即使group by字段匹配首列也无法“倒推”加速。

SQL Server 的 GROUP BY 为什么没走 Index Seek?
因为 SQL Server 不会为 GROUP BY 单独“优化出”索引查找路径——它只看 WHERE 条件是否能触发 Index Seek,而分组本身不构成 seek 条件。如果 WHERE 没命中索引首列,哪怕 GROUP BY 字段全在索引里,也只会退化成 Index Scan 或更糟的 Clustered Index Scan。
典型表现是执行计划里出现 Stream Aggregate 上接 Index Scan(不是 Index Seek),说明引擎扫了整个索引再分组,而非按需定位。
- 索引定义是
(ItemCode, [AVG], [Time]),但查询带的是WHERE [Time] BETWEEN ...——[Time]不是首列,该索引无法用于 seek -
GROUP BY ItemCode虽匹配索引首列,但 SQL Server 不会“倒推”用它来加速分组;它只关心 WHERE 是否能快速定位数据起点 - 即使强制指定索引(
WITH (INDEX(...))),只要 WHERE 条件不满足最左前缀,依然只能 scan
非聚集索引被跳过的真实原因:临界点(Tipping Point)
当查询返回行数超过某个阈值(通常是表总页数的 ~25%–30%),SQL Server 会判定:用非聚集索引 + 书签查找(Key Lookup)的成本,高于直接扫一遍聚集索引。这时它就“主动放弃”你的非聚集索引,改走 Clustered Index Scan。
这不是 bug,是成本估算结果。你看到 WHERE Value 走聚集扫描、<code>WHERE Value 却走非聚集索引,就是临界点在起作用。
- 临界点取决于**返回的页数**,不是行数;所以即使你只查 10 行,但如果这些行散落在 500 个不同数据页上,仍可能触发临界点
- 非覆盖索引(即索引不包含 SELECT 所有字段)更容易被跳过,因为 Key Lookup 是随机 IO,代价高
- 加
INCLUDE列让索引变成覆盖索引,可绕过临界点判断,强制复用该索引
怎么建索引才能让 GROUP BY 实际受益?
核心不是“让 GROUP BY 走索引”,而是让整个查询(WHERE + GROUP BY + SELECT)能在一个索引里闭环完成——即构建**覆盖型联合索引**,且顺序严格对齐访问模式。
- 先确保 WHERE 条件字段在索引最左:例如
WHERE [Time] >= ... AND [Time] → 索引应为 <code>([Time], ItemCode),而不是(ItemCode, [Time]) - 把聚合字段加到索引末尾:如要
MAX([AVG])和MAX([Time]),就把它们包含进索引,避免回表,例如([Time], ItemCode, [AVG], [Time]) - 注意字段重复:
[Time]已在 WHERE 条件中作为前导列,又在 SELECT 中被 MAX,索引里只需一份,不用重复写两次 - 避免在 WHERE 中对索引列用函数:
WHERE YEAR([Time]) = 2022会让整个索引失效,改用范围条件WHERE [Time] >= '2022-01-01' AND [Time]
执行计划里哪些信号说明 GROUP BY 索引没对上?
别只盯 Index Scan,重点看组合和算子链路:
- 出现
Hash Aggregate:说明分组没利用索引有序性,被迫哈希建临时表,基本等于全表扫描后处理 -
Sort+Stream Aggregate:说明索引没按GROUP BY列排序,引擎不得不额外排序 -
Key Lookup或RID Lookup高频出现:索引不覆盖,每次分组后还要回表取字段,IO 放大 - 实际读取行数(
Actual Rows)远大于预估(Estimated Rows):统计信息陈旧,优化器误判,也可能导致本该 seek 的走了 scan
最常被忽略的一点:SQL Server 永远不会为 GROUP BY 自动重写查询去“模拟跳跃扫描”。你建的索引顺序,就是它唯一能依赖的路径。错一个位置,整条链路就断了。











