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),
});
}
}