Seen on a customer's SQL Server 2005 SP4 (legacy ODBC): - agent-jobs declared @since with an initial value (2008+): "Incorrect syntax near CONVERT" and SQLI-JOB-001/002 never ran. Now DECLARE; SET. - plan-analysis used query_hash (2008+) and failed every 30 minutes for SQLI-PLAN-001/002/003. On 2005 a statement is now identified by sql_handle + offsets, as top SQL does. It needs sys.dm_exec_text_query_plan, so the new capability TextQueryPlan (2005 SP2, 9.0.3042) is required. - COM-DB-001 used OBJECT_SCHEMA_NAME (2005 SP2+); now joins sys.objects and sys.schemas. Audit of all collector SQL and community rules without minimumVersion 10.0: the rest is 2005 syntax or behind a version/capability check. Sql2005CompatibilityTests (2005 cannot run in CI): every collector that can run on 2005 is scanned for 2008+ syntax in its SQL (inline DECLARE, +=, MERGE, CONCAT/IIF/FORMAT/TRY_*, OFFSET/FETCH, date/time types, multi-row VALUES), version-branched files listed explicitly; the same for community rules; and the 2005 variants of plan analysis and top SQL must not use query_hash. Verified to fail on the old agent-jobs SQL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
73 lines
3.8 KiB
C#
73 lines
3.8 KiB
C#
using SQLInsight.Core.Data;
|
|
using SQLInsight.Core.Instances;
|
|
|
|
namespace SQLInsight.Collectors.Performance;
|
|
|
|
/// <summary>
|
|
/// The heaviest statements (CPU, last 24 hours) from the plan cache, with their cached plans analyzed.
|
|
/// Plans are fetched as text with <c>sys.dm_exec_text_query_plan</c>: <c>sys.dm_exec_query_plan</c> returns NULL
|
|
/// for plans nested deeper than the xml type allows. TOP is applied before any plan is fetched.
|
|
/// </summary>
|
|
public sealed class PlanAnalysisCollector : Collector<PlanAnalysisSnapshot>
|
|
{
|
|
public const int TopStatements = 50;
|
|
|
|
public override string Name => "plan-analysis";
|
|
public override TimeSpan DefaultInterval => TimeSpan.FromMinutes(30);
|
|
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.DynamicManagementViews, Capability.TextQueryPlan];
|
|
|
|
protected override async Task<PlanAnalysisSnapshot> 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 };
|
|
}
|
|
|
|
/// <param name="queryHash">query_hash exists from SQL Server 2008; on 2005 a statement is its batch and offsets.</param>
|
|
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;
|
|
""";
|
|
}
|
|
}
|