diff --git a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml index 29555d1..d5ae202 100644 --- a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml +++ b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml @@ -78,22 +78,34 @@ - + + - + + + + + + + + + + + + diff --git a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs index edbd75b..b6af494 100644 --- a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs +++ b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs @@ -556,6 +556,7 @@ private static QueryStorePlan GroupedRowToPlan(QueryStoreGroupedPlanRow row) QueryText = row.QueryText, PlanXml = row.PlanXml, CountExecutions = row.CountExecutions, + ExecutionTypeDesc = row.ExecutionTypeDesc, TotalCpuTimeUs = row.TotalCpuTimeUs, TotalDurationUs = row.TotalDurationUs, TotalLogicalIoReads = row.TotalLogicalIoReads, @@ -602,15 +603,42 @@ private static QueryStorePlan AggregateGroupedRows(List Plan.QueryHash; public string QueryPlanHash => Plan.QueryPlanHash; public string ModuleName => Plan.ModuleName; + public string ExecutionTypeDesc => Plan.ExecutionTypeDesc; public string ExecsDisplay => Plan.CountExecutions.ToString("N0"); public string TotalCpuDisplay => (Plan.TotalCpuTimeUs / 1000.0).ToString("N0"); diff --git a/src/PlanViewer.App/Dialogs/QueryStoreHistoryWindow.axaml b/src/PlanViewer.App/Dialogs/QueryStoreHistoryWindow.axaml index 04d9188..cccbcba 100644 --- a/src/PlanViewer.App/Dialogs/QueryStoreHistoryWindow.axaml +++ b/src/PlanViewer.App/Dialogs/QueryStoreHistoryWindow.axaml @@ -147,6 +147,7 @@ + diff --git a/src/PlanViewer.Core/Models/QueryStoreGroupBy.cs b/src/PlanViewer.Core/Models/QueryStoreGroupBy.cs index 6d62f70..1111854 100644 --- a/src/PlanViewer.Core/Models/QueryStoreGroupBy.cs +++ b/src/PlanViewer.Core/Models/QueryStoreGroupBy.cs @@ -44,6 +44,7 @@ public class QueryStoreGroupedPlanRow /// for a query_hash/plan_hash pair. Only meaningful for leaf-level (QueryId/PlanId) rows. /// public bool IsTopRepresentative { get; set; } + public string ExecutionTypeDesc { get; set; } = ""; } /// diff --git a/src/PlanViewer.Core/Models/QueryStoreHistoryRow.cs b/src/PlanViewer.Core/Models/QueryStoreHistoryRow.cs index b275acf..e89b5b7 100644 --- a/src/PlanViewer.Core/Models/QueryStoreHistoryRow.cs +++ b/src/PlanViewer.Core/Models/QueryStoreHistoryRow.cs @@ -27,6 +27,7 @@ public class QueryStoreHistoryRow public int MinDop { get; set; } public int MaxDop { get; set; } public DateTime? LastExecutionUtc { get; set; } + public string ExecutionTypeDesc { get; set; } = ""; // Display-formatted properties (2 decimal places) public string AvgDurationMsDisplay => AvgDurationMs.ToString("N2"); diff --git a/src/PlanViewer.Core/Models/QueryStorePlan.cs b/src/PlanViewer.Core/Models/QueryStorePlan.cs index d0369c3..f5ef750 100644 --- a/src/PlanViewer.Core/Models/QueryStorePlan.cs +++ b/src/PlanViewer.Core/Models/QueryStorePlan.cs @@ -13,6 +13,11 @@ public class QueryStoreFilter public string? QueryHash { get; set; } public string? QueryPlanHash { get; set; } public string? ModuleName { get; set; } + /// + /// One or more execution_type_desc values to filter by. + /// Single value → equality predicate; multiple values (e.g. "Aborted","Exception" for "Failed") → IN predicate. + /// + public string[]? ExecutionTypeDescs { get; set; } } public class QueryStorePlan @@ -22,6 +27,7 @@ public class QueryStorePlan public string QueryHash { get; set; } = ""; public string QueryPlanHash { get; set; } = ""; public string ModuleName { get; set; } = ""; + public string ExecutionTypeDesc { get; set; } = ""; public string QueryText { get; set; } = ""; public string PlanXml { get; set; } = ""; diff --git a/src/PlanViewer.Core/Services/QueryStoreService.cs b/src/PlanViewer.Core/Services/QueryStoreService.cs index 6ee0ad1..137c084 100644 --- a/src/PlanViewer.Core/Services/QueryStoreService.cs +++ b/src/PlanViewer.Core/Services/QueryStoreService.cs @@ -138,6 +138,16 @@ public static async Task> FetchTopPlansAsync( var phase3QueryJoin = needsQueryJoin ? " JOIN sys.query_store_query AS q ON p.query_id = q.query_id\n" : ""; + var phase2ExecutionTypeClause = ""; + if (filter?.ExecutionTypeDescs?.Length > 0) + { + var etParamNames = filter.ExecutionTypeDescs + .Select((_, i) => $"@executionType{i}") + .ToList(); + phase2ExecutionTypeClause = $"\nAND rs.execution_type_desc IN ({string.Join(", ", etParamNames)})"; + for (var i = 0; i < filter.ExecutionTypeDescs.Length; i++) + parameters.Add(new SqlParameter($"@executionType{i}", filter.ExecutionTypeDescs[i])); + } // Time-range filter: always filter on interval start_time (indexed). // The hoursBack fallback also uses interval start_time instead of @@ -199,7 +209,8 @@ FROM sys.query_store_runtime_stats_interval AS rsi total_physical_reads float NOT NULL, total_memory_pages float NOT NULL, total_executions bigint NOT NULL, - last_execution_time datetimeoffset NOT NULL + last_execution_time datetimeoffset NOT NULL, + execution_type_desc nvarchar(60) NOT NULL ); INSERT INTO #plan_stats SELECT @@ -211,14 +222,20 @@ INSERT INTO #plan_stats SUM(rs.avg_physical_io_reads * rs.count_executions), SUM(rs.avg_query_max_used_memory * rs.count_executions), SUM(rs.count_executions), - MAX(rs.last_execution_time) + MAX(rs.last_execution_time), + -- Pick execution_type_desc from the most-recently-executed interval to avoid + -- alphabetical bias: MAX would choose 'Regular' over 'Aborted'. + RTRIM(CAST(SUBSTRING(MAX( + CONVERT(char(27), CAST(rs.last_execution_time AS datetime2(7)), 121) + + CAST(ISNULL(rs.execution_type_desc, '') AS char(60)) + ), 28, 60) AS nvarchar(60))) FROM sys.query_store_runtime_stats AS rs WHERE EXISTS ( SELECT 1 FROM #intervals AS i WHERE i.runtime_stats_interval_id = rs.runtime_stats_interval_id -) +){phase2ExecutionTypeClause} GROUP BY rs.plan_id OPTION (RECOMPILE); @@ -236,6 +253,7 @@ WITH ranked AS ( ps.total_memory_pages, ps.total_executions, ps.last_execution_time, + ps.execution_type_desc, CASE WHEN ps.total_executions > 0 THEN ps.total_cpu_us / ps.total_executions ELSE 0 END AS avg_cpu_us, CASE WHEN ps.total_executions > 0 @@ -269,7 +287,8 @@ SELECT TOP ({topN}) CAST(r.total_writes AS bigint) AS total_writes, CAST(r.total_physical_reads AS bigint) AS total_physical_reads, CAST(r.total_memory_pages AS bigint) AS total_memory_pages, - r.last_execution_time + r.last_execution_time, + r.execution_type_desc INTO #top_plans FROM ranked AS r WHERE 1 = 1 {rnClause} @@ -301,7 +320,8 @@ FROM ranked AS r WHEN q.object_id <> 0 THEN OBJECT_SCHEMA_NAME(q.object_id) + N'.' + OBJECT_NAME(q.object_id) ELSE N'' - END + END, + tp.execution_type_desc FROM #top_plans AS tp JOIN sys.query_store_plan AS p ON tp.plan_id = p.plan_id JOIN sys.query_store_query AS q ON p.query_id = q.query_id @@ -346,6 +366,7 @@ ELSE N'' QueryHash = reader.IsDBNull(18) ? "" : reader.GetString(18), QueryPlanHash = reader.IsDBNull(19) ? "" : reader.GetString(19), ModuleName = reader.IsDBNull(20) ? "" : reader.GetString(20), + ExecutionTypeDesc = reader.IsDBNull(21) ? "" : reader.GetString(21), }); } @@ -392,7 +413,8 @@ THEN SUM(rs.avg_rowcount * rs.count_executions) / SUM(rs.count_executions) SUM(rs.avg_physical_io_reads * rs.count_executions), MIN(rs.min_dop), MAX(rs.max_dop), - MAX(rs.last_execution_time) + MAX(rs.last_execution_time), + MAX(rs.execution_type_desc) 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 @@ -436,6 +458,7 @@ JOIN sys.query_store_plan p MinDop = (int)reader.GetInt64(16), MaxDop = (int)reader.GetInt64(17), LastExecutionUtc = reader.IsDBNull(18) ? null : ((DateTimeOffset)reader.GetValue(18)).UtcDateTime, + ExecutionTypeDesc = reader.IsDBNull(19) ? "" : reader.GetString(19), }); } @@ -504,7 +527,8 @@ THEN SUM(rs.avg_rowcount * rs.count_executions) / SUM(rs.count_executions) SUM(rs.avg_physical_io_reads * rs.count_executions), MIN(rs.min_dop), MAX(rs.max_dop), - MAX(rs.last_execution_time) + MAX(rs.last_execution_time), + MAX(rs.execution_type_desc) 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 @@ -549,6 +573,7 @@ JOIN sys.query_store_query q MinDop = (int)reader.GetInt64(16), MaxDop = (int)reader.GetInt64(17), LastExecutionUtc = reader.IsDBNull(18) ? null : ((DateTimeOffset)reader.GetValue(18)).UtcDateTime, + ExecutionTypeDesc = reader.IsDBNull(19) ? "" : reader.GetString(19), }); } @@ -619,7 +644,8 @@ THEN SUM(rs.avg_rowcount * rs.count_executions) / SUM(rs.count_executions) MIN(rs.min_dop), MAX(rs.max_dop), MAX(rs.last_execution_time), - SUM(rs.avg_query_max_used_memory * rs.count_executions) + SUM(rs.avg_query_max_used_memory * rs.count_executions), + MAX(rs.execution_type_desc) 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 @@ -664,6 +690,7 @@ JOIN sys.query_store_query q MaxDop = (int)reader.GetInt64(16), LastExecutionUtc = reader.IsDBNull(17) ? null : ((DateTimeOffset)reader.GetValue(17)).UtcDateTime, TotalMemoryMb = reader.GetDouble(18), + ExecutionTypeDesc = reader.IsDBNull(19) ? "" : reader.GetString(19), }); } @@ -1004,6 +1031,16 @@ public static async Task FetchGroupedByQueryHashAsync( parameters.Add(new SqlParameter("@filterModule", moduleVal)); } var filterSql = filterClauses.Count > 0 ? "\n" + string.Join("\n", filterClauses) : ""; + var phase2ExecutionTypeClause = ""; + if (filter?.ExecutionTypeDescs?.Length > 0) + { + var etParamNames = filter.ExecutionTypeDescs + .Select((_, i) => $"@executionType{i}") + .ToList(); + phase2ExecutionTypeClause = $"\nAND rs.execution_type_desc IN ({string.Join(", ", etParamNames)})"; + for (var i = 0; i < filter.ExecutionTypeDescs.Length; i++) + parameters.Add(new SqlParameter($"@executionType{i}", filter.ExecutionTypeDescs[i])); + } var sql = $@" SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; @@ -1028,7 +1065,8 @@ FROM sys.query_store_runtime_stats_interval AS rsi total_physical_reads float NOT NULL, total_memory_pages float NOT NULL, total_executions bigint NOT NULL, - last_execution_time datetimeoffset NOT NULL + last_execution_time datetimeoffset NOT NULL, + execution_type_desc nvarchar(60) NOT NULL ); INSERT INTO #plan_stats SELECT @@ -1040,9 +1078,13 @@ INSERT INTO #plan_stats SUM(rs.avg_physical_io_reads * rs.count_executions), SUM(rs.avg_query_max_used_memory * rs.count_executions), SUM(rs.count_executions), - MAX(rs.last_execution_time) + MAX(rs.last_execution_time), + RTRIM(CAST(SUBSTRING(MAX( + CONVERT(char(27), CAST(rs.last_execution_time AS datetime2(7)), 121) + + CAST(ISNULL(rs.execution_type_desc, '') AS char(60)) + ), 28, 60) AS nvarchar(60))) FROM sys.query_store_runtime_stats AS rs -WHERE EXISTS (SELECT 1 FROM #intervals AS i WHERE i.runtime_stats_interval_id = rs.runtime_stats_interval_id) +WHERE EXISTS (SELECT 1 FROM #intervals AS i WHERE i.runtime_stats_interval_id = rs.runtime_stats_interval_id){phase2ExecutionTypeClause} GROUP BY rs.plan_id OPTION (RECOMPILE); @@ -1080,6 +1122,7 @@ THEN OBJECT_SCHEMA_NAME(q.object_id) + N'.' + OBJECT_NAME(q.object_id) SUM(ps.total_memory_pages) AS total_memory_pages, SUM(ps.total_executions) AS total_executions, MAX(ps.last_execution_time) AS last_execution_time, + MAX(ps.execution_type_desc) AS execution_type_desc, ROW_NUMBER() OVER (PARTITION BY q.query_hash ORDER BY SUM(ps.{metricCol}) DESC) AS rnum FROM #plan_stats ps JOIN sys.query_store_plan p ON ps.plan_id = p.plan_id @@ -1098,7 +1141,8 @@ ELSE N'' END CAST(total_physical_reads AS bigint) AS total_physical_reads, CAST(total_memory_pages AS bigint) AS total_memory_pages, total_executions, - last_execution_time + last_execution_time, + execution_type_desc INTO #plan_hash_rows FROM ph WHERE rnum <= 5; @@ -1121,6 +1165,7 @@ THEN OBJECT_SCHEMA_NAME(q.object_id) + N'.' + OBJECT_NAME(q.object_id) CAST(ps.total_memory_pages AS bigint) AS total_memory_pages, ps.total_executions, ps.last_execution_time, + ps.execution_type_desc, ROW_NUMBER() OVER (PARTITION BY q.query_hash, p.query_plan_hash ORDER BY ps.{metricCol} DESC) AS rn_top, ROW_NUMBER() OVER (PARTITION BY q.query_hash, p.query_plan_hash ORDER BY ps.{metricCol} ASC) AS rn_bottom FROM #plan_stats ps @@ -1152,7 +1197,8 @@ FROM ranked r.total_memory_pages, r.total_executions, r.last_execution_time, -CASE WHEN r.rn_top = 1 THEN 1 ELSE 0 END AS is_top +CASE WHEN r.rn_top = 1 THEN 1 ELSE 0 END AS is_top, +r.execution_type_desc FROM #ranked_light r JOIN sys.query_store_query_text qt ON r.query_text_id = qt.query_text_id JOIN sys.query_store_plan p ON r.plan_id = p.plan_id; @@ -1189,6 +1235,7 @@ FROM ranked CountExecutions = reader.GetInt64(13), LastExecutedUtc = ((DateTimeOffset)reader.GetValue(14)).UtcDateTime, IsTopRepresentative = reader.GetInt32(15) == 1, + ExecutionTypeDesc = reader.IsDBNull(16) ? "" : reader.GetString(16), }); } @@ -1210,6 +1257,7 @@ FROM ranked TotalMemoryGrantPages = reader.GetInt64(8), CountExecutions = reader.GetInt64(9), LastExecutedUtc = ((DateTimeOffset)reader.GetValue(10)).UtcDateTime, + ExecutionTypeDesc = reader.IsDBNull(11) ? "" : reader.GetString(11), }); } } @@ -1263,6 +1311,16 @@ public static async Task FetchGroupedByModuleAsync( parameters.Add(new SqlParameter("@filterQueryHash", filter.QueryHash.Trim())); } var filterSql = filterClauses.Count > 0 ? "\n" + string.Join("\n", filterClauses) : ""; + var phase2ExecutionTypeClause = ""; + if (filter?.ExecutionTypeDescs?.Length > 0) + { + var etParamNames = filter.ExecutionTypeDescs + .Select((_, i) => $"@executionType{i}") + .ToList(); + phase2ExecutionTypeClause = $"\nAND rs.execution_type_desc IN ({string.Join(", ", etParamNames)})"; + for (var i = 0; i < filter.ExecutionTypeDescs.Length; i++) + parameters.Add(new SqlParameter($"@executionType{i}", filter.ExecutionTypeDescs[i])); + } var sql = $@" SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; @@ -1287,7 +1345,8 @@ FROM sys.query_store_runtime_stats_interval AS rsi total_physical_reads float NOT NULL, total_memory_pages float NOT NULL, total_executions bigint NOT NULL, - last_execution_time datetimeoffset NOT NULL + last_execution_time datetimeoffset NOT NULL, + execution_type_desc nvarchar(60) NOT NULL ); INSERT INTO #plan_stats SELECT @@ -1299,9 +1358,13 @@ INSERT INTO #plan_stats SUM(rs.avg_physical_io_reads * rs.count_executions), SUM(rs.avg_query_max_used_memory * rs.count_executions), SUM(rs.count_executions), - MAX(rs.last_execution_time) + MAX(rs.last_execution_time), + RTRIM(CAST(SUBSTRING(MAX( + CONVERT(char(27), CAST(rs.last_execution_time AS datetime2(7)), 121) + + CAST(ISNULL(rs.execution_type_desc, '') AS char(60)) + ), 28, 60) AS nvarchar(60))) FROM sys.query_store_runtime_stats AS rs -WHERE EXISTS (SELECT 1 FROM #intervals AS i WHERE i.runtime_stats_interval_id = rs.runtime_stats_interval_id) +WHERE EXISTS (SELECT 1 FROM #intervals AS i WHERE i.runtime_stats_interval_id = rs.runtime_stats_interval_id){phase2ExecutionTypeClause} GROUP BY rs.plan_id OPTION (RECOMPILE); @@ -1342,6 +1405,7 @@ THEN OBJECT_SCHEMA_NAME(q.object_id) + N'.' + OBJECT_NAME(q.object_id) SUM(ps.total_memory_pages) AS total_memory_pages, SUM(ps.total_executions) AS total_executions, MAX(ps.last_execution_time) AS last_execution_time, + MAX(ps.execution_type_desc) AS execution_type_desc, ROW_NUMBER() OVER (PARTITION BY CASE WHEN q.object_id <> 0 THEN OBJECT_SCHEMA_NAME(q.object_id) + N'.' + OBJECT_NAME(q.object_id) @@ -1367,7 +1431,8 @@ THEN OBJECT_SCHEMA_NAME(q.object_id) + N'.' + OBJECT_NAME(q.object_id) CAST(total_physical_reads AS bigint) AS total_physical_reads, CAST(total_memory_pages AS bigint) AS total_memory_pages, total_executions, - last_execution_time + last_execution_time, + execution_type_desc INTO #qhash_rows FROM qh WHERE rnum <= 5; @@ -1390,6 +1455,7 @@ THEN OBJECT_SCHEMA_NAME(q.object_id) + N'.' + OBJECT_NAME(q.object_id) CAST(ps.total_memory_pages AS bigint) AS total_memory_pages, ps.total_executions, ps.last_execution_time, + ps.execution_type_desc, ROW_NUMBER() OVER (PARTITION BY CASE WHEN q.object_id <> 0 THEN OBJECT_SCHEMA_NAME(q.object_id) + N'.' + OBJECT_NAME(q.object_id) @@ -1433,7 +1499,8 @@ FROM ranked r.total_memory_pages, r.total_executions, r.last_execution_time, - CASE WHEN r.rn_top = 1 THEN 1 ELSE 0 END AS is_top + CASE WHEN r.rn_top = 1 THEN 1 ELSE 0 END AS is_top, + r.execution_type_desc FROM #ranked_light r JOIN sys.query_store_query_text qt ON r.query_text_id = qt.query_text_id JOIN sys.query_store_plan p ON r.plan_id = p.plan_id; @@ -1470,6 +1537,7 @@ FROM ranked CountExecutions = reader.GetInt64(13), LastExecutedUtc = ((DateTimeOffset)reader.GetValue(14)).UtcDateTime, IsTopRepresentative = reader.GetInt32(15) == 1, + ExecutionTypeDesc = reader.IsDBNull(16) ? "" : reader.GetString(16), }); } @@ -1490,6 +1558,7 @@ FROM ranked TotalMemoryGrantPages = reader.GetInt64(7), CountExecutions = reader.GetInt64(8), LastExecutedUtc = ((DateTimeOffset)reader.GetValue(9)).UtcDateTime, + ExecutionTypeDesc = reader.IsDBNull(10) ? "" : reader.GetString(10), }); } }