using System.Data.Common; using SQLInsight.Core.Data; using SQLInsight.Core.Instances; namespace SQLInsight.Collectors.Performance; /// /// Reads Query Store from every user database that has it on. Only the 100 heaviest queries (by total duration over /// eight days) are returned per database, so the cost stays bounded even with a large Query Store. /// public sealed class QueryStoreCollector : Collector { public const int TopQueries = 100; internal const string DatabasesSql = """ SELECT name FROM sys.databases WHERE is_query_store_on = 1 AND state = 0 AND database_id > 4 """; public override string Name => "query-store"; public override TimeSpan DefaultInterval => TimeSpan.FromMinutes(30); public override IReadOnlyCollection RequiredCapabilities => [Capability.QueryStore]; protected override async Task CollectAsync(CollectorContext context, QueryStoreSnapshot? previous) { var ct = context.CancellationToken; var names = await Db.QueryAsync(context.Connection, DatabasesSql, r => r.String(0), ct).ConfigureAwait(false); var databases = new List(names.Count); foreach (var name in names) { databases.Add(await ReadAsync(context.Connection, name, ct).ConfigureAwait(false)); } return new QueryStoreSnapshot { CapturedAt = context.Now, Databases = databases }; } private static async Task ReadAsync(DbConnection connection, string database, CancellationToken ct) { try { var plans = await Db.QueryAsync(connection, BuildStatsSql(database), r => new QueryPlanStats { QueryId = r.Long(0), PlanId = r.Long(1), IsRecent = r.Bool(2), Executions = r.Long(3), TotalDurationUs = r.Double(4), TotalCpuUs = r.Double(5), TotalLogicalReads = r.Double(6), MaxStdevDurationUs = r.Double(7), MaxDurationUs = r.Double(8), MinDurationUs = r.Double(9), LastExecution = r.IsNull(10) ? null : new DateTimeOffset(DateTime.SpecifyKind(r.DateTime(10), DateTimeKind.Utc)), IsForced = r.Bool(11), QueryText = r.NullableString(12), ObjectName = r.NullableString(13), }, ct).ConfigureAwait(false); var failed = await Db.QueryAsync(connection, BuildFailedForcedSql(database), r => new FailedForcedPlan(r.Long(0), r.Long(1), r.Long(2), r.NullableString(3), r.NullableString(4)), ct).ConfigureAwait(false); return new QueryStoreDatabase { Name = database, Plans = plans, FailedForcedPlans = failed }; } catch (DbException ex) { return new QueryStoreDatabase { Name = database, Plans = [], Error = ex.Message }; } } internal static string BuildStatsSql(string database) { var db = Db.Identifier(database); return $""" DECLARE @now datetimeoffset = SYSDATETIMEOFFSET(); DECLARE @recent datetimeoffset = DATEADD(hour, -24, @now); DECLARE @history datetimeoffset = DATEADD(day, -8, @now); WITH stats AS ( SELECT p.query_id, p.plan_id, CASE WHEN i.start_time >= @recent THEN 1 ELSE 0 END AS is_recent, SUM(rs.count_executions) AS execs, SUM(rs.avg_duration * rs.count_executions) AS total_duration, SUM(rs.avg_cpu_time * rs.count_executions) AS total_cpu, SUM(rs.avg_logical_io_reads * rs.count_executions) AS total_reads, MAX(rs.stdev_duration) AS max_stdev_duration, MAX(rs.max_duration) AS max_duration, MIN(rs.min_duration) AS min_duration, MAX(rs.last_execution_time) AS last_execution FROM {db}.sys.query_store_runtime_stats rs JOIN {db}.sys.query_store_runtime_stats_interval i ON i.runtime_stats_interval_id = rs.runtime_stats_interval_id JOIN {db}.sys.query_store_plan p ON p.plan_id = rs.plan_id WHERE i.start_time >= @history GROUP BY p.query_id, p.plan_id, CASE WHEN i.start_time >= @recent THEN 1 ELSE 0 END ), top_queries AS ( SELECT TOP ({TopQueries}) query_id FROM stats GROUP BY query_id ORDER BY SUM(total_duration) DESC ) SELECT s.query_id, s.plan_id, s.is_recent, s.execs, s.total_duration, s.total_cpu, s.total_reads, s.max_stdev_duration, s.max_duration, s.min_duration, CAST(SWITCHOFFSET(s.last_execution, '+00:00') AS datetime2(0)), p.is_forced_plan, LEFT(qt.query_sql_text, 2000), OBJECT_SCHEMA_NAME(q.object_id, DB_ID({Db.Literal(database)})) + N'.' + OBJECT_NAME(q.object_id, DB_ID({Db.Literal(database)})) FROM stats s JOIN top_queries t ON t.query_id = s.query_id JOIN {db}.sys.query_store_plan p ON p.plan_id = s.plan_id JOIN {db}.sys.query_store_query q ON q.query_id = s.query_id JOIN {db}.sys.query_store_query_text qt ON qt.query_text_id = q.query_text_id; """; } internal static string BuildFailedForcedSql(string database) { var db = Db.Identifier(database); return $""" SELECT p.query_id, p.plan_id, p.force_failure_count, p.last_force_failure_reason_desc, LEFT(qt.query_sql_text, 2000) FROM {db}.sys.query_store_plan p JOIN {db}.sys.query_store_query q ON q.query_id = p.query_id JOIN {db}.sys.query_store_query_text qt ON qt.query_text_id = q.query_text_id WHERE p.is_forced_plan = 1 AND p.force_failure_count > 0; """; } }