一眼识别Hash JOIN向Tempdb灌数据:查sys.dm_db_session_space_usage中internal_objects_alloc_page_count,若某会话该值几秒内飙升超10000页(≈78MB)即大概率发生spill,超100000页(≈780MB)已严重溢出。

怎么一眼看出Hash JOIN正在往Tempdb里灌数据
直接查 sys.dm_db_session_space_usage,重点关注 internal_objects_alloc_page_count。这个值不是“累计使用量”,而是当前会话正在占用的内部对象页数(1页 = 8KB)。如果某 session 的该值在几秒内从 0 涨到 50000+,基本就是 Hash JOIN 正在 spill。
运行这条语句即可快速定位:
SELECT session_id, internal_objects_alloc_page_count, user_objects_alloc_page_count FROM sys.dm_db_session_space_usage WHERE internal_objects_alloc_page_count > 10000 ORDER BY internal_objects_alloc_page_count DESC;
- 超过 10000 页(≈78MB)就值得盯住;超过 100000 页(≈780MB)大概率已严重溢出
- 注意:该视图只反映“当前活跃会话”,执行完就归零,必须实时抓取
- 别依赖
sp_spaceused—— 它对内部对象完全不敏感,数值严重偏低
执行计划里怎么确认SpillLevel真的发生了
启用 SET STATISTICS XML ON 后执行 SQL,看生成的 XML 计划中 节点是否带 SpillLevel="1" 或属性 Warning="Operator used tempdb to spill data"。
常见误判点:
- 图形化执行计划里只显示黄色感叹号,但不标具体 SpillLevel —— 必须看 XML 原文
-
EstimateRows和ActualRows差距超 10 倍,且GrantedMemoryKB明显小于ActualRows × 行宽 × 2,是 spill 的强信号 - CTE 或视图嵌套越深,
EstimateRows越不可信,即使没报 Warning,也可能已静默 spill
有没有不打断业务的实时监控手段
有,但得组合用。单靠一个 DMV 或告警阈值容易漏掉“短时爆发型”溢出。
推荐轻量级轮询脚本(每 5 秒跑一次):
SELECT
r.session_id,
r.status,
r.wait_type,
r.wait_time,
s.internal_objects_alloc_page_count,
SUBSTRING(t.text, (r.statement_start_offset/2)+1,
((CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END - r.statement_start_offset)/2) + 1) AS stmt
FROM sys.dm_exec_requests r
JOIN sys.dm_db_session_space_usage s ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE s.internal_objects_alloc_page_count > 50000
AND r.status = 'running';
- 重点过滤
wait_type IN ('PAGEIOLATCH_IO', 'SOS_SCHEDULER_YIELD', 'SORT'),这是 spill 导致 I/O 或调度瓶颈的典型等待 - 不要长期开
QUERYTRACEON 3604, 3605—— 日志刷屏、影响性能,仅调试用 - 扩展事件(XEvent)可捕获
query_post_execution_showplan事件并筛选含SpillLevel的计划,但需提前建会话,不适合临时排查
为什么查到了spill,却找不到对应SQL文本
因为 spill 可能发生在子计划里,比如 CTE 展开后、视图内联后、或参数化查询重编译后的实际执行分支中。你看到的 sql_handle 可能指向最外层语句,而非真正 spill 的那个 JOIN 子树。
这时要:
- 用
sys.dm_exec_query_plan提取完整 XML,全文搜索 - 检查该算子的
TableCardinality和上游EstimateRows,反推它来自哪个表或子查询 - 若涉及临时表,务必确认是否显式建了索引 —— 无索引的
#tmpJOIN 几乎必 spill,且不会在原始 SQL 中暴露
真正难处理的不是报错的 spill,而是那些 SpillLevel="1" 却没触发 ERROR 701 的查询:它们悄悄吃光 Tempdb 空间,又不留下明显错误日志,只能靠持续监控 internal_objects_alloc_page_count 的毛刺来发现。










