SSMS通过 Query Store 定位慢 SQL 通用SQL模板。

📅 2026/7/9 6:29:54 👁️ 阅读次数 📝 编程学习
SSMS通过 Query Store 定位慢 SQL 通用SQL模板。

典型流程

1. 用户反馈接口慢 2. 用 Query Store 查最近 1 小时总耗时最高 SQL 3. 找到对应 SQL 4. 打开实际执行计划 5. 看慢在哪里:扫描、回表、排序、哈希、估计偏差 6. 调整 SQL 或索引 7. 用 STATISTICS IO/TIME 对比优化前后 8. 再看 Query Store 后续表现是否下降


我们可以打开SSMS直接执行这个SQL模板定位慢SQL是哪一条。

SELECT TOP (20) LEFT(qt.query_sql_text, 500) AS [SQL文本], rs.count_executions AS [执行次数], CONVERT(decimal(18,2), rs.avg_duration / 1000.0) AS [平均耗时_毫秒], CONVERT(decimal(18,2), (rs.avg_duration * rs.count_executions) / 1000.0) AS [总耗时_毫秒], CONVERT(decimal(18,2), rs.avg_cpu_time / 1000.0) AS [平均CPU_毫秒], CONVERT(decimal(18,2), rs.avg_logical_io_reads) AS [平均逻辑读取次数], rsi.start_time AS [统计开始时间], rsi.end_time AS [统计结束时间], p.plan_id AS [执行计划ID] FROM sys.query_store_query_text qt JOIN sys.query_store_query q ON q.query_text_id = qt.query_text_id JOIN sys.query_store_plan p ON p.query_id = q.query_id JOIN sys.query_store_runtime_stats rs ON rs.plan_id = p.plan_id JOIN sys.query_store_runtime_stats_interval rsi ON rsi.runtime_stats_interval_id = rs.runtime_stats_interval_id WHERE rsi.start_time >= DATEADD(hour, -1, SYSUTCDATETIME()) ORDER BY rs.avg_duration * rs.count_executions DESC;

这个可以作为Query Store 慢 SQL 定位通用模板

它的作用是:

查看最近 1 小时内,总耗时最高的前 20 条 SQL。

排序依据是:

平均耗时 × 执行次数 = 总耗时

所以它不是只找“单次最慢”的 SQL,而是找“对系统整体影响最大”的 SQL。

我们直接执行这个sql模板:

本次 Query Store 结果

Query Store 查到 1 条主要 SQL:

SELECT TOP (100) p.ProductId, p.ProductName, SUM(oi.LineAmount) AS Revenue FROM dbo.Products p JOIN dbo.OrderItems oi ON oi.ProductId = p.ProductId GROUP BY p.ProductId, p.ProductName ORDER BY Revenue DESC

对应结果:

执行次数:1 平均耗时:25191.88 ms 总耗时:25191.88 ms 平均 CPU:188.33 ms 平均逻辑读取次数:594 统计时间:2026-07-08 07:30:00 到 07:45:00 UTC 执行计划 ID:31

这条 SQL 的业务含义

这条 SQL 是商品销售额排行榜:

从订单明细 OrderItems 中统计每个商品的销售额, 再关联 Products 拿商品名称, 按销售额从高到低排序, 取前 100 个商品。

它代表一种常见报表场景:

商品销售排行 聚合统计 JOIN 查询 ORDER BY 排序 TOP N 排行榜

为什么它会出现在慢 SQL 里

这条 SQL 天然比普通查询更重,因为它需要:

读取 OrderItems 订单明细 按 ProductId 分组 计算 SUM(LineAmount) JOIN Products 获取商品名称 按 Revenue 排序 取 TOP 100

它不是按主键查一行,而是典型的统计报表 SQL。

本次结果里最明显的是:

平均耗时 = 25191.88 ms

也就是单次执行约 25 秒。

但同时:

平均 CPU = 188.33 ms 平均逻辑读取次数 = 594

CPU 和逻辑读取并不算特别夸张,说明这次 25 秒耗时可能还受到其他因素影响,例如:

等待 阻塞 磁盘读取 首次执行缓存未命中 客户端接收结果 系统负载

所以不能只看平均耗时,还要结合:

执行计划 等待信息 STATISTICS IO/TIME 是否被阻塞 是否首次执行

怎么分析这条 SQL

第一步,看主要表:

OrderItems 是核心表,因为销售额来自 SUM(oi.LineAmount) Products 是维表,用来取 ProductName

第二步,看主要操作:

JOIN GROUP BY SUM ORDER BY TOP

第三步,看执行计划里有没有:

OrderItems 扫描 Hash Match / 哈希匹配 Stream Aggregate / 流聚合 Sort / 排序 Index Scan / 索引扫描

第四步,看消息里的读取:

OrderItems 逻辑读取次数 Products 逻辑读取次数 CPU 时间 占用时间

可能的优化方向

如果这是高频接口或报表,优化方向通常不是只靠一个索引。

可以考虑:

1. 给 OrderItems(ProductId) INCLUDE(LineAmount) 建覆盖索引 2. 给 Products(ProductId) 保持主键索引 3. 增加时间条件,比如只统计最近 30 天 4. 把销售额提前汇总到日报/月报表 5. 对排行榜结果做缓存 6. 避免每次请求都实时扫订单明细做全量聚合

示例索引:

CREATE INDEX IX_OrderItems_ProductId_LineAmount ON dbo.OrderItems(ProductId) INCLUDE (LineAmount);

但如果数据量很大,更推荐:

预聚合表 / 汇总表 / 缓存

例如维护一张:

ProductSalesSummary

里面提前保存:

ProductId TotalRevenue SoldQuantity 统计日期

查询排行榜时直接查汇总表,而不是每次实时扫OrderItems

本次学习结论

Query Store 可以帮助我们从历史执行记录中找出最值得关注的 SQL。 本次定位到的是商品销售额排行榜 SQL。 它慢的原因不是简单的 WHERE 查询慢,而是典型的报表聚合:JOIN + GROUP BY + SUM + ORDER BY。 对于这种 SQL,要重点看 OrderItems 的读取、聚合和排序成本。 如果是生产高频场景,优先考虑覆盖索引、时间范围过滤、预聚合和缓存。

一句话总结:

这个 Query Store 模板用于先找到“最影响系统的 SQL”,然后再结合执行计划和 STATISTICS IO/TIME

另外,关于执行后可能出现没有任何结果的情况:

如果最近 1 小时没有 Query Store 记录,或者 Query Store 还没刷新统计,就会没有结果。

先按这个顺序排查。

1. 先确认 Query Store 是否正常

SELECT actual_state_desc AS [当前状态], readonly_reason AS [只读原因], current_storage_size_mb AS [当前占用MB], max_storage_size_mb AS [最大空间MB] FROM sys.database_query_store_options;

正常希望看到:

当前状态 = READ_WRITE

原因找到了:你的 Query Store 当前是关的。

当前状态 = OFF

所以你执行 Query Store 模板没有结果是正常的,因为数据库根本没有记录查询历史。

只读原因 = 0说明不是空间满或只读问题,就是单纯没开启。

开启 Query Store

在当前数据库执行:

ALTER DATABASE CURRENT SET QUERY_STORE = ON;

然后建议设置成可读写:

ALTER DATABASE CURRENT SET QUERY_STORE ( OPERATION_MODE = READ_WRITE );

开启后验证

SELECT actual_state_desc AS [当前状态], readonly_reason AS [只读原因], current_storage_size_mb AS [当前占用MB], max_storage_size_mb AS [最大空间MB] FROM sys.database_query_store_options;

你希望看到:

当前状态 = READ_WRITE