4. 等待统计与性能瓶颈定位
SQL Server 的性能问题,本质上很少是“CPU 不够快”这么简单。一个查询变慢,往往是因为它在某个环节上“等”了太久——等磁盘把数据页读进来、等内存授予到位、等锁释放、等 CPU 时间片。等待统计(Wait Statistics)就是把这些“等待”按类型、按时间量化出来,让我们从“感觉慢”走到“知道慢在哪里”。
理解等待统计,先要建立一个核心公式:响应时间 = 工作时间 + 等待时间。工作时间是 CPU 真正执行指令的时间,等待时间则是线程处于挂起状态、等待某种资源就绪的时间。SQL Server 为每一次等待都记录了等待类型(wait_type)、等待次数(waiting_tasks_count)、累计等待毫秒数(wait_time_ms)和最大单次等待(max_wait_time_ms)。这些数据通过 sys.dm_os_wait_stats 暴露出来,是定位瓶颈的第一手材料。
一、从累计等待到“当前谁在等”
sys.dm_os_wait_stats 是自 SQL Server 启动以来的累计值,适合看长期趋势,但不适合直接判断“此刻”的瓶颈。更实用的做法是取两次快照做差值,观察一段时间窗口内新增的等待。下面这段脚本是常用的采集方式:
-- 第一次快照
SELECT wait_type, waiting_tasks_count, wait_time_ms
INTO #w1 FROM sys.dm_os_wait_stats;
WAITFOR DELAY '00:00:30'; -- 观察 30 秒
-- 第二次快照并计算差值
SELECT w2.wait_type,
w2.waiting_tasks_count - w1.waiting_tasks_count AS wait_cnt,
w2.wait_time_ms - w1.wait_time_ms AS wait_ms
FROM sys.dm_os_wait_stats w2
JOIN #w1 w1 ON w1.wait_type = w2.wait_type
WHERE w2.wait_time_ms - w1.wait_time_ms > 0
ORDER BY wait_ms DESC;
要判断“当前谁在等”,则用 sys.dm_os_waiting_tasks,它列出此刻正在等待的会话、等待类型、阻塞它的会话(blocking_session_id)以及等待资源描述。把这两者结合:累计差值告诉你“哪类等待在增长”,实时视图告诉你“是哪些语句、被谁挡住”。
二、常见等待类型与资源映射
等待类型有几百种,但生产环境中反复出现的核心类型并不多。下面这张表把等待类型与它指向的资源瓶颈对应起来,是排障时最常查的对照关系:
| 等待类型 | 指向的瓶颈 | 典型成因 |
|---|---|---|
| PAGEIOLATCH_SH / EX | 磁盘 I/O | 数据页不在缓冲池,需从磁盘读取 |
| WRITELOG | 日志 I/O | 事务提交时日志写入慢,日志盘压力大 |
| CXPACKET / CXCONSUMER | 并行查询 | 并行执行中线程间同步,常伴随并行计划 |
| SOS_SCHEDULER_YIELD | CPU | 线程主动让出 CPU,CPU 压力高的信号 |
| RESOURCE_SEMAPHORE | 内存(查询授予) | 内存授予不足,查询排队等内存 |
| LCK_M_* | 锁 | 被其他事务的锁阻塞 |
| PAGELATCH_* | 内存中的页闩锁 | 热点页(如自增索引末尾页)争用 |
需要特别区分 PAGEIOLATCH 和 PAGELATCH:前者带 IO,说明页不在内存、要读盘,是 I/O 问题;后者不带 IO,说明页已在内存,但多个线程争抢同一页的闩锁,是并发争用问题。名字只差几个字母,排查方向完全不同。
三、把等待映射到四大资源
等待统计的价值在于它能同时覆盖 CPU、I/O、内存、锁四类瓶颈,而不需要分别去猜。下图展示了从等待类型到资源瓶颈的映射关系,以及每一类对应的进一步排查入口:
四、排除“良性等待”,聚焦真实瓶颈
直接看 sys.dm_os_wait_stats 的累计值会被大量无害等待淹没。例如 CHECKPOINT_QUEUE、LAZYWRITER_SLEEP、SQLTRACE_BUFFER_FLUSH、BROKER_TASK_STOP 等,都是后台任务在“正常休息”,不代表性能问题。排查时应先把这些良性等待过滤掉,只看真正消耗响应时间的类型。
另一个重要概念是“等待占比”。假设 30 秒窗口内总等待 60 万毫秒,其中 PAGEIOLATCH_SH 占 35 万毫秒,那 I/O 就是当前最突出的矛盾;如果 SOS_SCHEDULER_YIELD 占 30 万毫秒,则 CPU 压力更值得关注。占比比绝对值更能说明问题,因为不同服务器的负载规模差异很大。
五、从等待回到具体语句
等待统计只告诉你“哪类资源紧张”,要落到“哪条语句”,还需要结合实时会话。下面这段查询把当前等待会话、等待类型和正在执行的语句关联起来:
SELECT r.session_id,
r.blocking_session_id,
r.wait_type,
r.wait_time,
r.wait_resource,
SUBSTRING(t.text, 1, 120) AS running_sql
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.session_id > 50
AND r.wait_type IS NOT NULL
ORDER BY r.wait_time DESC;
如果 blocking_session_id 不为 0,说明是锁等待,顺着这个 ID 就能找到阻塞源头;如果 wait_resource 显示为某个数据页或键值,则能进一步定位到具体对象。这样,等待统计就从“资源级”下钻到了“语句级”,形成完整的定位链路。
六、定位之后做什么
等待统计本身不解决问题,它负责把问题指出来。CPU 等待高,就去查高 CPU 语句、并行计划和缺失索引;I/O 等待高,就去看缓冲池命中率、大表扫描和日志写入;内存授予等待高,就检查排序/哈希操作和内存配置;锁等待高,就梳理事务粒度和隔离级别。每一次优化之后,重新采集一次等待差值,对比目标等待类型是否下降——这就是验证优化是否有效的客观依据。
把等待统计当作性能排障的“入口仪表盘”:先用它确定方向,再用更细的 DMV 和工具深入,最后用等待差值验证效果。这条路径比凭经验猜测要可靠得多。