DMV Snapshot Deltas for a Query Activity Interval

DMV snapshot deltas measure counter growth between two observations of cached query statements. A lifetime CPU total cannot answer an interval question. Compare surviving statement identities and report what the cache comparison cannot cover.

Two wooden trays of ceramic samples beside an hourglass, with a separate unmatched-sample tray.

Keep statement identity beside the counters

sys.dm_exec_query_stats records totals for statements in cached plans. Its rows disappear when those plans leave the cache. The counters reflect completed executions, so they do not describe every query currently running.

Match plan_handle, sql_handle, both statement offsets, creation_time and plan_generation_num. The generation distinguishes recompiled instances. A shared query_hash alone does not establish a comparable counter lifetime.

This example targets ordinary cached statements on SQL Server 2016 or later. Run the three blocks in order in one connection, because the later blocks use the temporary tables created by the first two. Natively compiled procedure counters need separate interpretation; this is not a universal workload collector.

Capture twice without resetting the cache

The collector reads server DMVs and writes only its temporary tables. It does not clear caches, recompile queries or change server settings.

-- SQL Server rowstore cached statements. Read-only server DMVs.
-- Writes only local temporary tables, then drops those exact tables.
-- Five-second demonstration, not a benchmark or full workload history.
SET NOCOUNT ON;
DROP TABLE IF EXISTS #QueryComparison;
DROP TABLE IF EXISTS #QuerySnapshot2;
DROP TABLE IF EXISTS #QuerySnapshot1;
DROP TABLE IF EXISTS #CaptureBounds;
CREATE TABLE #CaptureBounds(Capture1StartUtc datetime2(7),Capture1EndUtc datetime2(7),
       Capture2StartUtc datetime2(7),Capture2EndUtc datetime2(7));
INSERT #CaptureBounds(Capture1StartUtc) VALUES(SYSUTCDATETIME());
SELECT plan_handle,sql_handle,statement_start_offset,statement_end_offset,
       creation_time,plan_generation_num,query_hash,
       execution_count,total_worker_time,total_logical_reads
INTO #QuerySnapshot1
FROM sys.dm_exec_query_stats;
UPDATE #CaptureBounds SET Capture1EndUtc=SYSUTCDATETIME();

For a short demonstration, wait five seconds between captures. For an investigation, choose a gap that covers the workload of interest. Record each collection’s start and end, because a DMV scan is not an instantaneous snapshot.

WAITFOR DELAY '00:00:05';
UPDATE #CaptureBounds SET Capture2StartUtc=SYSUTCDATETIME();
SELECT plan_handle,sql_handle,statement_start_offset,statement_end_offset,
       creation_time,plan_generation_num,query_hash,
       execution_count,total_worker_time,total_logical_reads
INTO #QuerySnapshot2
FROM sys.dm_exec_query_stats;
UPDATE #CaptureBounds SET Capture2EndUtc=SYSUTCDATETIME();

Report unmatched rows before ranking activity

Use a full join to expose identities present in only one capture. Label those rows FIRST_ONLY or SECOND_ONLY. Neither label, by itself, proves when a plan disappeared or why it appeared.

Keep matching rows with decreasing counters out of the activity ranking. The example labels them COUNTER_DECREASE. Only MATCHED rows with positive execution growth enter its ranking.

WITH Compared AS
(
    SELECT COALESCE(b.query_hash,a.query_hash) AS query_hash,
           COALESCE(b.plan_handle,a.plan_handle) AS plan_handle,
           COALESCE(b.sql_handle,a.sql_handle) AS sql_handle,
           COALESCE(b.statement_start_offset,a.statement_start_offset)
               AS statement_start_offset,
           COALESCE(b.statement_end_offset,a.statement_end_offset)
               AS statement_end_offset,
           COALESCE(b.creation_time,a.creation_time) AS creation_time,
           COALESCE(b.plan_generation_num,a.plan_generation_num)
               AS plan_generation_num,
           CASE WHEN a.plan_handle IS NULL THEN 'SECOND_ONLY'
                WHEN b.plan_handle IS NULL THEN 'FIRST_ONLY'
                WHEN b.execution_count<a.execution_count
                  OR b.total_worker_time<a.total_worker_time
                  OR b.total_logical_reads<a.total_logical_reads
                    THEN 'COUNTER_DECREASE'
                ELSE 'MATCHED' END AS Coverage,
           b.execution_count-a.execution_count AS Executions,
           b.total_worker_time-a.total_worker_time AS WorkerMicroseconds,
           b.total_logical_reads-a.total_logical_reads AS LogicalReads
    FROM #QuerySnapshot1 AS a
    FULL JOIN #QuerySnapshot2 AS b
      ON b.plan_handle=a.plan_handle AND b.sql_handle=a.sql_handle
     AND b.statement_start_offset=a.statement_start_offset
     AND b.statement_end_offset=a.statement_end_offset
     AND b.creation_time=a.creation_time
     AND b.plan_generation_num=a.plan_generation_num
)
SELECT * INTO #QueryComparison FROM Compared;

SELECT Capture1StartUtc,Capture1EndUtc,Capture2StartUtc,Capture2EndUtc,
       DATEDIFF_BIG(millisecond,Capture1EndUtc,Capture2StartUtc) AS GapMilliseconds
FROM #CaptureBounds;
SELECT Coverage,COUNT_BIG(*) AS StatementRows
FROM #QueryComparison GROUP BY Coverage ORDER BY Coverage;

SELECT TOP (10) query_hash,Executions AS IntervalExecutions,
       WorkerMicroseconds AS IntervalWorkerMicroseconds,
       LogicalReads AS IntervalLogicalReads,
       statement_start_offset,statement_end_offset,plan_generation_num
FROM #QueryComparison
WHERE Coverage='MATCHED' AND Executions>0
ORDER BY WorkerMicroseconds DESC,LogicalReads DESC,query_hash;

IF EXISTS
(
    SELECT 1 FROM #QueryComparison WHERE Coverage='MATCHED'
    AND (Executions<0 OR WorkerMicroseconds<0 OR LogicalReads<0)
)
    THROW 51003,'A decreasing counter entered the matched population.',1;

SELECT 'Completed, no persistent objects or server settings changed' AS CollectorStatus;
DROP TABLE #QueryComparison;
DROP TABLE #QuerySnapshot2;
DROP TABLE #QuerySnapshot1;
DROP TABLE #CaptureBounds;

The output stays at statement level and preserves offsets and plan generation. Before cleanup, the temporary comparison also carries the handles. The three blocks together give you the capture bounds, the coverage summary and the ranking. They drop their temporary tables after reporting. Remove the last few DROP lines only when you need to investigate the underlying identities.

Snapshot Delta Steps

Read an empty ranking honestly

A successful empty ranking means the matched population had no observed positive execution delta. It does not prove that the whole server was idle. Unmatched plans, unfinished work and collection gaps remain possible.

My October 2, 2026 sample produced an empty activity ranking. Both captures succeeded; their coverage summary shows which statements could be compared. This is an observation from a short sample, not a performance benchmark.

Actual native SSMS Light collector output shows a 5009 millisecond gap, one matched statement, one second-only statement and empty activity ranking.
Actual sample from my test server, October 2, 2026: a 5,009 ms gap, one matched statement and one second-only statement. The matched statement had no positive execution delta, so the activity ranking is empty. Select the image to inspect every native pixel.

A small set of made-up sample rows makes the subtraction easy to inspect. It changes one surviving statement from 10 executions to 13, giving a delta of 3. Worker time increases by 900 microseconds, and logical reads increase by 45.

Those numbers are sample inputs, not measured server activity. The sample also includes a statement with no activity, missing identities, a new generation and decreasing counters. Only the comparable active row reaches the ranking. This tests the comparison without forcing any server cache changes.

SET NOCOUNT ON;
DROP TABLE IF EXISTS #QueryComparison;
DROP TABLE IF EXISTS #QuerySnapshot2;
DROP TABLE IF EXISTS #QuerySnapshot1;
CREATE TABLE #QuerySnapshot1
(
    plan_handle varbinary(64),sql_handle varbinary(64),
    statement_start_offset int,statement_end_offset int,
    creation_time datetime,plan_generation_num bigint,query_hash binary(8),
    execution_count bigint,total_worker_time bigint,total_logical_reads bigint
);
SELECT * INTO #QuerySnapshot2 FROM #QuerySnapshot1;
-- 0x01: successful interval subtraction; 0x02: no completed activity.
-- 0x03: first-only; 0x04: counter decrease; 0x05: recompiled generation.
INSERT #QuerySnapshot1 VALUES
(0x01,0x01,0,20,'20261002',1,0x0000000000000001,10,1000,100),
(0x02,0x02,0,20,'20261002',1,0x0000000000000002,10,1000,100),
(0x03,0x03,0,20,'20261002',1,0x0000000000000003,10,1000,100),
(0x04,0x04,0,20,'20261002',1,0x0000000000000004,10,1000,100),
(0x05,0x05,0,20,'20261002',1,0x0000000000000005,10,1000,100);
INSERT #QuerySnapshot2 VALUES
(0x01,0x01,0,20,'20261002',1,0x0000000000000001,13,1900,145),
(0x02,0x02,0,20,'20261002',1,0x0000000000000002,10,1000,100),
(0x04,0x04,0,20,'20261002',1,0x0000000000000004,1,100,10),
(0x05,0x05,0,20,'20261002',2,0x0000000000000005,1,100,10),
(0x06,0x06,0,20,'20261002',1,0x0000000000000006,7,700,70);

WITH Compared AS
(
    SELECT COALESCE(b.query_hash,a.query_hash) AS query_hash,
           CASE WHEN a.plan_handle IS NULL THEN 'SECOND_ONLY'
                WHEN b.plan_handle IS NULL THEN 'FIRST_ONLY'
                WHEN b.execution_count<a.execution_count
                  OR b.total_worker_time<a.total_worker_time
                  OR b.total_logical_reads<a.total_logical_reads
                    THEN 'COUNTER_DECREASE'
                ELSE 'MATCHED' END AS Coverage,
           b.execution_count-a.execution_count AS Executions,
           b.total_worker_time-a.total_worker_time AS WorkerMicroseconds,
           b.total_logical_reads-a.total_logical_reads AS LogicalReads
    FROM #QuerySnapshot1 AS a FULL JOIN #QuerySnapshot2 AS b
      ON b.plan_handle=a.plan_handle AND b.sql_handle=a.sql_handle
     AND b.statement_start_offset=a.statement_start_offset
     AND b.statement_end_offset=a.statement_end_offset
     AND b.creation_time=a.creation_time
     AND b.plan_generation_num=a.plan_generation_num
)
SELECT * INTO #QueryComparison FROM Compared;

SELECT Coverage,COUNT_BIG(*) AS StatementRows
FROM #QueryComparison GROUP BY Coverage ORDER BY Coverage;
SELECT query_hash,Executions AS IntervalExecutions,
       WorkerMicroseconds AS IntervalWorkerMicroseconds,
       LogicalReads AS IntervalLogicalReads
FROM #QueryComparison WHERE Coverage='MATCHED' AND Executions>0;

DROP TABLE #QueryComparison;
DROP TABLE #QuerySnapshot2;
DROP TABLE #QuerySnapshot1;
SSMS grids: four statement-coverage categories and one matched delta: three executions, 900 microseconds, 45 reads.
Made-up sample inputs, not measured server activity. One comparable active row produces 3 executions, 900 microseconds and 45 logical reads. Select the image to inspect every native pixel.

Use the interval result within its limits

Worker time is CPU time, reported in microseconds with millisecond accuracy. Logical reads are page-access counts, not bytes or disk-read counts. Neither column directly measures elapsed response time.

An execution spanning a capture boundary can contribute work outside your chosen gap when its counters become visible. An evicted plan may lose its final counters entirely. Treat the report as observed counter growth, not a complete interval ledger.

A cache restart, eviction or recompilation can reduce comparable coverage. Do not add an unmatched row’s lifetime total to the interval ranking. Preserve the coverage summary so readers can judge what the result supports.

For durable workload history, compare this evidence with Query Store when it is available and collecting the relevant queries. Its capture policy, retention and aggregation intervals also matter. A higher CPU delta suggests where to investigate, rather than proving the cause of a slowdown.

Each capture consumes CPU and temporary storage proportional to the observed cache. Measure that collection cost before scheduling repeated samples. This example collects handles and counters, without retrieving potentially sensitive query text.

Permissions

SQL Server 2022 and later require VIEW SERVER PERFORMANCE STATE for this DMV. Earlier versions use VIEW SERVER STATE. Obtain the appropriate read permission before collecting, without changing services or server configuration.

Run it on a quiet afternoon first, and read the coverage before the ranking.

A counter delta is not a workload ledger, it is growth in what the cache still remembered.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

SQL Cache, SQL DMV, SQL Performance, SQL Server
Previous Post
Partition Stats Permissions: A SQL Server 2025 CU9 Counterpoint
Next Post
Checking 32-Bit Leftovers: Linked Server Providers and ODBC Drivers

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.