Chapter 11 · Query Store, Parameter Sensitivity, and Intelligent Query Processing

Query Store Architecture, Capture Modes, Runtime Stats, Wait Stats, and Storage

Use Query Store as a database-scoped persisted history of query text, plans, runtime statistics, wait categories, capture policy, storage state, and cleanup behavior.

Advanced150–190 minutesQuery Store architecture & evidence labSQL Server 2025 CU7 · 17.0.4065.4Compatibility 170 · Developer/ExpressSSMS 22.8.2 · Last reviewed August 2026

Learning outcomes

ServiceHub receives a familiar incident report: “the same endpoint became slow after yesterday’s deployment, but the live plan cache no longer contains the bad plan.” A transient DMV snapshot cannot answer what happened before a cache eviction, restart, recompilation, or plan change. Query Store exists to retain database-scoped query text, plans, aggregated runtime statistics, and wait categories so a practitioner can compare behavior over time instead of reconstructing history from memory.

01

Explain Query Store as a database-scoped, persisted performance repository rather than a second plan cache.

02

Read desired/effective state, capture mode, storage quota, cleanup mode, runtime interval, and wait-stat settings.

03

Join Query Store query, plan, runtime-interval, runtime-statistics, and wait-statistics views without double-counting.

04

Distinguish persisted historical aggregates from real-time DMVs and understand reset/removal consequences.

05

Build and inspect a disposable Query Store workload using free SQL Server 2025 Developer/Express tooling.

Lab bootstrap: isolate Query Store changes from ServiceHubLab

Query Store configuration is database state. The mandatory lab therefore uses ServiceHubQSLab, not the long-lived ServiceHubLab. The settings below are teaching values for a small disposable database, not universal production recommendations. A production quota, capture policy, retention period, and aggregation interval must be sized from workload volume and evidence.

sql · create the disposable Query Store lab and skewed ServiceHub workload
USE master;GOIF DB_ID(N'ServiceHubQSLab') IS NULLBEGIN  CREATE DATABASE ServiceHubQSLab;END;GOALTER DATABASE ServiceHubQSLab SET COMPATIBILITY_LEVEL = 170;ALTER DATABASE ServiceHubQSLab SET QUERY_STORE = ON(  OPERATION_MODE = READ_WRITE,  QUERY_CAPTURE_MODE = AUTO,  WAIT_STATS_CAPTURE_MODE = ON,  INTERVAL_LENGTH_MINUTES = 5,  MAX_STORAGE_SIZE_MB = 100,  SIZE_BASED_CLEANUP_MODE = AUTO,  CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 7));GOUSE ServiceHubQSLab;GOIF SCHEMA_ID(N'lab11') IS NULL EXEC(N'CREATE SCHEMA lab11 AUTHORIZATION dbo;');GOIF OBJECT_ID(N'lab11.WorkOrderFact',N'U') IS NULLBEGIN  CREATE TABLE lab11.WorkOrderFact  (    work_order_id bigint IDENTITY(1,1) NOT NULL,    customer_id int NOT NULL,    region_code char(3) NOT NULL,    status varchar(12) NOT NULL,    priority tinyint NOT NULL,    opened_at datetime2(0) NOT NULL,    amount decimal(12,2) NOT NULL,    notes varchar(200) NULL,    CONSTRAINT PK_lab11_WorkOrderFact PRIMARY KEY CLUSTERED(work_order_id)  );  ;WITH n AS  (    SELECT TOP (40000)      ROW_NUMBER() OVER (ORDER BY a.object_id,b.object_id) AS n    FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b  )  INSERT lab11.WorkOrderFact(customer_id,region_code,status,priority,opened_at,amount,notes)  SELECT CASE WHEN n <= 24000 THEN 1 ELSE 2 + n % 3999 END,         CASE WHEN n <= 27000 THEN 'N01' WHEN n <= 35000 THEN 'W02' ELSE 'E03' END,         CASE WHEN n % 23 = 0 THEN 'ESCALATED' WHEN n % 5 = 0 THEN 'CLOSED' ELSE 'OPEN' END,         CASE WHEN n <= 27000 THEN 1 ELSE 5 END,         DATEADD(minute,n,'2026-01-01T00:00:00'),         CAST(20 + (n % 8000) / 10.0 AS decimal(12,2)),         CASE WHEN n % 41=0 THEN REPLICATE('x',180) END  FROM n;  CREATE INDEX IX_lab11_Customer    ON lab11.WorkOrderFact(customer_id)    INCLUDE(region_code,status,opened_at,amount);  CREATE INDEX IX_lab11_RegionStatus    ON lab11.WorkOrderFact(region_code,status,opened_at)    INCLUDE(customer_id,priority,amount);END;GOCREATE OR ALTER PROCEDURE lab11.GetOrdersByCustomer  @customer_id intASBEGIN  SET NOCOUNT ON;  SELECT work_order_id,region_code,status,opened_at,amount  FROM lab11.WorkOrderFact  WHERE customer_id=@customer_id  ORDER BY opened_at,work_order_id;END;GO
Expected state

On SQL Server 2025, ServiceHubQSLab should report compatibility 170 and Query Store actual_state_desc = READ_WRITE. If desired and actual states differ, do not assume collection is healthy; inspect readonly_reason and storage state.

1. The repository has several identities, not one “query row”

Query Store normalizes its model into related catalog views. sys.query_store_query_text stores captured statement text. sys.query_store_query identifies a logical captured query and records context such as object association and query hash. sys.query_store_plan stores plans that SQL Server generated for those queries. Runtime rows are aggregated by plan and by a configurable runtime statistics interval; they are not one row per execution.

This matters operationally. If a query has three plans across four runtime intervals, joining query → plan → runtime statistics can produce several rows. Summing averages directly is mathematically wrong. Use count_executions as the weight when producing a combined average, and keep time windows explicit.

sql · inspect effective Query Store configuration before interpreting history
USE ServiceHubQSLab;GOSELECT actual_state_desc,desired_state_desc,readonly_reason,       query_capture_mode_desc,wait_stats_capture_mode_desc,       interval_length_minutes,current_storage_size_mb,max_storage_size_mb,       size_based_cleanup_mode_desc,stale_query_threshold_daysFROM sys.database_query_store_options;GOSELECT DB_NAME() AS database_name,       compatibility_level,is_query_store_onFROM sys.databasesWHERE database_id=DB_ID();GO

For SQL Server 2022 and later, the Query Store catalog views used in this lesson generally require VIEW DATABASE PERFORMANCE STATE or a greater database permission for read-only diagnosis. Changing Query Store options or forcing behavior requires stronger database permissions; the course assumes an administrator-level account only on this disposable local database.

2. Capture mode is an admission policy, not a promise that every execution is stored

Query capture mode controls which statements are admitted to Query Store. ALL is broad, AUTO uses built-in significance criteria to avoid filling the store with low-value one-off statements, NONE stops capturing new queries while retaining existing history, and CUSTOM lets administrators specify capture-policy thresholds on supported versions. The right choice depends on troubleshooting goals and workload shape.

ServiceHub uses AUTO for the mandatory lab. Run a stable tagged workload enough times that it becomes observable; do not switch blindly to ALL just because one test statement is absent.

sql · generate repeat executions and then locate the captured query
USE ServiceHubQSLab;GOEXEC lab11.GetOrdersByCustomer @customer_id=1;EXEC lab11.GetOrdersByCustomer @customer_id=2;EXEC lab11.GetOrdersByCustomer @customer_id=1;EXEC lab11.GetOrdersByCustomer @customer_id=17;EXEC lab11.GetOrdersByCustomer @customer_id=1;GOSELECT q.query_id,q.query_hash,q.object_id,q.count_compiles,       p.plan_id,p.plan_type_desc,p.is_forced_plan,       qt.query_sql_textFROM sys.query_store_query_text AS qtJOIN sys.query_store_query AS q ON q.query_text_id=qt.query_text_idJOIN sys.query_store_plan AS p ON p.query_id=q.query_idWHERE qt.query_sql_text LIKE N'%lab11.WorkOrderFact%customer_id=@customer_id%'ORDER BY q.query_id,p.plan_id;GO

Absence after only one tiny execution does not prove Query Store is broken under AUTO. Verify capture mode, execute a representative workload, and inspect state. Conversely, presence in Query Store proves the statement was captured; it does not prove current plan-cache residency or current blocking state.

3. Runtime and wait data are historical aggregates

sys.query_store_runtime_stats records aggregate metrics such as execution count, duration, CPU, logical I/O and row counts for a plan during a runtime interval. sys.query_store_runtime_stats_interval gives the interval boundaries. Query Store wait statistics aggregate waits into categories per plan and interval; they are useful for asking “what kind of waiting accompanied this plan over time?” rather than “what is this session waiting on right now?”

sql · read runtime and wait evidence with its time interval attached
USE ServiceHubQSLab;GOSELECT q.query_id,p.plan_id,p.plan_type_desc,       rsi.start_time,rsi.end_time,       rs.count_executions,rs.avg_duration,rs.avg_cpu_time,       rs.avg_logical_io_reads,rs.avg_rowcountFROM sys.query_store_query_text AS qtJOIN sys.query_store_query AS q ON q.query_text_id=qt.query_text_idJOIN sys.query_store_plan AS p ON p.query_id=q.query_idJOIN sys.query_store_runtime_stats AS rs ON rs.plan_id=p.plan_idJOIN sys.query_store_runtime_stats_interval AS rsi  ON rsi.runtime_stats_interval_id=rs.runtime_stats_interval_idWHERE qt.query_sql_text LIKE N'%lab11.WorkOrderFact%customer_id=@customer_id%'ORDER BY rsi.start_time,p.plan_id;GOSELECT q.query_id,p.plan_id,rsi.start_time,       ws.wait_category_desc,ws.total_query_wait_time_ms,ws.avg_query_wait_time_msFROM sys.query_store_query_text AS qtJOIN sys.query_store_query AS q ON q.query_text_id=qt.query_text_idJOIN sys.query_store_plan AS p ON p.query_id=q.query_idJOIN sys.query_store_wait_stats AS ws ON ws.plan_id=p.plan_idJOIN sys.query_store_runtime_stats_interval AS rsi  ON rsi.runtime_stats_interval_id=ws.runtime_stats_interval_idWHERE qt.query_sql_text LIKE N'%lab11.WorkOrderFact%customer_id=@customer_id%'ORDER BY rsi.start_time,ws.total_query_wait_time_ms DESC;GO
Interpretation discipline

Query Store wait categories show correlation with captured executions, not root cause by themselves. A storage wait category can be a symptom of a changed access path; a CPU-heavy interval can reflect more rows, different concurrency, or a plan change. Correlate plans, runtime metrics, waits, deployment time, statistics changes, and workload mix.

4. Storage and cleanup are part of correctness of the evidence

Query Store data lives inside the user database and has a configured maximum size. If it reaches conditions that make it read-only, new evidence stops accumulating. desired_state_desc = READ_WRITE is therefore insufficient; check actual_state_desc, readonly_reason, current size, quota, and cleanup mode. Size-based cleanup can remove older or less valuable history to recover space.

The wrong reaction to a confusing store is “clear Query Store and start over.” A clear operation destroys the historical baseline you may need to explain the incident, and feedback mechanisms can depend on Query Store persistence. First export or record the relevant evidence, identify the exact problem—quota, capture policy, excessive ad hoc text, retention, or corruption/error state—and make the smallest change.

sql · inventory storage pressure without deleting history
USE ServiceHubQSLab;GOSELECT actual_state_desc,readonly_reason,current_storage_size_mb,max_storage_size_mb,       flush_interval_seconds,interval_length_minutes,       size_based_cleanup_mode_desc,query_capture_mode_descFROM sys.database_query_store_options;GOSELECT COUNT_BIG(*) AS captured_queries FROM sys.query_store_query;SELECT COUNT_BIG(*) AS captured_plans FROM sys.query_store_plan;SELECT MIN(start_time) AS oldest_interval,MAX(end_time) AS newest_intervalFROM sys.query_store_runtime_stats_interval;GO

5. Production judgment and bridge

Use Query Store when you need a persisted account of query/plan behavior across cache turnover and time. Do not treat it as a live session monitor, a full tracing system, or an infinite telemetry warehouse. Size it, monitor its state, preserve enough history to span release/incident cycles, and document any capture-policy change because that change alters what the absence of data means.

The mandatory lab assumes SQL Server 2025 CU7 build 17.0.4065.4, compatibility 170, a single local Developer/Express instance, and SSMS 22.8.2 or supported VS Code MSSQL/current sqlcmd. No cluster, cloud service, restart, or paid production license is required. SQL Server 2025 can also support Query Store on readable Availability Group secondaries, but that topology-specific capability is not required here and has replica-aware state that must be interpreted separately.

Check your understanding

  1. Why can Query Store answer some questions that sys.dm_exec_query_stats cannot?
  2. What is wrong with summing avg_duration across runtime-stat rows?
  3. Does desired_state_desc=READ_WRITE prove Query Store is currently collecting?
  4. What does an empty Query Store wait row prove about a query?
  5. Why is clearing Query Store a poor first response to an incident?
Review the answers

1. Query Store persists database-scoped history across many cache changes, while plan-cache DMVs are transient and reset/evict with cache lifecycle.

2. Each row is already an average for a plan and interval; combine them with execution-count weighting or compare intervals directly.

3. No. Verify actual_state_desc, readonly_reason, quota, capture mode, and other effective settings.

4. Very little by itself. The query may not have been captured, may not have executed in that interval, or may have no recorded wait category; absence is not proof of zero contention.

5. It deletes the baseline and can remove evidence or persisted feedback needed to diagnose and compare the regression.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.