news 2026/10/12 20:28:32

Azure SQL Query Store实战:缺失索引排查与优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Azure SQL Query Store实战:缺失索引排查与优化

1. 从一个真实场景说起:为什么我盯上了Query Store

数据库性能问题最让人头疼的地方,不是问题本身有多复杂,而是你根本不知道问题出在哪。我见过太多团队,数据库跑着跑着突然某条查询从几十毫秒变成几秒,开发说SQL没改过,运维说服务器资源够用,DBA说执行计划看着也没毛病——三方扯皮,最后不了了之,等下次高峰期再炸一遍。

这种场景下,Query Store就是那个能帮你“翻旧账”的东西。它不像传统的DMV那样只给你当前时刻的快照,而是持续记录查询的执行历史,包括执行计划、运行时统计、等待类型等等。你可以理解为:以前你只能看到病人现在的体温,现在你有了过去一个月的体温曲线。

这一篇是Azure SQL DB/DW系列的第4篇,专门聊Query Store在实际案例中的应用。第一个案例我选了“缺失索引”——这是最常见、最容易见效、也最容易被误用的优化手段。说它常见,是因为几乎每个性能问题排查到最后都会碰到索引缺失的提示;说它容易见效,加个索引可能直接让查询快十倍;说它容易被误用,是因为很多人看到“缺失索引”的提示就无脑加,结果写性能急剧下降,得不偿失。

这篇文章适合谁看?如果你是在用Azure SQL Database或者Azure Synapse Analytics(原SQL DW)的开发或运维人员,平时需要处理查询性能问题,对Query Store只有耳闻但没实际用过,那这篇就是写给你的。如果你已经在用Query Store但只停留在“打开它”的阶段,这篇文章会告诉你怎么真正把它用起来。

我个人的习惯是:任何Azure SQL DB的性能排查,第一步永远是打开Query Store,第二步是看Top Resource Consumers,第三步才是根据具体情况深入。缺失索引的发现和验证,恰好是这三步的典型串联。

2. Query Store到底解决了什么痛点

2.1 传统排查手段的局限性

在没有Query Store之前,我们排查性能问题主要靠这几样东西:sys.dm_exec_query_stats、sys.dm_exec_sql_text、sys.dm_exec_query_plan,再加上SQL Server Profiler或者Extended Events。这套组合拳不是不能用,但有几个硬伤。

第一个硬伤是数据易失。DMV里的数据只反映当前缓存中的内容,一旦发生内存压力、服务重启、或者计划被逐出缓存,历史数据就没了。你上午发现的问题,下午想回头查,可能什么痕迹都找不到。更别说计划缓存被刷掉之后,连当时用的什么执行计划都还原不出来。

第二个硬伤是缺少时间维度。DMV给你的是累计值,你只能看到“这条查询总共执行了多少次、总共消耗了多少CPU”,但你看不到“它在过去一小时内每分钟的CPU消耗趋势”。这就导致你很难判断一个问题是在恶化、在好转、还是稳定存在。

第三个硬伤是执行计划的历史对比困难。同一个查询,可能因为统计信息更新、参数嗅探、或者数据分布变化,在不同时间用了不同的执行计划。传统手段只能看到当前缓存的计划,想看历史计划?除非你之前手动抓过,否则没戏。

2.2 Query Store的核心机制

Query Store本质上是在数据库内部建了一套“飞行记录仪”。它把查询的编译信息、执行计划、运行时统计持久化到内部表中,这些数据不受内存压力影响,也不受重启影响(当然有保留策略控制)。它的工作流程大致是这样的:

当一条查询被编译时,Query Store记录下它的SQL文本和初始执行计划。每次执行时,运行时统计(执行次数、持续时间、CPU、逻辑读、物理读、内存授予等)被聚合到固定的时间窗口(默认是60分钟,可配置)。如果执行计划发生变化,新计划也会被记录下来,并且和之前的计划做关联。

这里有个关键设计:Query Store的数据是按时间区间聚合的,不是每次执行都存一条。默认情况下,它把一小时内同一查询同一计划的执行统计合并成一行。这个设计很聪明,既控制了数据量,又保留了趋势信息。你可以通过sys.query_store_runtime_stats视图看到每个时间区间的聚合数据。

2.3 在Azure SQL DB中的特殊价值

Azure SQL Database和本地SQL Server在Query Store的使用上有一些差异,这些差异直接影响你的排查策略。

首先是默认开启状态。Azure SQL Database从某个时间点开始,新建数据库默认开启Query Store,而且默认配置是“自动”模式。这意味着你拿到一个新库,很可能Query Store已经在跑了,不需要你手动开启。但要注意,自动模式下的保留策略可能比较激进,数据保留时间有限,如果你要排查很久以前的问题,可能得提前调整配置。

其次是没有本地文件系统的概念。本地SQL Server的Query Store数据存在文件组里,你可以直接看文件大小。Azure SQL DB里你只能通过sys.database_query_store_options看当前空间使用情况,而且空间上限受服务层级影响。Basic和Standard层级的空间限制比较紧,Premium和Business Critical层级宽松很多。如果你在低层级上跑Query Store,要特别关注空间回收。

第三是和Query Performance Insight的集成。Azure门户里有个Query Performance Insight功能,底层就是Query Store。你可以直接在门户里看Top CPU、Top Duration、Top Execution Count的查询,不用写T-SQL。但门户的功能比较浅,真要深入分析还是得自己查Query Store视图。

注意:Azure SQL Database的Query Store默认操作模式是AUTO,这个模式下如果Query Store自己觉得“压力大”,可能会自动切换到只读模式,停止写入新数据。排查问题时如果发现数据有断档,先检查actual_state_desc是不是READ_ONLY。

3. 缺失索引案例的完整排查过程

3.1 案例背景与初始现象

假设我们有一个典型的电商订单库,跑在Azure SQL Database的S3层级上。最近业务方反馈“订单查询页面变慢了”,具体表现是:按用户ID查最近订单列表,以前一两秒出结果,现在要五六秒,高峰期更慢。

这个场景太常见了,常见到很多人会直接说“加个索引不就完了”。但我的习惯是先看Query Store,确认问题到底出在哪条查询上,再决定怎么处理。因为“订单查询页面慢”可能对应好几条SQL,盲目加索引可能加错地方。

第一步,确认Query Store状态:

SELECT desired_state_desc, actual_state_desc, readonly_reason, current_storage_size_mb, max_storage_size_mb, query_capture_mode_desc, size_based_cleanup_mode_desc, stale_query_threshold_days FROM sys.database_query_store_options;

如果actual_state_desc不是READ_WRITE,说明Query Store没在正常记录,得先处理这个问题。常见原因是空间满了或者被手动切成了只读。

3.2 用Query Store定位问题查询

确认Query Store正常后,下一步是找出“谁在消耗资源”。我一般会按CPU时间排序,因为CPU消耗高的查询通常也是影响面最大的:

SELECT TOP 10 q.query_id, qt.query_sql_text, SUM(rs.count_executions) AS total_executions, SUM(rs.avg_cpu_time * rs.count_executions) / 1000 AS total_cpu_ms, SUM(rs.avg_duration * rs.count_executions) / 1000 AS total_duration_ms, SUM(rs.avg_logical_io_reads * rs.count_executions) AS total_logical_reads FROM sys.query_store_query q JOIN sys.query_store_query_text qt ON q.query_text_id = qt.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 JOIN sys.query_store_runtime_stats_interval rsi ON rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id WHERE rsi.start_time >= DATEADD(HOUR, -24, GETUTCDATE()) GROUP BY q.query_id, qt.query_sql_text ORDER BY total_cpu_ms DESC;

这个查询会给你过去24小时内CPU消耗最高的10条查询。注意我用了GETUTCDATE(),因为Query Store的时间戳是UTC的,如果你用本地时间可能会漏数据。

跑完这个查询,假设我们发现一条查询特别扎眼:它的总CPU时间远高于其他查询,而且执行次数很多。看SQL文本,大概是这样:

SELECT OrderId, OrderDate, TotalAmount, Status FROM Orders WHERE CustomerId = @CustomerId AND OrderDate >= @StartDate ORDER BY OrderDate DESC;

这就是典型的“按用户查订单列表”的查询。接下来要看它的执行计划,确认是不是因为缺索引导致的全表扫描。

3.3 从执行计划中确认缺失索引

Query Store里看执行计划有两种方式:看图形化计划(在SSMS或Azure Data Studio里点开),或者直接查plan XML。我一般先用XML快速扫一眼有没有“Missing Index”的提示:

SELECT p.plan_id, p.query_plan, rs.avg_duration, rs.avg_logical_io_reads, rs.count_executions FROM sys.query_store_plan p JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id WHERE p.query_id = @QueryId ORDER BY rs.runtime_stats_interval_id DESC;

把query_plan字段点开,搜索“MissingIndexes”。如果执行计划里包含缺失索引的提示,你会看到类似这样的XML片段:

<MissingIndexes> <MissingIndexGroup Impact="89.23"> <MissingIndex Database="[OrderDB]" Schema="[dbo]" Table="[Orders]"> <ColumnGroup Usage="EQUALITY"> <Column Name="[CustomerId]" ColumnId="2" /> </ColumnGroup> <ColumnGroup Usage="INEQUALITY"> <Column Name="[OrderDate]" ColumnId="3" /> </ColumnGroup> <ColumnGroup Usage="INCLUDE"> <Column Name="[TotalAmount]" ColumnId="4" /> <Column Name="[Status]" ColumnId="5" /> </ColumnGroup> </MissingIndex> </MissingIndexGroup> </MissingIndexes>

这个提示告诉你:如果建一个索引,以CustomerId为等值条件列,OrderDate为不等值条件列,然后INCLUDE TotalAmount和Status,优化器估计能提升89.23%的性能。

但这里有个关键点:不要直接照搬这个提示。缺失索引提示只是优化器基于当前查询的“建议”,它不考虑其他查询的需求,也不考虑写操作的成本。我见过太多人直接按提示建索引,结果索引建了一堆,写性能掉了一半。

3.4 验证索引效果的正确姿势

在真正建索引之前,我强烈建议做两件事:一是用Query Store的历史数据确认这个查询的执行计划是否稳定,二是用“假设索引”功能预估效果。

先看执行计划稳定性。如果同一个query_id对应多个plan_id,说明执行计划发生过变化。你可以这样查:

SELECT p.plan_id, p.is_forced_plan, p.force_failure_count, rs.avg_duration, rs.avg_logical_io_reads, rs.count_executions, rsi.start_time, rsi.end_time FROM sys.query_store_plan p JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id JOIN sys.query_store_runtime_stats_interval rsi ON rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id WHERE p.query_id = @QueryId ORDER BY rsi.start_time DESC;

如果发现某个计划突然变差(比如逻辑读从几百跳到几万),那可能是统计信息过期或者参数嗅探导致的。这种情况下,加索引可能有用,但更根本的解法可能是更新统计信息或者用OPTION (RECOMPILE)。

再看假设索引。SQL Server和Azure SQL DB都支持用WITH (INDEX(...))提示来模拟索引效果,但更准确的方式是用Database Engine Tuning Advisor(本地)或者Query Store的“计划强制”功能(Azure)。在Azure SQL DB里,你可以先建一个测试库,把索引建上去,跑同样的查询对比。如果不想建测试库,至少用SET STATISTICS IO ON和SET STATISTICS TIME ON在加索引前后各跑一次,对比逻辑读和CPU时间。

我个人的经验是:如果一个缺失索引提示的Impact超过80%,而且对应的查询是读多写少的场景,那加索引基本稳赚。如果Impact在50%以下,或者表本身写操作很频繁,就要慎重了。

4. 索引创建与后续验证的实操细节

4.1 索引创建语句的编写要点

假设经过验证,我们决定加索引。根据缺失索引提示和实际查询模式,索引语句大概是这样:

CREATE NONCLUSTERED INDEX IX_Orders_CustomerId_OrderDate ON dbo.Orders (CustomerId, OrderDate DESC) INCLUDE (TotalAmount, Status) WITH (ONLINE = ON, MAXDOP = 2);

这里有几个细节值得展开说。

键列顺序:CustomerId在前,OrderDate在后。因为查询里CustomerId是等值条件,OrderDate是不等值条件(>=)。索引的键列顺序应该遵循“等值列在前,不等值列在后”的原则,这样优化器能最大化利用索引的查找能力。如果反过来,OrderDate在前,那对于“某个用户的订单”这个查询,索引就得先扫所有OrderDate满足条件的行,再过滤CustomerId,效率差很多。

排序方向:OrderDate用了DESC。因为查询里有ORDER BY OrderDate DESC,如果索引的排序方向和查询一致,优化器可以直接用索引的有序性避免排序操作。但要注意,如果同一个索引还要服务ORDER BY OrderDate ASC的查询,那DESC可能就不合适了。这种情况下可以考虑建两个索引,或者接受排序开销。

INCLUDE列:TotalAmount和Status放在INCLUDE里,而不是键列里。因为这两个列只出现在SELECT列表里,不参与WHERE过滤和JOIN条件。放在INCLUDE里可以避免索引键过大,同时让查询变成覆盖查询(Covering Query),直接从索引里取数据,不用回表。

ONLINE = ON:在Azure SQL Database里,ONLINE = ON是默认支持的(除了Basic层级),建索引时不会阻塞写操作。但要注意,ONLINE建索引需要更多空间和更长时间,如果表特别大,得评估影响。

MAXDOP = 2:限制并行度。Azure SQL Database的S3层级有计算资源限制,如果建索引时把所有CPU都占了,可能影响其他查询。设成2是个保守的选择,具体值可以根据服务层级调整。

4.2 索引创建后的效果验证

索引建完后,不能只看“感觉快了”,得有数据支撑。我一般会做三件事。

第一,清空Query Store的旧数据或者等一个新的时间窗口,然后跑同样的业务查询,对比加索引前后的运行时统计。Query Store会自动记录新计划,你可以这样对比:

SELECT p.plan_id, rs.avg_duration, rs.avg_cpu_time, rs.avg_logical_io_reads, rs.count_executions, rsi.start_time FROM sys.query_store_plan p JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id JOIN sys.query_store_runtime_stats_interval rsi ON rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id WHERE p.query_id = @QueryId ORDER BY rsi.start_time DESC;

如果新计划的avg_logical_io_reads从几万降到几百,avg_duration从几秒降到几十毫秒,那说明索引生效了。

第二,检查写操作有没有受影响。加索引最怕的就是INSERT/UPDATE/DELETE变慢。你可以查一下Orders表相关的写操作在加索引前后的平均持续时间:

SELECT qt.query_sql_text, rs.avg_duration, rs.count_executions, rsi.start_time FROM sys.query_store_query q JOIN sys.query_store_query_text qt ON q.query_text_id = qt.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 JOIN sys.query_store_runtime_stats_interval rsi ON rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id WHERE qt.query_sql_text LIKE '%INSERT%Orders%' OR qt.query_sql_text LIKE '%UPDATE%Orders%' OR qt.query_sql_text LIKE '%DELETE%Orders%' ORDER BY rsi.start_time DESC;

如果写操作的平均持续时间明显上升,比如从1毫秒变成5毫秒,而读操作的收益又不足以抵消这个代价,那可能得考虑调整索引或者干脆删掉。

第三,观察一段时间内的整体资源趋势。Query Store的sys.query_store_runtime_stats_interval视图可以看每个时间区间的整体统计:

SELECT rsi.start_time, rsi.end_time, SUM(rs.avg_cpu_time * rs.count_executions) / 1000 AS total_cpu_ms, SUM(rs.avg_logical_io_reads * rs.count_executions) AS total_logical_reads, SUM(rs.count_executions) AS total_executions FROM sys.query_store_runtime_stats rs JOIN sys.query_store_runtime_stats_interval rsi ON rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id WHERE rsi.start_time >= DATEADD(DAY, -7, GETUTCDATE()) GROUP BY rsi.start_time, rsi.end_time ORDER BY rsi.start_time DESC;

如果加索引后整体CPU和逻辑读都下降了,那说明优化是正向的。

4.3 索引维护的长期策略

加索引不是一劳永逸的事。随着数据量增长和查询模式变化,今天的好索引可能变成明天的坏索引。我一般会建议团队建立这样的维护节奏:

每月跑一次“索引使用情况”检查,看看哪些索引几乎没被用过,哪些索引的写代价远大于读收益。Query Store本身不直接提供索引使用统计,但你可以结合sys.dm_db_index_usage_stats和Query Store的执行计划来推断。

每季度做一次“缺失索引”复查。Query Store里积累的缺失索引提示可以作为参考,但不要盲目采纳。重点看那些Impact高、出现频率高、且对应查询是核心业务的提示。

每次发版后观察一周。新功能上线往往带来新的查询模式,Query Store能帮你快速发现新出现的性能问题。

提示:Azure SQL Database的Query Store有“自动计划修正”功能(Automatic Plan Correction),可以自动检测计划回归并强制使用好的计划。但这个功能不能替代索引优化,它解决的是计划选择问题,不是索引缺失问题。

5. 常见问题与排查技巧实录

5.1 Query Store数据不完整或断档

这是最常见的问题之一。表现是:你明明知道某个时间段有问题,但Query Store里就是没有那段时间的数据。原因通常有这几个:

空间不足导致自动切换到只读。Azure SQL Database的Query Store空间上限取决于服务层级。Basic层级只有10MB左右,S3层级大概100MB,Premium层级可以到1GB。如果数据写入速度超过清理速度,Query Store会进入READ_ONLY状态。你可以查readonly_reason字段确认:

SELECT actual_state_desc, readonly_reason, current_storage_size_mb, max_storage_size_mb FROM sys.database_query_store_options;

readonly_reason是个位掩码,常见值有:1表示空间满,2表示内部错误,4表示手动设置只读,8表示达到空间上限。如果是空间问题,可以调大max_storage_size_mb(如果服务层级允许),或者调小INTERVAL_LENGTH_MINUTES让聚合更粗粒度,减少数据量。

保留策略太激进。Query Store默认的STALE_QUERY_THRESHOLD_DAYS是30天,但实际保留时间还受空间清理策略影响。如果空间紧张,旧数据会被优先清理。你可以查一下最早的数据是什么时候的:

SELECT MIN(start_time) AS earliest_data, MAX(end_time) AS latest_data FROM sys.query_store_runtime_stats_interval;

如果最早数据只有几天前,而你需要更长的历史,那就得调整保留策略或者升级服务层级。

查询捕获模式设置不当。Query Store有三种捕获模式:ALL(捕获所有查询)、AUTO(自动过滤不重要的查询)、NONE(不捕获)。AUTO模式下,一些执行次数少、资源消耗低的查询不会被记录。如果你要排查的恰好是这类查询,就得临时改成ALL模式。但要注意,ALL模式会产生大量数据,可能加速空间耗尽。

5.2 缺失索引提示不准确或误导

缺失索引提示是优化器基于单个查询给出的建议,它有几个已知的局限性:

不考虑现有索引。有时候优化器提示的索引和你已有的某个索引高度重叠,只是列顺序或INCLUDE列略有不同。这种情况下,与其新建索引,不如调整现有索引。你可以用下面的查询看看现有索引的定义:

SELECT i.name AS index_name, i.type_desc, STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) AS key_columns, STRING_AGG(CASE WHEN ic.is_included_column = 1 THEN c.name END, ', ') AS included_columns FROM sys.indexes i JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE i.object_id = OBJECT_ID('dbo.Orders') GROUP BY i.name, i.type_desc;

不考虑写操作成本。优化器只关心读查询的性能,不关心索引对INSERT/UPDATE/DELETE的影响。如果表写操作频繁,缺失索引提示的Impact再高也得慎重。

可能受参数嗅探影响。如果查询用了参数,优化器基于某组特定参数值生成的计划可能不适合其他参数值。这种情况下,缺失索引提示可能只对特定参数有效。你可以用Query Store的“计划对比”功能看看不同参数值下的计划差异。

Impact百分比可能虚高。Impact是基于优化器的成本估算模型算出来的,不一定反映真实收益。我见过Impact 95%的提示,加了索引后实际只快了20%。所以一定要用实际运行时统计验证。

5.3 加索引后查询反而变慢

这种情况虽然少见,但确实存在。常见原因有:

索引选择错误。优化器可能选了一个“看起来成本低”但实际效率差的索引。比如新加的索引统计信息还没更新,优化器低估了它的成本。解决办法是更新统计信息:

UPDATE STATISTICS dbo.Orders WITH FULLSCAN;

索引碎片或页密度问题。新加的索引如果键列顺序不合理,可能导致页分裂频繁,反而增加I/O。你可以查一下索引的碎片率:

SELECT i.name, ps.avg_fragmentation_in_percent, ps.page_count FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('dbo.Orders'), NULL, NULL, 'SAMPLED') ps JOIN sys.indexes i ON ps.object_id = i.object_id AND ps.index_id = i.index_id;

如果碎片率超过30%,考虑重建索引。

参数嗅探导致计划回归。新索引改变了优化器的选择空间,可能让它选了一个对当前参数值不好但对其他参数值好的计划。这种情况下,可以用Query Store强制使用之前的计划,或者给查询加OPTION (RECOMPILE)。

写操作阻塞。如果索引建在写频繁的表上,且没有用ONLINE = ON,建索引期间可能阻塞其他操作。建完后如果写操作变慢,可能是索引维护开销导致的。

5.4 Query Store相关的问题速查表

问题现象可能原因排查方法解决思路
Query Store无数据未开启或已切换到只读查sys.database_query_store_options开启或调整空间/保留策略
数据断档空间不足或清理策略激进查readonly_reason和最早数据时间调大空间或缩短聚合间隔
缺失索引提示不出现捕获模式为AUTO且查询被过滤查query_capture_mode_desc临时改为ALL模式
加索引后查询变慢统计信息未更新或计划回归对比加索引前后的执行计划更新统计信息或强制旧计划
索引未被使用索引列顺序或INCLUDE列不合理查执行计划中的实际使用索引调整索引定义或删除重建
写操作变慢索引维护开销对比加索引前后的写操作持续时间评估是否值得保留索引

5.5 几个我踩过的坑

第一个坑:在Basic层级上开Query Store。Basic层级的空间限制太紧,Query Store跑不了几个小时就满了,然后自动切只读。如果你非要在Basic层级上用,建议把INTERVAL_LENGTH_MINUTES设成60以上,STALE_QUERY_THRESHOLD_DAYS设成1或2,尽量减少数据量。

第二个坑:直接照搬缺失索引提示的列顺序。有一次优化器提示的索引是(OrderDate, CustomerId),我直接建了,结果查询还是慢。后来发现实际查询里CustomerId是等值条件,OrderDate是不等值条件,应该把CustomerId放前面。优化器的提示有时候不考虑查询的实际参数化模式,得自己判断。

第三个坑:忽略Query Store的时间戳是UTC。有一次排查问题,我用本地时间过滤,结果查出来的数据对不上。后来才发现Query Store的start_time和end_time都是UTC,必须用GETUTCDATE()而不是GETDATE()。

第四个坑:在高峰期建索引。虽然用了ONLINE = ON,但建索引还是消耗了大量CPU和I/O,导致其他查询变慢。后来我养成了习惯:建索引前先看Query Store里的资源趋势,避开业务高峰期。

第五个坑:加了索引但没更新统计信息。新索引的统计信息是空的,优化器可能低估它的收益,继续用旧计划。建完索引后手动跑一次UPDATE STATISTICS,能让优化器更快用上新索引。

6. 从缺失索引延伸到Query Store的更多用法

缺失索引只是Query Store的一个应用场景。实际上,Query Store能做的事情远不止这些。比如你可以用它来追踪计划回归:同一个查询突然变慢,对比新旧计划的运行时统计,就能定位到是哪个计划变差了。再比如你可以用它来做容量规划:看过去一个月的资源消耗趋势,预估未来需要升级到什么服务层级。

我个人的习惯是,每次做性能优化前后,都用Query Store留一份“基线数据”。优化前的数据作为对照,优化后的数据作为验证。这样过一段时间回头看,能清楚知道每次优化到底带来了多少收益。

还有一个很实用的技巧:用Query Store的“计划强制”功能来稳定关键查询的性能。如果某个核心查询因为参数嗅探导致计划不稳定,你可以强制它使用某个已知良好的计划。这个功能在Azure SQL Database里是内置的,不需要额外配置。

最后再分享一个小经验:Query Store的视图很多,但常用的就那么几个。sys.query_store_runtime_stats看运行时统计,sys.query_store_plan看执行计划,sys.query_store_query_text看SQL文本,sys.query_store_wait_stats看等待类型。把这四个视图的关系搞清楚,大部分排查场景都能覆盖。等待类型的分析我打算放在下一篇里展开讲,那又是另一个能挖出很多故事的方向。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 20:26:55

数学建模竞赛种植策略可视化:四类图表代码实战与避坑指南

简介&#xff1a;这份资源面向参加2024年高教社杯全国大学生数学建模竞赛C题的学生与指导教师&#xff0c;聚焦农作物种植策略的可视化实现。包内共31个文件&#xff0c;以20个png图表、6个Python脚本和5个xlsx数据表为主&#xff0c;压缩包约2MB&#xff0c;涵盖数据预处理、描…

作者头像 李华
网站建设 2026/10/12 20:22:16

AI编程助手老是“失忆”?用claude-mem构建项目持久记忆的实践指南

如果你也经历过“昨天它还在&#xff0c;今天它失忆了”的时刻&#xff0c;那这篇文章大概值得读完。我平时在终端里用 AI 编程助手&#xff0c;最头疼的不是模型能力不够&#xff0c;而是新会话完全不继承旧上下文。项目稍稍复杂一点&#xff0c;同样的需求背景要反复解释两三…

作者头像 李华
网站建设 2026/10/12 20:21:36

Claude Code用不好?问题不在工具,而在两个关键误区

一开始接触 Claude Code 的那两周&#xff0c;我几乎在每一个深夜都对着终端怀疑人生&#xff1a;让它重构一个类&#xff0c;它把同文件里另一段业务逻辑一并“顺带优化”了&#xff1b;让它补测试&#xff0c;它在测试用例里自作主张改掉了被测试函数的返回值&#xff1b;让它…

作者头像 李华
网站建设 2026/10/12 20:20:08

基于MFC的LALR(1)分析表自动构造工具设计与实现

简介&#xff1a;这是一套基于MFC界面开发的LALR(1)分析表自动构造程序资源包&#xff0c;专门用于编译原理课程设计&#xff0c;面向需要完成语法分析阶段实验的高校学生。程序能够对任意给定的文法自动构造LR(1)项目集规范族&#xff0c;再通过合并同心集得到LALR(1)项目集规…

作者头像 李华
网站建设 2026/10/12 20:18:14

从零手写gRPC C++项目:proto生成、CMake构建与编译实战

1. 为什么值得从零手写一次 gRPC C 项目gRPC 这套东西&#xff0c;第一次接触的人很容易被它绕晕。Python、Go 那边几行代码就能跑起来&#xff0c;到了 C 这边&#xff0c;光是把项目生成、编译跑通&#xff0c;就能卡掉一大半人。我自己最早接触 gRPC C 的时候&#xff0c;也…

作者头像 李华