应查XML执行计划中SpillLevel="1"或"Warning: Operator used tempdb to spill data",定位Hash Match或Sort节点溢出;临时表须显式建索引,避免全表扫描反复spill。

看执行计划里有没有 Hash Match 或 Sort 节点溢出
SQL Server 的 GROUP BY 溢出不是“内存不够”这种模糊描述,而是具体算子在执行时把中间结果写到了 tempdb——最常见就是 Hash Match(用于聚合或连接)和 Sort(用于 ORDER BY、窗口函数或隐式排序分组)。打开 SET STATISTICS XML ON 后执行查询,在 XML 执行计划里搜索 SpillLevel="1" 或警告文字 "Warning: Operator used tempdb to spill data",就能准确定位到哪个节点在落盘。
容易踩的坑:
- 只看图形化执行计划,忽略 XML 里的 Spill 提示——图形界面默认不显示溢出警告
- 误以为
Sort溢出只跟 ORDER BY 有关,其实 GROUP BY 在无索引支撑时也会触发隐式排序(尤其当优化器选错算法) - 嵌套 CTE 或子查询后,外层 GROUP BY 的基数估算严重失真,
EstimateRows显示 100 行,实际输入几百万行,内存分配严重不足
查 tempdb 的物理 IO 和等待类型
溢出本质是 tempdb 文件被高频读写。如果观察到 PAGEIOLATCH_SH 等待飙升、tempdb 数据文件所在磁盘队列长度持续 >2,基本可以锁定问题源头。用以下语句快速确认:
SELECT
session_id,
wait_type,
wait_duration_ms,
resource_description
FROM sys.dm_os_waiting_tasks
WHERE wait_type LIKE 'PAGEIOLATCH_%' AND resource_description LIKE '2:%';
其中 2: 表示 tempdb 的数据库 ID,resource_description 会显示具体页号(如 2:1:123456),说明正在从 tempdb 的某个数据页读取 spilled 数据。
注意:tempdb 的日志文件(ldf)也可能成为瓶颈——溢出过程伴随大量日志生成,若日志文件放在慢盘或未预分配,会进一步拖慢整个流程。
临时表没建索引是最大隐形雷
很多人把复杂 GROUP BY 拆成 SELECT INTO #tmp 再聚合,以为能“控制节奏”,结果更糟:SQL Server 默认给临时表建的统计信息极弱,且 不自动建任何索引。后续所有 JOIN、WHERE、GROUP BY 都只能全表扫描,反复触发 Hash Match 溢出。
正确做法是显式建索引:
- 在
INSERT INTO #tmp完成后立刻建索引:CREATE INDEX IX_tmp_OrderDate_StoreId ON #OrderSummary (OrderDate, StoreId); - 避免对临时表做多次 INSERT + GROUP BY;一次插入、一次索引、一次聚合,减少中间状态
- 如果临时表只用于单次 JOIN,考虑用表变量(
@tmp)替代——它不会触发统计信息更新开销,但仅限小数据量(
参数和隔离级别也会悄悄放大溢出风险
tempdb 溢出不只是查询本身的问题。快照隔离(READ_COMMITTED_SNAPSHOT 或 SNAPSHOT)开启后,长事务会滞留大量版本记录,挤占 tempdb 空间,间接导致新查询因空间不足而 spill。另外,MAXDOP 设置过高可能让多个线程并发申请 hash 内存,争抢 tempdb 缓冲区。
排查时要同步检查:
-
DBCC SQLPERF('sys.dm_os_wait_stats')中TEMPDB相关等待是否突增 -
SELECT name, is_read_committed_snapshot_on FROM sys.databases WHERE name = DB_NAME()确认隔离级别是否意外开启 - 存储过程中是否用了
OPTION (RECOMPILE)却没清理参数嗅探缓存,导致旧执行计划复用失败、基数误估加剧溢出
真正难缠的从来不是单个溢出点,而是多个小问题叠在一起:临时表没索引 + 快照隔离未关闭 + 外层 CTE 基数误估 + tempdb 文件未预分配——它们各自看起来都“差不多”,合起来就让查询从秒级变成分钟级。

















