4. Query Store目录视图初探

Query Store 把捕获到的查询、计划与运行时统计分别落在多张系统目录视图中,这些视图位于每个启用了 Query Store 的数据库内,通过 sys.query_store_* 命名空间访问。理解它们之间的主键与外键关系,是后续做性能治理的前提:只有先知道“一条查询文本存在哪、它的多个计划存在哪、每个计划在不同时间窗口的统计存在哪”,才能写出正确的关联查询,而不是把不同粒度的数据错误地拼在一起。

核心视图的分工

最常用的四张视图构成一条从“查询文本”到“执行统计”的链路:

  • sys.query_store_query_text:保存去重后的查询文本,主键 query_text_id,同一段 SQL 文本只存一份。
  • sys.query_store_query:保存查询的编译上下文,主键 query_id,通过 query_text_id 关联文本;同一文本在不同 SET 选项或不同数据库上下文下会生成不同的 query_id。
  • sys.query_store_plan:保存该查询的每个执行计划,主键 plan_id,通过 query_id 关联查询;一个查询可以有多个计划,对应计划回退或重编译。
  • sys.query_store_runtime_stats:保存计划在时间窗口内的聚合运行时统计,通过 plan_id 关联计划,并带有 runtime_stats_interval_id 指向 sys.query_store_runtime_stats_interval 定义的时间区间。

此外还有 sys.query_store_wait_stats(等待类别统计)和 sys.query_store_plan_feedback(计划反馈)等辅助视图,它们同样以 plan_id 为关联键。下图展示了这条关联链以及各视图的关键列,箭头方向表示外键引用方向。

Query Store 目录视图关联链 sys.query_store_query_text query_text_id (PK) query_sql_text statement_sql_handle sys.query_store_query query_id (PK) query_text_id (FK) object_id / context_settings_id is_internal_query sys.query_store_plan plan_id (PK) query_id (FK) query_plan (XML) is_forced_plan / force_failure_count runtime_stats_interval runtime_stats_interval_id (PK) start_time / end_time interval_type_desc sys.query_store_runtime_stats plan_id (FK) runtime_stats_interval_id (FK) count_executions avg_duration / avg_cpu_time avg_logical_io_reads sys.query_store_wait_stats plan_id (FK) wait_category_desc avg_query_wait_time_ms query_text_id query_id plan_id interval_id plan_id 关联顺序:query_text → query → plan → runtime_stats / wait_stats

从查询文本定位到运行时统计

实际排查时,通常先拿到一段可疑 SQL 或一个存储过程名,再逐层向下钻取。下面这段示例按 query_sql_text 模糊匹配定位查询,并汇总其各计划在最近时间窗口内的平均耗时与执行次数。示例中的文本与数值均为教学假设,实际结果取决于目标实例的负载。

SELECT
    qt.query_sql_text,
    q.query_id,
    p.plan_id,
    p.is_forced_plan,
    rs.count_executions,
    rs.avg_duration / 1000.0        AS avg_duration_ms,
    rs.avg_cpu_time / 1000.0        AS avg_cpu_ms,
    rs.avg_logical_io_reads,
    rsi.start_time,
    rsi.end_time
FROM sys.query_store_query_text AS qt
JOIN sys.query_store_query       AS q  ON q.query_text_id = qt.query_text_id
JOIN sys.query_store_plan        AS p  ON p.query_id      = q.query_id
JOIN sys.query_store_runtime_stats AS rs ON rs.plan_id    = p.plan_id
JOIN sys.query_store_runtime_stats_interval AS rsi
     ON rsi.runtime_stats_interval_id = rs.runtime_stats_interval_id
WHERE qt.query_sql_text LIKE N'%SalesOrderHeader%'
  AND rsi.start_time >= DATEADD(HOUR, -24, GETUTCDATE())
ORDER BY rs.avg_duration DESC;

这条语句体现了几个关键点:avg_duration、avg_cpu_time 等列的单位是微秒,除以 1000 得到毫秒更便于阅读;runtime_stats_interval_id 必须与区间视图关联,否则无法判断统计属于哪个时间窗口;同一 query_id 可能返回多行 plan_id,这正是计划回退的信号。

识别计划回退与强制计划

当同一查询出现多个 plan_id 且各自都有执行记录时,说明优化器在不同时间选择了不同计划。可以按 query_id 分组,统计计划数量与各计划的平均耗时差异:

SELECT
    q.query_id,
    COUNT(DISTINCT p.plan_id)              AS plan_count,
    MIN(rs.avg_duration) / 1000.0          AS best_avg_ms,
    MAX(rs.avg_duration) / 1000.0          AS worst_avg_ms,
    SUM(rs.count_executions)               AS total_execs
FROM sys.query_store_query AS q
JOIN sys.query_store_plan  AS p  ON p.query_id = q.query_id
JOIN sys.query_store_runtime_stats AS rs ON rs.plan_id = p.plan_id
GROUP BY q.query_id
HAVING COUNT(DISTINCT p.plan_id) > 1
ORDER BY worst_avg_ms - best_avg_ms DESC;

若 best_avg_ms 与 worst_avg_ms 差距显著,且执行次数都不可忽略,就属于典型的计划不稳定。此时可进一步查看 p.is_forced_plan 与 p.force_failure_count:前者为 1 表示该计划已被强制,后者大于 0 表示强制失败过,需要检查计划是否仍兼容当前对象结构。

查询 sys.query_store_plan 时,query_plan 列是 XML 类型,直接 SELECT 会返回大段 XML。建议用 p.query_plan.query('...') 提取特定节点,或先只取 plan_id 再按需展开,避免结果集过大影响排查效率。

等待统计的补充视角

运行时统计回答“慢了多少”,等待统计回答“慢在哪里”。sys.query_store_wait_stats 以 plan_id 和区间为粒度,按 wait_category_desc 分类记录等待时间。将它与运行时统计按相同 plan_id 关联,可以判断某计划的高耗时是 CPU 密集还是被锁、IO 或并行等待拖累:

SELECT
    ws.plan_id,
    ws.wait_category_desc,
    ws.avg_query_wait_time_ms,
    ws.total_query_wait_time_ms
FROM sys.query_store_wait_stats AS ws
WHERE ws.plan_id = 42          -- 替换为实际 plan_id
ORDER BY ws.total_query_wait_time_ms DESC;

需要注意,等待统计并非所有版本都默认采集,且其区间与运行时统计区间一一对应。若某 plan_id 在等待统计中无记录,应先确认 Query Store 的等待统计采集是否开启,而不是直接判定该计划没有等待。

使用目录视图时的常见判断依据

观察点含义后续动作
同一 query_id 多个 plan_id计划回退或重编译比较各计划 avg_duration,评估是否强制稳定计划
count_executions 很小但 avg_duration 很高偶发慢查询,样本不足延长观察窗口,避免过早下结论
avg_logical_io_reads 异常高可能缺失索引或统计信息过期结合执行计划查看扫描算子
is_forced_plan = 1 且 force_failure_count > 0强制计划失效检查对象变更,重新评估强制策略
等待类别集中在 Lock / IO瓶颈不在 CPU转向阻塞链或存储层排查