应查sys.dm_exec_query_stats找突增join语句,通过query_hash聚合相同结构sql,对比昨日同时间段avg_elapsed_time比值>3即标红,再结合执行计划、阻塞等待及统计信息验证根因。

查 sys.dm_exec_query_stats 找突增的 JOIN 语句
生产环境里,JOIN 变慢往往不是单条 SQL 突然写错,而是某几条语句执行时间、逻辑读或 CPU 暴涨。直接看慢日志容易漏掉“温水煮青蛙”型问题,得靠系统视图抓异常波动。
关键不是找“最慢”的,而是找“比昨天慢 5 倍”的——sys.dm_exec_query_stats 里有 last_execution_time、total_logical_reads、total_elapsed_time,配合 query_hash 能聚合相同结构的语句(忽略参数值差异)。
- 先筛出最近 1 小时内
avg_elapsed_time> 1000ms 且execution_count> 10 的语句 - 用
query_hash分组,算avg_elapsed_time与昨日同时间段均值的比值,> 3 就标红 - 用
sys.dm_exec_sql_text(plan_handle)提取原始 SQL,重点盯带JOIN、ON、FROM ... JOIN ...的片段
用 sys.dm_exec_cached_plans 对比执行计划是否突变
很多 JOIN 变慢根本不是数据量涨了,而是执行计划换了——比如统计信息过期后,优化器误判驱动表行数,选了个全表扫描的嵌套循环;或者参数嗅探导致复用了一个只适合“小参数”的计划。
同一个 query_hash 可能对应多个缓存计划(plan_handle),要对比它们的 usecounts 和生成时间:
- 查
sys.dm_exec_cached_plans+sys.dm_exec_query_plan,提取 XML 计划里<relop logicalop="Hash Join"></relop>或<relop logicalop="Nested Loops"></relop>的子节点结构 - 重点关注
EstimateRows和实际运行时的ActualRows是否偏差巨大(比如预估 100 行,实际扫 20 万行) - 如果发现新计划里出现
<indexscan></indexscan>替代了原来的<indexseek></indexseek>,基本就是统计信息没更新或索引失效
结合 sys.dm_exec_requests 看实时阻塞与等待类型
JOIN 语句卡住不动?不一定是它自己慢,可能是被锁住了。尤其多表 JOIN 常伴随长事务或未提交的 UPDATE/INSERT,让后续 SELECT 在 LCK_M_S 上干等。
运行中查 sys.dm_exec_requests,过滤 status = 'running' 或 status = 'suspended':
-
wait_type是LCK_M_S或LCK_M_U?说明在等锁,顺藤摸瓜查blocking_session_id -
wait_type是PAGEIOLATCH_SH?说明磁盘 IO 跟不上,JOIN 中的排序/哈希操作正在疯狂读 TempDB 或数据文件 -
wait_type是ASYNC_NETWORK_IO?注意:这不是数据库问题,是客户端取结果太慢,导致连接一直挂着,后续 JOIN 请求排队
别跳过 STATS_DATE() 和索引有效性验证
你建了 idx_orders_user_id,但 JOIN 还是走全表扫描?可能索引根本没生效——要么字段类型不匹配(INT vs VARCHAR),要么统计信息过期,要么索引碎片太高(>30%)。
验证三步不能省:
- 用
STATS_DATE(object_id, index_id)查统计信息最后更新时间,超过 7 天或数据变更 > 20% 就该UPDATE STATISTICS - 用
sys.dm_db_index_usage_stats看user_seeks是否为 0 —— 如果从没被 SEEK 过,这索引大概率是摆设 - 用
sys.dm_db_index_physical_stats查avg_fragmentation_in_percent,>30% 的索引重建前,执行计划很可能拒绝使用它
真正卡住 JOIN 的,往往不是语法或逻辑,而是这些“看起来正常”的底层状态悄然失准。查计划、比哈希、盯等待、验统计——四件事缺一不可,少一个就容易把锅甩给代码,而问题其实在数据库的毛细血管里。










