group by 慢大概率因哈希分组退化至 tempdb:先用 set statistics xml on 查执行计划中 hashkeysprobe 或 hash match 及 tempdbpages;再用 sys.dm_db_task_space_usage 定位 internal_objects_alloc_page_count 高且含 group by 的语句;区分 hash match(看 tempdbpages)与 stream aggregate+sort(查 sys.dm_exec_query_profiles 中 sort spills);注意统计信息过期易致误选排序路径,且 tempdb 占用延迟释放。

查 GROUP BY 是否在用 tempdb 做哈希分组
GROUP BY 慢或卡住,大概率不是逻辑问题,而是 SQL Server 把哈希表刷到 tempdb 里了。先确认它是不是真在用 tempdb:运行查询前加 SET STATISTICS XML ON,执行后打开执行计划 XML,搜 HashKeysProbe 或 Hash Match —— 如果有,且属性里 EstimateRows 和 ActualRows 差一个数量级,基本就是哈希分组退化;再看 TempDbPages 字段(部分版本显示为 UsedTempdbSpace),非零就坐实了。
定位具体是哪个 GROUP BY 查询占了 tempdb
用 sys.dm_db_task_space_usage 查正在跑的任务级 tempdb 分配,配合 sys.dm_exec_requests 关联当前 SQL 文本:
SELECT
t.session_id,
t.request_id,
t.task_address,
t.database_id,
t.user_objects_alloc_page_count - t.user_objects_dealloc_page_count AS user_pages_diff,
t.internal_objects_alloc_page_count - t.internal_objects_dealloc_page_count AS internal_pages_diff,
r.command,
SUBSTRING(qt.text, (r.statement_start_offset/2)+1,
((CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text) ELSE r.statement_end_offset END - r.statement_start_offset)/2) + 1) AS statement_text
FROM sys.dm_db_task_space_usage t
JOIN sys.dm_exec_requests r ON t.session_id = r.session_id AND t.request_id = r.request_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS qt
WHERE t.internal_objects_alloc_page_count > 0
ORDER BY internal_pages_diff DESC;
重点看 internal_objects_alloc_page_count —— GROUP BY 的哈希工作表属于 internal object,不是 user object;statement_text 里带 GROUP BY 且页数高,就是它。
区分是哈希分组还是排序分组在吃 tempdb
同一个 GROUP BY,执行计划可能走 Hash Match Aggregate(哈希分组)或 Stream Aggregate + Sort(排序分组),后者更吃 tempdb 且更隐蔽:
- Hash Match:看执行计划里是否有
Hash Keys Build,TempDbPages高,但一般不触发Sort运算符 - Stream Aggregate:必须前置
Sort运算符,而 Sort 很容易溢出——查sys.dm_exec_query_profiles(需启用SET STATISTICS PROFILE ON),找Sort节点的Spills字段,大于 0 就说明排序已写 tempdb - 统计信息过期时,优化器常低估行数,误选 Sort 路径;用
DBCC SHOW_STATISTICS看ModificationCount是否远超Rowcount的 20%
避免监控本身干扰生产
别在高峰期反复跑 sys.dm_db_file_space_usage 全局视图——它本身要扫描所有分配单元,可能加重 tempdb 压力。更轻量的做法是:
- 只查
sys.dm_db_session_space_usage和sys.dm_db_task_space_usage,它们是 per-session/per-task 的轻量快照 - 对可疑会话,用
DBCC INPUTBUFFER(<session_id>)</session_id>快速看最后执行语句,比拉完整 SQL 文本快 - 临时开启
QUERYTRACEON 3604, 3605把哈希内存使用打到 errorlog,但仅限诊断,不能长期开 - 真正要长期监控,得靠扩展事件(XEvent)捕获
query_post_execution_showplan+sort_warning事件,而不是轮询 DMV
最易被忽略的一点:GROUP BY 占用的 tempdb 空间,在查询结束后不会立刻归还——它由 lazy writer 异步清理,所以看到的“高占用”可能是上一个大查询的残留,不是当前活跃查询导致的。










