From e7eb3cf64016409ed8a9dfba0c33e22fac8b3ace Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:32:59 -0500 Subject: [PATCH] Optimize Lite query store collector: CTE + 60-minute window Restructure the Query Store collection query to avoid nvarchar(max) query_sql_text in GROUP BY. Aggregation and ranking now happen in a CTE that only touches sys.query_store_query, query_store_plan, and query_store_runtime_stats. The expensive join to sys.query_store_query_text is deferred until after TOP 100 filtering, dramatically reducing sort memory and I/O. Also narrows the lookback window from 24 hours to 60 minutes to match the collection interval and reduce scan volume. Addresses #134. Co-Authored-By: Claude Opus 4.6 --- .../RemoteCollectorService.QueryStore.cs | 75 ++++++++++++------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/Lite/Services/RemoteCollectorService.QueryStore.cs b/Lite/Services/RemoteCollectorService.QueryStore.cs index abde056c..651643a1 100644 --- a/Lite/Services/RemoteCollectorService.QueryStore.cs +++ b/Lite/Services/RemoteCollectorService.QueryStore.cs @@ -81,36 +81,57 @@ ORDER BY d.name EXECUTE [{dbName.Replace("]", "]]")}].sys.sp_executesql N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT TOP (100) - query_id = qsq.query_id, - plan_id = qsp.plan_id, + ;WITH ranked AS + ( + SELECT TOP (100) + query_id = qsq.query_id, + plan_id = qsp.plan_id, + query_text_id = qsq.query_text_id, + query_hash = qsq.query_hash, + query_plan_hash = qsp.query_plan_hash, + execution_count = SUM(qsrs.count_executions), + avg_duration_ms = CONVERT(decimal(18,2), AVG(qsrs.avg_duration) / 1000.0), + avg_cpu_time_ms = CONVERT(decimal(18,2), AVG(qsrs.avg_cpu_time) / 1000.0), + avg_logical_reads = CONVERT(decimal(18,2), AVG(qsrs.avg_logical_io_reads)), + avg_logical_writes = CONVERT(decimal(18,2), AVG(qsrs.avg_logical_io_writes)), + avg_physical_reads = CONVERT(decimal(18,2), AVG(qsrs.avg_physical_io_reads)), + avg_rowcount = CONVERT(decimal(18,2), AVG(qsrs.avg_rowcount)), + last_execution_time = MAX(qsrs.last_execution_time), + total_duration = SUM(qsrs.avg_duration * qsrs.count_executions) + FROM sys.query_store_query AS qsq + JOIN sys.query_store_plan AS qsp + ON qsp.query_id = qsq.query_id + JOIN sys.query_store_runtime_stats AS qsrs + ON qsrs.plan_id = qsp.plan_id + WHERE qsrs.last_execution_time >= DATEADD(MINUTE, -60, SYSDATETIME()) + GROUP BY + qsq.query_id, + qsp.plan_id, + qsq.query_text_id, + qsq.query_hash, + qsp.query_plan_hash + ORDER BY + total_duration DESC + ) + SELECT + r.query_id, + r.plan_id, query_text = qst.query_sql_text, - query_hash = CONVERT(varchar(64), qsq.query_hash, 1), - execution_count = SUM(qsrs.count_executions), - avg_duration_ms = CONVERT(decimal(18,2), AVG(qsrs.avg_duration) / 1000.0), - avg_cpu_time_ms = CONVERT(decimal(18,2), AVG(qsrs.avg_cpu_time) / 1000.0), - avg_logical_reads = CONVERT(decimal(18,2), AVG(qsrs.avg_logical_io_reads)), - avg_logical_writes = CONVERT(decimal(18,2), AVG(qsrs.avg_logical_io_writes)), - avg_physical_reads = CONVERT(decimal(18,2), AVG(qsrs.avg_physical_io_reads)), - avg_rowcount = CONVERT(decimal(18,2), AVG(qsrs.avg_rowcount)), - last_execution_time = MAX(qsrs.last_execution_time), - query_plan_hash = CONVERT(varchar(64), qsp.query_plan_hash, 1) - FROM sys.query_store_query AS qsq + query_hash = CONVERT(varchar(64), r.query_hash, 1), + r.execution_count, + r.avg_duration_ms, + r.avg_cpu_time_ms, + r.avg_logical_reads, + r.avg_logical_writes, + r.avg_physical_reads, + r.avg_rowcount, + r.last_execution_time, + query_plan_hash = CONVERT(varchar(64), r.query_plan_hash, 1) + FROM ranked AS r JOIN sys.query_store_query_text AS qst - ON qst.query_text_id = qsq.query_text_id - JOIN sys.query_store_plan AS qsp - ON qsp.query_id = qsq.query_id - JOIN sys.query_store_runtime_stats AS qsrs - ON qsrs.plan_id = qsp.plan_id - WHERE qsrs.last_execution_time >= DATEADD(HOUR, -24, SYSDATETIME()) - GROUP BY - qsq.query_id, - qsp.plan_id, - qst.query_sql_text, - qsq.query_hash, - qsp.query_plan_hash + ON qst.query_text_id = r.query_text_id ORDER BY - SUM(qsrs.avg_duration * qsrs.count_executions) DESC + r.total_duration DESC OPTION(RECOMPILE);';"; sqlSw.Start();