using SQLInsight.Core.Data; using SQLInsight.Core.Instances; namespace SQLInsight.Collectors.Performance; /// /// The heaviest statements (CPU, last 24 hours) from the plan cache, with their cached plans analyzed. /// Plans are fetched as text with sys.dm_exec_text_query_plan: sys.dm_exec_query_plan returns NULL /// for plans nested deeper than the xml type allows. TOP is applied before any plan is fetched. /// public sealed class PlanAnalysisCollector : Collector { public const int TopStatements = 50; public override string Name => "plan-analysis"; public override TimeSpan DefaultInterval => TimeSpan.FromMinutes(30); public override IReadOnlyCollection RequiredCapabilities => [Capability.DynamicManagementViews, Capability.TextQueryPlan]; protected override async Task CollectAsync(CollectorContext context, PlanAnalysisSnapshot? previous) { var memory = context.Capabilities.Has(Capability.QueryStatsMemoryColumns); var queryHash = context.Instance.Version.Major >= 10; var statements = await Db.QueryAsync(context.Connection, BuildSql(memory, queryHash), r => new AnalyzedStatement { QueryHash = r.String(0), DatabaseName = r.NullableString(1), StatementText = r.NullableString(2), Executions = r.Long(3), TotalWorkerTimeUs = r.Long(4), TotalElapsedTimeUs = r.Long(5), TotalLogicalReads = r.Long(6), TotalSpills = r.NullableLong(7), TotalGrantKb = r.NullableLong(8), TotalUsedGrantKb = r.NullableLong(9), LastExecution = r.NullableDateTime(10), Warnings = ShowplanAnalyzer.Analyze(r.NullableString(11)), }, context.CancellationToken).ConfigureAwait(false); return new PlanAnalysisSnapshot { CapturedAt = context.Now, Statements = statements }; } /// query_hash exists from SQL Server 2008; on 2005 a statement is its batch and offsets. internal static string BuildSql(bool memoryColumns, bool queryHash = true) { var key = queryHash ? "CONVERT(varchar(20), qs.query_hash, 1)" : "sys.fn_varbintohexstr(qs.sql_handle) + ':' + CAST(qs.statement_start_offset AS varchar(12)) + ':' + CAST(qs.statement_end_offset AS varchar(12))"; var memory = memoryColumns ? "qs.total_spills, qs.total_grant_kb, qs.total_used_grant_kb" : "CAST(NULL AS bigint), CAST(NULL AS bigint), CAST(NULL AS bigint)"; return $""" WITH top_statements AS ( SELECT TOP ({TopStatements}) qs.* FROM sys.dm_exec_query_stats qs WHERE qs.last_execution_time >= DATEADD(hour, -24, GETDATE()) ORDER BY qs.total_worker_time DESC ) SELECT {key}, DB_NAME(CONVERT(int, pa.value)), LEFT(SUBSTRING(t.text, qs.statement_start_offset / 2 + 1, (CASE WHEN qs.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2 + 1), 4000), qs.execution_count, qs.total_worker_time, qs.total_elapsed_time, qs.total_logical_reads, {memory}, qs.last_execution_time, tp.query_plan FROM top_statements qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) t OUTER APPLY sys.dm_exec_text_query_plan(qs.plan_handle, qs.statement_start_offset, qs.statement_end_offset) tp OUTER APPLY (SELECT value FROM sys.dm_exec_plan_attributes(qs.plan_handle) WHERE attribute = N'dbid') pa ORDER BY qs.total_worker_time DESC; """; } }