SQL Server 慢查询怎么定位?一套从"卡"到"快"的完整排查流程
很多 DBA 和后端开发都有类似的经历:系统上线跑得好好的,某天突然接口变慢、CPU 飙高,甚至连接池被打满。老板一句"查一下数据库",你就得从茫茫 SQL 中找出那条"罪魁祸首"。
SQL Server 的慢查询排查,其实有一套非常成熟的"套路"。本文我会结合生产实战,给你一套从现象 → 定位 → 分析 → 优化 → 验证的完整流程,尽量不堆概念,多讲"怎么用"。
一、先搞清楚:慢的是"哪一类问题"
在动手之前,先分清方向,避免"乱查一通"。
慢查询通常分为几类:
| 类型 | 典型特征 |
|---|---|
| 单条 SQL 慢 | 某接口偶尔或持续慢,整体 QPS 不高 |
| 批量 SQL 慢 | 定时任务、报表、数据同步变慢 |
| 并发导致慢 | 平时正常,高峰期变慢,CPU/锁等待高 |
| 偶发性慢 | 无明显规律,可能与统计信息、编译有关 |
排查原则:
先整体(实例级),再局部(库 / SQL);先资源(CPU、IO、内存),再执行计划。
二、第一步:用 DMV 快速找到"最可疑"的 SQL
SQL Server 自带大量动态管理视图(DMV),不需要开 Trace,就能快速定位问题 SQL。
1. 查"最耗 CPU"的 SQL
vbnet
SELECT TOP 20
qs.total_worker_time / 1000 AS total_cpu_ms,
qs.execution_count,
qs.total_worker_time / qs.execution_count / 1000 AS avg_cpu_ms,
SUBSTRING(
st.text,
(qs.statement_start_offset/2) + 1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2) + 1
) AS statement_text,
qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
ORDER BY qs.total_worker_time DESC;
重点关注:
total_cpu_ms:累计 CPU 消耗avg_cpu_ms:单次平均 CPUexecution_count:执行频率(高频 + 高 CPU = 典型性能杀手)
2. 查"最耗 IO"的 SQL
vbnet
SELECT TOP 20
qs.total_logical_reads AS total_reads,
qs.total_logical_writes AS total_writes,
qs.execution_count,
(qs.total_logical_reads + qs.total_logical_writes) / qs.execution_count AS avg_io,
SUBSTRING(
st.text,
(qs.statement_start_offset/2) + 1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2) + 1
) AS statement_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY (qs.total_logical_reads + qs.total_logical_writes) DESC;
经验:
- 逻辑读高,但返回行数少 → 大概率是索引缺失 / 索引失效
- 写操作多 → 关注是否有大量 UPDATE / DELETE 未分批
3. 查"执行最久"的 SQL(Duration)
vbnet
SELECT TOP 20
qs.total_elapsed_time / 1000 AS total_elapsed_ms,
qs.total_elapsed_time / qs.execution_count / 1000 AS avg_elapsed_ms,
qs.execution_count,
SUBSTRING(
st.text,
(qs.statement_start_offset/2) + 1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2) + 1
) AS statement_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY qs.total_elapsed_time DESC;
三、第二步:看"正在发生什么"------实时会话排查
DMV 是历史统计,如果问题正在发生,需要看实时会话。
1. 查看当前正在执行的请求
sql
SELECT
r.session_id,
r.status,
r.command,
r.wait_type,
r.wait_time,
r.cpu_time,
r.total_elapsed_time,
t.text AS sql_text,
r.blocking_session_id
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.session_id <> @@SPID
ORDER BY r.total_elapsed_time DESC;
关键字段解读:
wait_type:当前等待类型(PAGEIOLATCH_SH = IO 慢,LCK_M_ = 锁等待)blocking_session_id:被谁阻塞status:RUNNING / SUSPENDED / RUNNABLE
2. 查看阻塞链(谁锁了谁)
sql
SELECT
session_id,
blocking_session_id,
wait_type,
wait_time,
wait_resource,
text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle)
WHERE blocking_session_id <> 0;
常见阻塞场景:
- 长事务未提交(BEGIN TRAN 后忘了 COMMIT)
- 大批量 UPDATE / DELETE 没分批
- 不合理的事务隔离级别(如 Serializable)
四、第三步:重点分析执行计划(慢查询的"真相")
找到可疑 SQL 后,90% 的问题都在执行计划里。
1. 如何拿到执行计划
- SSMS:点击"包括实际执行计划"(Ctrl + M)
- DMV 中:
sys.dm_exec_query_plan(plan_handle) - 计划缓存:
sys.dm_exec_cached_plans
2. 执行计划中"必看"的红色/黄色警告
| 警告 | 含义 |
|---|---|
| Table Scan | 全表扫描,缺少合适索引 |
| Index Scan | 索引扫描,可能索引设计不合理 |
| Key Lookup | 书签查找,RID/键查找导致大量随机 IO |
| Sort(无索引) | 内存排序,数据量大时很慢 |
| Hash Match(大表) | 哈希连接,内存压力大 |
| 实际行数 >> 预估行数 | 统计信息过期 |
3. 一个典型慢 SQL 示例
sql
SELECT *
FROM Orders
WHERE CustomerID = @CustomerID
ORDER BY CreateTime DESC;
问题:
SELECT *导致 Key LookupCreateTime排序无索引- 可能缺失
(CustomerID, CreateTime)联合索引
优化后:
scss
CREATE INDEX IX_Orders_CustomerID_CreateTime
ON Orders(CustomerID, CreateTime DESC)
INCLUDE (OrderStatus, TotalAmount);
五、第四步:检查索引与统计信息
1. 缺失索引建议(来自 SQL Server 自身)
vbnet
SELECT
migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) AS improvement_measure,
mid.statement,
mid.equality_columns,
mid.inequality_columns,
mid.included_columns
FROM sys.dm_db_missing_index_group_stats migs
JOIN sys.dm_db_missing_index_groups mig
ON migs.group_handle = mig.index_group_handle
JOIN sys.dm_db_missing_index_details mid
ON mig.index_handle = mid.index_handle
ORDER BY improvement_measure DESC;
注意:
- 这是"建议",不是"照抄"
- 不要在一个表上建几十个索引,写性能会下降
2. 统计信息是否过期
scss
SELECT
object_name(stat.object_id) AS table_name,
stat.name AS stats_name,
STATS_DATE(stat.object_id, stat.stats_id) AS last_updated,
modification_counter
FROM sys.stats stat
CROSS APPLY sys.dm_db_stats_properties(stat.object_id, stat.stats_id) AS sp
WHERE STATS_DATE(stat.object_id, stat.stats_id) < DATEADD(DAY, -7, GETDATE())
ORDER BY modification_counter DESC;
经验:
- 大表建议开启自动更新统计信息
- 数据分布倾斜严重的列,可手动更新统计信息
六、第五步:系统级资源瓶颈排查
如果 SQL 本身看起来"没那么差",就要看是不是被系统资源拖慢。
1. 等待统计(全局视角)
vbnet
SELECT TOP 10
wait_type,
wait_time_ms,
wait_time_ms - signal_wait_time_ms AS resource_wait_ms,
signal_wait_time_ms,
waiting_tasks_count
FROM sys.dm_os_wait_stats
ORDER BY wait_time_ms DESC;
常见等待类型速查:
| 等待类型 | 含义 |
|---|---|
| PAGEIOLATCH_SH | 磁盘 IO 慢 |
| LCK_M_IX / LCK_M_U | 锁等待 |
| ASYNC_NETWORK_IO | 客户端接收慢(返回数据太多) |
| SOS_SCHEDULER_YIELD | CPU 争抢 |
| WRITELOG | 日志写入慢(磁盘 / 事务大) |
2. 内存压力检查
vbnet
SELECT
total_physical_memory_kb / 1024 AS total_mem_mb,
available_physical_memory_kb / 1024 AS available_mem_mb,
system_memory_state_desc
FROM sys.dm_os_sys_memory;
现象:
- Page Life Expectancy(PLE)过低
- 大量 Lazy Writes / Free List Stalls
七、第六步:历史慢查询 ------ 用 Query Store(强烈推荐)
SQL Server 2016+ 的 Query Store 是排查"昨天慢、今天慢"的神器。
1. 开启 Query Store
ini
ALTER DATABASE YourDB
SET QUERY_STORE = ON
(
OPERATION_MODE = READ_WRITE,
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30)
);
2. 按耗时排名查询
vbnet
SELECT TOP 20
q.query_id,
t.query_sql_text,
rs.avg_duration / 1000 AS avg_duration_ms,
rs.avg_cpu_time / 1000 AS avg_cpu_ms,
rs.avg_logical_io_reads
FROM sys.query_store_query q
JOIN sys.query_store_query_text t
ON q.query_text_id = t.query_text_id
JOIN sys.query_store_plan p
ON q.query_id = p.query_id
JOIN sys.query_store_runtime_stats rs
ON p.plan_id = rs.plan_id
ORDER BY rs.avg_duration DESC;
优势:
- 可以看到"执行计划变更"导致的性能回退
- 支持"强制旧计划"(Plan Forcing)
八、一套完整的排查流程总结(可直接当 SOP 用)
生产环境慢查询排查 SOP:
- 确认现象:慢的是接口 / 任务 / 整体?
- 实例级 :
sys.dm_os_wait_stats看等待类型 - SQL 级 :
sys.dm_exec_query_stats找高 CPU / 高 IO / 高 Duration SQL - 实时级 :
sys.dm_exec_requests看当前阻塞和等待 - 执行计划:重点看 Scan、Lookup、Sort、预估偏差
- 索引/统计:缺失索引 + 统计信息过期
- 历史对比:Query Store 看执行计划变化
- 优化验证:改 SQL / 加索引 / 调整参数后,重新跑并对比指标
九、常见"坑位"提醒(经验之谈)
- 参数嗅探问题 :同一 SQL,不同参数性能差异巨大 →
OPTION (RECOMPILE) - 隐式转换 :
VARCHARvsNVARCHAR→ 索引失效 - OR 条件 :
WHERE a = @a OR b = @b→ 很难走索引 - 返回数据太多 :
SELECT *+ 前端分页 → 实际是数据库在分页 - 大事务:一个事务里更新上万行 → 锁 + 日志 + 回滚风险
十、写在最后
SQL Server 慢查询排查,本质上是一个 "数据驱动"的过程:
不是"我觉得这条 SQL 慢",而是"DMV / 执行计划 / 等待统计告诉我它慢,以及为什么慢"。
这套流程我在多个生产环境中反复验证过,基本能覆盖 90% 的慢查询场景。你可以把它收藏起来,下次数据库"卡了",按步骤来,效率会高很多。