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;
""";
}
}