diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml b/Dashboard/Controls/QueryPerformanceContent.xaml
index f478a328..b7c71c9d 100644
--- a/Dashboard/Controls/QueryPerformanceContent.xaml
+++ b/Dashboard/Controls/QueryPerformanceContent.xaml
@@ -727,6 +727,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1348,6 +1364,14 @@
+
+
+
+
+
+
+
+
@@ -1364,6 +1388,14 @@
+
+
+
+
+
+
+
+
@@ -1404,6 +1436,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dashboard/Models/QueryStatsItem.cs b/Dashboard/Models/QueryStatsItem.cs
index 3f9c77cd..9ab5de1c 100644
--- a/Dashboard/Models/QueryStatsItem.cs
+++ b/Dashboard/Models/QueryStatsItem.cs
@@ -34,6 +34,8 @@ public class QueryStatsItem
public long? AvgLogicalReads { get; set; }
public long? AvgLogicalWrites { get; set; }
public long? AvgPhysicalReads { get; set; }
+ public long? MinPhysicalReads { get; set; }
+ public long? MaxPhysicalReads { get; set; }
public long? AvgRows { get; set; }
public long? MinRows { get; set; }
public long? MaxRows { get; set; }
diff --git a/Dashboard/Models/QueryStoreItem.cs b/Dashboard/Models/QueryStoreItem.cs
index ad6a1ce1..4f12ab66 100644
--- a/Dashboard/Models/QueryStoreItem.cs
+++ b/Dashboard/Models/QueryStoreItem.cs
@@ -64,6 +64,23 @@ public class QueryStoreItem
public bool IsForcedPlan { get; set; }
public short? CompatibilityLevel { get; set; }
+ // Plan forcing details
+ public long? ForceFailureCount { get; set; }
+ public string? LastForceFailureReasonDesc { get; set; }
+ public string? PlanForcingType { get; set; }
+
+ // CLR time (pre-calculated in ms)
+ public double? MinClrTimeMs { get; set; }
+ public double? MaxClrTimeMs { get; set; }
+
+ // Physical IO reads (memory-optimized tables, SQL 2017+)
+ public long? MinNumPhysicalIoReads { get; set; }
+ public long? MaxNumPhysicalIoReads { get; set; }
+
+ // Log bytes used (SQL 2017+)
+ public long? MinLogBytesUsed { get; set; }
+ public long? MaxLogBytesUsed { get; set; }
+
// Handle
public string? QueryPlanHash { get; set; }
@@ -73,10 +90,12 @@ public class QueryStoreItem
// Display helpers - memory in MB (8KB pages * 8 / 1024)
public double? AvgMemoryMb => AvgMemoryPages.HasValue ? AvgMemoryPages.Value * 8.0 / 1024.0 : null;
+ public double? MinMemoryMb => MinMemoryPages.HasValue ? MinMemoryPages.Value * 8.0 / 1024.0 : null;
public double? MaxMemoryMb => MaxMemoryPages.HasValue ? MaxMemoryPages.Value * 8.0 / 1024.0 : null;
// Tempdb in MB
public double? AvgTempdbMb => AvgTempdbPages.HasValue ? AvgTempdbPages.Value * 8.0 / 1024.0 : null;
+ public double? MinTempdbMb => MinTempdbPages.HasValue ? MinTempdbPages.Value * 8.0 / 1024.0 : null;
public double? MaxTempdbMb => MaxTempdbPages.HasValue ? MaxTempdbPages.Value * 8.0 / 1024.0 : null;
// Property aliases for XAML binding compatibility
diff --git a/Dashboard/Services/DatabaseService.QueryPerformance.cs b/Dashboard/Services/DatabaseService.QueryPerformance.cs
index e61e43da..11e987a9 100644
--- a/Dashboard/Services/DatabaseService.QueryPerformance.cs
+++ b/Dashboard/Services/DatabaseService.QueryPerformance.cs
@@ -786,6 +786,8 @@ ELSE QUOTENAME(MAX(pl.schema_name)) + N'.' + QUOTENAME(MAX(pl.object_name))
avg_logical_writes = SUM(pl.total_logical_writes) / NULLIF(SUM(pl.execution_count), 0),
total_physical_reads = SUM(pl.total_physical_reads),
avg_physical_reads = SUM(pl.total_physical_reads) / NULLIF(SUM(pl.execution_count), 0),
+ min_physical_reads = MIN(pl.min_physical_reads),
+ max_physical_reads = MAX(pl.max_physical_reads),
total_rows = SUM(pl.total_rows),
avg_rows = SUM(pl.total_rows) / NULLIF(SUM(pl.execution_count), 0),
min_rows = MIN(pl.min_rows),
@@ -849,22 +851,24 @@ USE HINT('ENABLE_PARALLEL_PLAN_PREFERENCE')
AvgLogicalWrites = reader.IsDBNull(18) ? null : reader.GetInt64(18),
TotalPhysicalReads = reader.IsDBNull(19) ? 0 : reader.GetInt64(19),
AvgPhysicalReads = reader.IsDBNull(20) ? null : reader.GetInt64(20),
- TotalRows = reader.IsDBNull(21) ? 0 : reader.GetInt64(21),
- AvgRows = reader.IsDBNull(22) ? null : reader.GetInt64(22),
- MinRows = reader.IsDBNull(23) ? null : reader.GetInt64(23),
- MaxRows = reader.IsDBNull(24) ? null : reader.GetInt64(24),
- MinDop = reader.IsDBNull(25) ? null : reader.GetInt16(25),
- MaxDop = reader.IsDBNull(26) ? null : reader.GetInt16(26),
- MinGrantKb = reader.IsDBNull(27) ? null : reader.GetInt64(27),
- MaxGrantKb = reader.IsDBNull(28) ? null : reader.GetInt64(28),
- TotalSpills = reader.IsDBNull(29) ? 0 : reader.GetInt64(29),
- MinSpills = reader.IsDBNull(30) ? null : reader.GetInt64(30),
- MaxSpills = reader.IsDBNull(31) ? null : reader.GetInt64(31),
- QueryText = reader.IsDBNull(32) ? null : reader.GetString(32),
- QueryPlanXml = reader.IsDBNull(33) ? null : reader.GetString(33),
- QueryPlanHash = reader.IsDBNull(34) ? null : reader.GetString(34),
- SqlHandle = reader.IsDBNull(35) ? null : reader.GetString(35),
- PlanHandle = reader.IsDBNull(36) ? null : reader.GetString(36)
+ MinPhysicalReads = reader.IsDBNull(21) ? null : reader.GetInt64(21),
+ MaxPhysicalReads = reader.IsDBNull(22) ? null : reader.GetInt64(22),
+ TotalRows = reader.IsDBNull(23) ? 0 : reader.GetInt64(23),
+ AvgRows = reader.IsDBNull(24) ? null : reader.GetInt64(24),
+ MinRows = reader.IsDBNull(25) ? null : reader.GetInt64(25),
+ MaxRows = reader.IsDBNull(26) ? null : reader.GetInt64(26),
+ MinDop = reader.IsDBNull(27) ? null : reader.GetInt16(27),
+ MaxDop = reader.IsDBNull(28) ? null : reader.GetInt16(28),
+ MinGrantKb = reader.IsDBNull(29) ? null : reader.GetInt64(29),
+ MaxGrantKb = reader.IsDBNull(30) ? null : reader.GetInt64(30),
+ TotalSpills = reader.IsDBNull(31) ? 0 : reader.GetInt64(31),
+ MinSpills = reader.IsDBNull(32) ? null : reader.GetInt64(32),
+ MaxSpills = reader.IsDBNull(33) ? null : reader.GetInt64(33),
+ QueryText = reader.IsDBNull(34) ? null : reader.GetString(34),
+ QueryPlanXml = reader.IsDBNull(35) ? null : reader.GetString(35),
+ QueryPlanHash = reader.IsDBNull(36) ? null : reader.GetString(36),
+ SqlHandle = reader.IsDBNull(37) ? null : reader.GetString(37),
+ PlanHandle = reader.IsDBNull(38) ? null : reader.GetString(38)
});
}
@@ -1098,7 +1102,16 @@ public async Task> GetQueryStoreDataAsync(int hoursBack = 2
is_forced_plan = MAX(CONVERT(tinyint, qsd.is_forced_plan)),
compatibility_level = MAX(qsd.compatibility_level),
query_sql_text = CONVERT(nvarchar(max), MAX(qsd.query_sql_text)),
- query_plan_hash = CONVERT(nvarchar(20), MAX(qsd.query_plan_hash), 1)
+ query_plan_hash = CONVERT(nvarchar(20), MAX(qsd.query_plan_hash), 1),
+ force_failure_count = SUM(qsd.force_failure_count),
+ last_force_failure_reason_desc = MAX(qsd.last_force_failure_reason_desc),
+ plan_forcing_type = MAX(qsd.plan_forcing_type),
+ min_clr_time_ms = MIN(qsd.min_clr_time) / 1000.0,
+ max_clr_time_ms = MAX(qsd.max_clr_time) / 1000.0,
+ min_num_physical_io_reads = MIN(qsd.min_num_physical_io_reads),
+ max_num_physical_io_reads = MAX(qsd.max_num_physical_io_reads),
+ min_log_bytes_used = MIN(qsd.min_log_bytes_used),
+ max_log_bytes_used = MAX(qsd.max_log_bytes_used)
FROM collect.query_store_data AS qsd
WHERE (
(@useCustomDates = 0 AND qsd.server_last_execution_time >= DATEADD(HOUR, -@hoursBack, SYSDATETIME()))
@@ -1172,7 +1185,16 @@ USE HINT('ENABLE_PARALLEL_PLAN_PREFERENCE')
IsForcedPlan = !reader.IsDBNull(35) && reader.GetByte(35) == 1,
CompatibilityLevel = reader.IsDBNull(36) ? null : reader.GetInt16(36),
QuerySqlText = reader.IsDBNull(37) ? null : reader.GetString(37),
- QueryPlanHash = reader.IsDBNull(38) ? null : reader.GetString(38)
+ QueryPlanHash = reader.IsDBNull(38) ? null : reader.GetString(38),
+ ForceFailureCount = reader.IsDBNull(39) ? null : reader.GetInt64(39),
+ LastForceFailureReasonDesc = reader.IsDBNull(40) ? null : reader.GetString(40),
+ PlanForcingType = reader.IsDBNull(41) ? null : reader.GetString(41),
+ MinClrTimeMs = reader.IsDBNull(42) ? null : Convert.ToDouble(reader.GetValue(42), CultureInfo.InvariantCulture),
+ MaxClrTimeMs = reader.IsDBNull(43) ? null : Convert.ToDouble(reader.GetValue(43), CultureInfo.InvariantCulture),
+ MinNumPhysicalIoReads = reader.IsDBNull(44) ? null : reader.GetInt64(44),
+ MaxNumPhysicalIoReads = reader.IsDBNull(45) ? null : reader.GetInt64(45),
+ MinLogBytesUsed = reader.IsDBNull(46) ? null : reader.GetInt64(46),
+ MaxLogBytesUsed = reader.IsDBNull(47) ? null : reader.GetInt64(47)
// QueryPlanXml is fetched on-demand via GetQueryStorePlanXmlAsync
});
}
diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml
index fb98b4ec..d921112a 100644
--- a/Lite/Controls/ServerTab.xaml
+++ b/Lite/Controls/ServerTab.xaml
@@ -365,6 +365,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Lite/Services/LocalDataService.QueryStats.cs b/Lite/Services/LocalDataService.QueryStats.cs
index f9a70d21..74a7467f 100644
--- a/Lite/Services/LocalDataService.QueryStats.cs
+++ b/Lite/Services/LocalDataService.QueryStats.cs
@@ -61,6 +61,14 @@ public async Task> GetTopQueriesByCpuAsync(int serverId, int
MAX(max_worker_time) AS max_worker_time,
MIN(min_elapsed_time) AS min_elapsed_time,
MAX(max_elapsed_time) AS max_elapsed_time,
+ MIN(min_physical_reads) AS min_physical_reads,
+ MAX(max_physical_reads) AS max_physical_reads,
+ MIN(min_rows) AS min_rows,
+ MAX(max_rows) AS max_rows,
+ MIN(min_grant_kb) AS min_grant_kb,
+ MAX(max_grant_kb) AS max_grant_kb,
+ MIN(min_spills) AS min_spills,
+ MAX(max_spills) AS max_spills,
MAX(query_plan_hash) AS query_plan_hash,
MAX(sql_handle) AS sql_handle,
MAX(plan_handle) AS plan_handle,
@@ -105,11 +113,19 @@ ORDER BY SUM(delta_elapsed_time) DESC
MaxCpuUs = reader.IsDBNull(13) ? 0 : reader.GetInt64(13),
MinElapsedUs = reader.IsDBNull(14) ? 0 : reader.GetInt64(14),
MaxElapsedUs = reader.IsDBNull(15) ? 0 : reader.GetInt64(15),
- QueryPlanHash = reader.IsDBNull(16) ? "" : reader.GetString(16),
- SqlHandle = reader.IsDBNull(17) ? "" : reader.GetString(17),
- PlanHandle = reader.IsDBNull(18) ? "" : reader.GetString(18),
- QueryText = reader.IsDBNull(19) ? "" : reader.GetString(19),
- QueryPlan = reader.IsDBNull(20) ? null : reader.GetString(20)
+ MinPhysicalReads = reader.IsDBNull(16) ? 0 : reader.GetInt64(16),
+ MaxPhysicalReads = reader.IsDBNull(17) ? 0 : reader.GetInt64(17),
+ MinRows = reader.IsDBNull(18) ? 0 : reader.GetInt64(18),
+ MaxRows = reader.IsDBNull(19) ? 0 : reader.GetInt64(19),
+ MinGrantKb = reader.IsDBNull(20) ? 0 : reader.GetInt64(20),
+ MaxGrantKb = reader.IsDBNull(21) ? 0 : reader.GetInt64(21),
+ MinSpills = reader.IsDBNull(22) ? 0 : reader.GetInt64(22),
+ MaxSpills = reader.IsDBNull(23) ? 0 : reader.GetInt64(23),
+ QueryPlanHash = reader.IsDBNull(24) ? "" : reader.GetString(24),
+ SqlHandle = reader.IsDBNull(25) ? "" : reader.GetString(25),
+ PlanHandle = reader.IsDBNull(26) ? "" : reader.GetString(26),
+ QueryText = reader.IsDBNull(27) ? "" : reader.GetString(27),
+ QueryPlan = reader.IsDBNull(28) ? null : reader.GetString(28)
});
}
@@ -386,13 +402,13 @@ public async Task> GetTopProceduresByCpuAsync(int server
MAX(max_worker_time) AS max_worker_time,
MIN(min_elapsed_time) AS min_elapsed_time,
MAX(max_elapsed_time) AS max_elapsed_time,
- SUM(total_spills) AS total_spills,
MIN(min_logical_reads) AS min_logical_reads,
MAX(max_logical_reads) AS max_logical_reads,
MIN(min_physical_reads) AS min_physical_reads,
MAX(max_physical_reads) AS max_physical_reads,
MIN(min_logical_writes) AS min_logical_writes,
MAX(max_logical_writes) AS max_logical_writes,
+ SUM(total_spills) AS total_spills,
MIN(min_spills) AS min_spills,
MAX(max_spills) AS max_spills,
MAX(cached_time) AS cached_time,
@@ -434,13 +450,13 @@ ORDER BY SUM(delta_elapsed_time) DESC
MaxWorkerTimeUs = reader.IsDBNull(11) ? 0 : reader.GetInt64(11),
MinElapsedTimeUs = reader.IsDBNull(12) ? 0 : reader.GetInt64(12),
MaxElapsedTimeUs = reader.IsDBNull(13) ? 0 : reader.GetInt64(13),
- TotalSpills = reader.IsDBNull(14) ? 0 : reader.GetInt64(14),
- MinLogicalReads = reader.IsDBNull(15) ? 0 : reader.GetInt64(15),
- MaxLogicalReads = reader.IsDBNull(16) ? 0 : reader.GetInt64(16),
- MinPhysicalReads = reader.IsDBNull(17) ? 0 : reader.GetInt64(17),
- MaxPhysicalReads = reader.IsDBNull(18) ? 0 : reader.GetInt64(18),
- MinLogicalWrites = reader.IsDBNull(19) ? 0 : reader.GetInt64(19),
- MaxLogicalWrites = reader.IsDBNull(20) ? 0 : reader.GetInt64(20),
+ MinLogicalReads = reader.IsDBNull(14) ? 0 : reader.GetInt64(14),
+ MaxLogicalReads = reader.IsDBNull(15) ? 0 : reader.GetInt64(15),
+ MinPhysicalReads = reader.IsDBNull(16) ? 0 : reader.GetInt64(16),
+ MaxPhysicalReads = reader.IsDBNull(17) ? 0 : reader.GetInt64(17),
+ MinLogicalWrites = reader.IsDBNull(18) ? 0 : reader.GetInt64(18),
+ MaxLogicalWrites = reader.IsDBNull(19) ? 0 : reader.GetInt64(19),
+ TotalSpills = reader.IsDBNull(20) ? 0 : reader.GetInt64(20),
MinSpills = reader.IsDBNull(21) ? 0 : reader.GetInt64(21),
MaxSpills = reader.IsDBNull(22) ? 0 : reader.GetInt64(22),
CachedTime = reader.IsDBNull(23) ? (DateTime?)null : reader.GetDateTime(23),
@@ -621,6 +637,14 @@ public class QueryStatsRow
public long MaxCpuUs { get; set; }
public long MinElapsedUs { get; set; }
public long MaxElapsedUs { get; set; }
+ public long MinPhysicalReads { get; set; }
+ public long MaxPhysicalReads { get; set; }
+ public long MinRows { get; set; }
+ public long MaxRows { get; set; }
+ public long MinGrantKb { get; set; }
+ public long MaxGrantKb { get; set; }
+ public long MinSpills { get; set; }
+ public long MaxSpills { get; set; }
public string QueryPlanHash { get; set; } = "";
public string SqlHandle { get; set; } = "";
public string PlanHandle { get; set; } = "";
@@ -654,13 +678,13 @@ public class ProcedureStatsRow
public long MaxWorkerTimeUs { get; set; }
public long MinElapsedTimeUs { get; set; }
public long MaxElapsedTimeUs { get; set; }
- public long TotalSpills { get; set; }
public long MinLogicalReads { get; set; }
public long MaxLogicalReads { get; set; }
public long MinPhysicalReads { get; set; }
public long MaxPhysicalReads { get; set; }
public long MinLogicalWrites { get; set; }
public long MaxLogicalWrites { get; set; }
+ public long TotalSpills { get; set; }
public long MinSpills { get; set; }
public long MaxSpills { get; set; }
public DateTime? CachedTime { get; set; }
diff --git a/install/47_create_reporting_views.sql b/install/47_create_reporting_views.sql
index fc320108..93ae386f 100644
--- a/install/47_create_reporting_views.sql
+++ b/install/47_create_reporting_views.sql
@@ -86,6 +86,44 @@ WITH
WHERE cl.collection_time >= DATEADD(DAY, -7, SYSDATETIME())
GROUP BY
cl.collector_name
+),
+ /*
+ Count consecutive recent failures per collector.
+ If the last 5+ runs are all errors, the collector is
+ actively broken regardless of 7-day failure percentage.
+ */
+ recent_failures AS
+(
+ SELECT
+ collector_name = r.collector_name,
+ consecutive_failures =
+ MIN
+ (
+ CASE
+ WHEN r.collection_status <> N'ERROR'
+ THEN r.rn
+ ELSE NULL
+ END
+ )
+ FROM
+ (
+ SELECT
+ cl.collector_name,
+ cl.collection_status,
+ rn = ROW_NUMBER() OVER
+ (
+ PARTITION BY
+ cl.collector_name
+ ORDER BY
+ cl.collection_time DESC
+ )
+ FROM config.collection_log AS cl
+ WHERE cl.collection_status NOT IN (N'CONFIG_CHANGE', N'TABLE_MISSING', N'TABLE_CREATED', N'SKIPPED')
+ AND cl.collection_time >= DATEADD(DAY, -7, SYSDATETIME())
+ ) AS r
+ WHERE r.rn <= 20
+ GROUP BY
+ r.collector_name
)
SELECT
collector_name = cs.collector_name,
@@ -99,6 +137,12 @@ SELECT
THEN N'NEVER_RUN'
WHEN DATEDIFF(HOUR, cs.last_success_time, SYSDATETIME()) > 24
THEN N'STALE'
+ WHEN rf.consecutive_failures IS NULL
+ THEN N'FAILING'
+ WHEN rf.consecutive_failures > 5
+ THEN N'FAILING'
+ WHEN rf.consecutive_failures > 3
+ THEN N'WARNING'
WHEN cs.failed_runs * 100.0 / NULLIF(cs.total_runs, 0) > 50
THEN N'FAILING'
WHEN cs.failed_runs * 100.0 / NULLIF(cs.total_runs, 0) > 10
@@ -115,7 +159,9 @@ SELECT
failed_runs_7d = cs.failed_runs,
avg_duration_ms = CONVERT(integer, cs.avg_duration_ms),
total_rows_collected_7d = cs.total_rows_collected
-FROM collector_stats AS cs;
+FROM collector_stats AS cs
+LEFT JOIN recent_failures AS rf
+ ON cs.collector_name = rf.collector_name;
GO
/*