SQL Server 慢查询怎么定位?一套从“卡”到“快”的完整排查流程

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:单次平均 CPU
  • execution_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 Lookup
  • CreateTime 排序无索引
  • 可能缺失 (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:

  1. 确认现象:慢的是接口 / 任务 / 整体?
  2. 实例级sys.dm_os_wait_stats 看等待类型
  3. SQL 级sys.dm_exec_query_stats 找高 CPU / 高 IO / 高 Duration SQL
  4. 实时级sys.dm_exec_requests 看当前阻塞和等待
  5. 执行计划:重点看 Scan、Lookup、Sort、预估偏差
  6. 索引/统计:缺失索引 + 统计信息过期
  7. 历史对比:Query Store 看执行计划变化
  8. 优化验证:改 SQL / 加索引 / 调整参数后,重新跑并对比指标

九、常见"坑位"提醒(经验之谈)

  • 参数嗅探问题 :同一 SQL,不同参数性能差异巨大 → OPTION (RECOMPILE)
  • 隐式转换VARCHAR vs NVARCHAR → 索引失效
  • OR 条件WHERE a = @a OR b = @b → 很难走索引
  • 返回数据太多SELECT * + 前端分页 → 实际是数据库在分页
  • 大事务:一个事务里更新上万行 → 锁 + 日志 + 回滚风险

十、写在最后

SQL Server 慢查询排查,本质上是一个 "数据驱动"的过程

不是"我觉得这条 SQL 慢",而是"DMV / 执行计划 / 等待统计告诉我它慢,以及为什么慢"。

这套流程我在多个生产环境中反复验证过,基本能覆盖 90% 的慢查询场景。你可以把它收藏起来,下次数据库"卡了",按步骤来,效率会高很多。

相关推荐
水深火乐1 小时前
interface和any
后端
步行cgn1 小时前
MyBatis Error evaluating expression ‘ids‘. Return value (3) was not iterable 错误详
java·后端
神奇小汤圆1 小时前
Spring Boot + LangChain4j实现RAG——从零搭建企业级知识库问答系统
后端
H_Peak1 小时前
Python运算符与流程控制:if条件判断
后端
H_Peak1 小时前
Python字符串操作:从基础到正则入门
后端
自进化Agent智能体1 小时前
Hermes 子代理委派 —— 并行工作的艺术
后端
步行cgn1 小时前
MyBatis 动态 SQL 中的 <foreach> 标签完全解析
后端
SomeB1oody2 小时前
【RustyML入门】5.0. 模型评估
开发语言·后端·机器学习·rust·教程
唐青枫3 小时前
别只把 enum 当常量列表:Zig 枚举、状态机与 Tagged Union 实战
后端