先查sys.dm_db_session_space_usage中internal_objects_alloc_page_count>100000的会话,再通过sys.dm_exec_requests关联SQL文本和wait_type(如SORT、PAGEIOLATCH_IO),最后用sys.dm_exec_query_plan确认Sort或Hash Match溢出节点。

立刻查出正在溢出的会话和SQL
别等报错再动,internal_objects_alloc_page_count 超过 10 万页(约 800MB)的会话基本就在往 TempDB 灌哈希溢出页。直接跑:
SELECT session_id, internal_objects_alloc_page_count FROM sys.dm_db_session_space_usage WHERE internal_objects_alloc_page_count > 100000;
把返回的 session_id 带入下面这句,拿到真实 SQL 文本和等待类型:
SELECT t.text, r.wait_type, r.wait_time FROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t WHERE r.session_id = [你的session_id];
重点关注 wait_type 是 SORT、PAGEIOLATCH_IO 或 SOS_SCHEDULER_YIELD 的——这些是 Hash Match 溢出的典型信号。
紧急止血:KILL + 改写 + 绕开Hash
确认是某个大关联查询在作祟后,立刻干预,而不是先调优:
- 用
KILL [session_id]干掉它,尤其当wait_type = 'PAGEIOLATCH_IO'且持续超 30 秒时,说明已在疯狂刷磁盘 - 如果不能停业务,改写调用方 SQL:把
WHERE条件塞进子查询,例如把JOIN orders o ON u.id = o.user_id WHERE o.order_date >= '2025-01-01'改成JOIN (SELECT * FROM orders WHERE order_date >= '2025-01-01') o ON u.id = o.user_id - 临时加
OPTION (LOOP JOIN, MAXDOP 1)强压执行计划避开 Hash,但前提是驱动表够小、连接列有索引,否则可能更慢
为什么加了索引,执行计划还是选Hash Match?
索引本身不阻止 Hash Match;优化器选它,往往是因为统计信息陈旧或内存估算崩了:
- 检查连接列统计是否过期:
DBCC SHOW_STATISTICS('orders', 'IX_orders_user_id')中Rows和Rows Sampled差距超过 20%,就该UPDATE STATISTICS orders IX_orders_user_id WITH FULLSCAN - 看执行计划 XML 里的
GrantedMemoryKB,如果只有几 MB,但EstimateRows是百万级,说明内存授予严重不足——不是没资源,而是优化器“算错了” -
WHERE写在外层、或用 CTE/视图包装后没下推,会让优化器误判驱动表大小,强行把大表当构建表
收缩TempDB几乎无效,真正卡住的是版本记录
DBCC SHRINKFILE 在活跃事务或快照隔离下基本不生效,尤其当 sys.dm_db_file_space_usage 中 version_store_reserved_page_count 持续上涨时:
- 运行
DBCC OPENTRAN查有没有未提交的长事务,KILL 掉它 - 检查数据库是否启用了
READ_COMMITTED_SNAPSHOT,如果是,而又有长时间运行的 UPDATE/DELETE,版本记录就会堆在 TempDB 里不释放 - 临时禁用快照隔离(
ALTER DATABASE xxx SET READ_COMMITTED_SNAPSHOT OFF)能立竿见影,但需评估应用一致性影响
最隐蔽的瓶颈,从来不是某条 SQL 写得差,而是基数估算偏差 + 版本滞留 + 并发叠加——三者一碰,TempDB 就在无声中被吃光。










