确认Hash Match溢出需查sys.dm_db_session_space_usage中internal_objects_alloc_page_count>100000的会话,再结合执行计划SpillLevel>0或XML中SpillToTempDb="1"判断;根本原因是统计信息过期、过滤未下推或连接列无索引,临时表缺索引亦是隐蔽诱因。

临时表 JOIN 大表本身不爆 tempdb,真正爆的是 Hash Match 或 Sort 操作因内存估算失败而溢出到磁盘——这几乎总是统计信息过期、过滤未下推、或连接列无有效索引导致的。
怎么确认是 Hash Match 在 spill(溢出)?
别看错误日志,直接查运行时分配:
- 执行
SELECT session_id, internal_objects_alloc_page_count FROM sys.dm_db_session_space_usage WHERE internal_objects_alloc_page_count > 100000,找出高占用会话 - 用该
session_id关联sys.dm_exec_requests和sys.dm_exec_sql_text,拿到正在跑的 SQL - 在图形化执行计划里找
Hash Match算子,右键 → 属性 → 看SpillLevel是否 > 0;或在 XML 计划里搜<relop. match></relop.>
为什么加了索引,JOIN 还走 Hash Match?
索引对 Hash Match 没有“禁用”效果;优化器选它,是因为它觉得这是最快路径——而这判断严重依赖两件事:
-
DBCC SHOW_STATISTICS显示连接列的Rows和Rows Sampled差距大 → 统计信息过期,必须UPDATE STATISTICS - 执行计划里
GrantedMemoryKB远小于实际需要(比如估算 1 万行 × 行宽 200 字节 ≈ 2MB,但只批了 512KB)→ 内存不足强制 spill - WHERE 条件写在外层(如
FROM #tmp t JOIN orders o ON t.id = o.user_id WHERE o.order_date >= '2025-01-01'),优化器看不到过滤后的真实基数,误判驱动表大小
临时表本身没索引,是最隐蔽的坑
你建了 #tmp,INSERT 了几百万行,然后 JOIN 大表——但 #tmp 默认无任何索引,优化器只能扫全表,还大概率选 Hash Match。这不是语法错,是设计盲区:
- 显式给临时表加索引:
CREATE INDEX IX_#tmp_user_id ON #tmp(user_id)(注意:SQL Server 2014+ 支持在临时表上建非聚集索引) - 更稳妥的做法:把过滤和 JOIN 拆开,先 INSERT INTO #tmp_filtered SELECT * FROM #tmp WHERE ...,再 JOIN,让优化器分阶段估算
- 避免用表变量
@t替代#tmp做大结果集操作——表变量无统计信息,优化器一律按 1 行估算,极易触发灾难性 Hash Match
紧急干预和长期规避的关键差异
临时表 JOIN 爆 tempdb 的难处在于:它往往不报错,只变慢;或者只在高峰期偶发 spill,监控阈值很难设准。最易被忽略的是——
- spill 发生后,
internal_objects_alloc_page_count不会在查询结束后立刻归零,而是等到会话断开才释放;这意味着一个长连接的旧查询可能持续占着几十 GB tempdb 空间,新查询全被卡住 - 用
OPTION (RECOMPILE)能绕过参数嗅探引发的低效计划,但它治标不治本;如果底层统计信息常年不更新,下次参数一换,又退化 - tempdb 文件数量配得再多,只要大小不一致(比如一个 8GB、一个 16GB),比例填充就失效,所有 Hash Match spill 都往小文件里挤,I/O 争用比空间不足更早拖垮系统

















