namespace SQLInsight.Core.Data;
///
/// Cumulative statistics for the statements that ran since the previous collection (sys.dm_exec_query_stats,
/// grouped per statement), plus statements remembered from earlier collections so their totals can still be diffed.
///
public sealed record QueryStatsSnapshot
{
/// Most statements remembered between collections; the least recently executed are forgotten first.
public const int MaxRemembered = 2000;
public required DateTimeOffset CapturedAt { get; init; }
/// The server's own clock (GETDATE()) at capture: creation_time and last_execution_time are in it.
public required DateTime ServerTime { get; init; }
public required DateTime SqlServerStartTime { get; init; }
public required IReadOnlyList Queries { get; init; }
}
///
/// One statement's totals since its first plan in cache was compiled. Times are in microseconds, as the DMV gives them.
///
public sealed record QueryStat
{
/// query_hash as hex (SQL Server 2008+), or sql_handle:start:end on SQL Server 2005.
public required string Key { get; init; }
public string? DatabaseName { get; init; }
/// The statement, cut to characters. Customer data: never sent to the hub.
public string? Text { get; init; }
public required long Executions { get; init; }
public required long WorkerTimeUs { get; init; }
public required long ElapsedTimeUs { get; init; }
public required long LogicalReads { get; init; }
public required long PhysicalReads { get; init; }
public required long LogicalWrites { get; init; }
/// The oldest cached plan's creation_time (server clock). A newer value means the plan was recompiled.
public required DateTime CreatedAt { get; init; }
/// last_execution_time (server clock).
public required DateTime LastExecutionAt { get; init; }
public const int MaxTextLength = 4000;
}
///
/// What each statement used between two collections.
///
public sealed record QueryStatsDelta
{
public required DateTimeOffset From { get; init; }
public required DateTimeOffset To { get; init; }
public required IReadOnlyList Queries { get; init; }
///
/// The change per statement. A statement only gets a delta when its starting point is known: it was seen before
/// with the same plan, or its plan was compiled after the previous collection (then all of its totals are new).
/// A plan recompiled or evicted in between, or counters that went backwards, start over: no negative numbers.
/// After an instance restart there is no delta at all.
///
public static QueryStatsDelta? Compute(QueryStatsSnapshot? previous, QueryStatsSnapshot current)
{
ArgumentNullException.ThrowIfNull(current);
if (previous is null || previous.SqlServerStartTime != current.SqlServerStartTime || current.CapturedAt <= previous.CapturedAt)
{
return null;
}
var before = previous.Queries.ToDictionary(q => q.Key, StringComparer.Ordinal);
var changes = new List();
foreach (var now in current.Queries)
{
// Remembered from an earlier collection but not run since: nothing new.
if (now.LastExecutionAt < previous.ServerTime && before.ContainsKey(now.Key))
{
continue;
}
var newSinceLast = now.CreatedAt >= previous.ServerTime;
QueryStatDelta? delta;
if (before.TryGetValue(now.Key, out var then) && then.CreatedAt == now.CreatedAt && !WentBackwards(then, now))
{
delta = Difference(now, then);
}
else
{
delta = newSinceLast ? Difference(now, null) : null;
}
if (delta is { Executions: > 0 } or { WorkerTimeUs: > 0 })
{
changes.Add(delta);
}
}
return new QueryStatsDelta
{
From = previous.CapturedAt,
To = current.CapturedAt,
Queries = changes.OrderByDescending(q => q.WorkerTimeUs).ThenByDescending(q => q.ElapsedTimeUs).ToList(),
};
}
///
/// The snapshot to keep for the next collection: this collection's statements plus the remembered ones not seen
/// now, at most . After a restart the old ones are dropped.
///
public static IReadOnlyList Remember(QueryStatsSnapshot? previous, IReadOnlyList current, DateTime sqlServerStartTime)
{
ArgumentNullException.ThrowIfNull(current);
var keys = current.Select(q => q.Key).ToHashSet(StringComparer.Ordinal);
var older = previous is null || previous.SqlServerStartTime != sqlServerStartTime
? []
: previous.Queries.Where(q => !keys.Contains(q.Key));
return current.Concat(older.OrderByDescending(q => q.LastExecutionAt))
.Take(QueryStatsSnapshot.MaxRemembered)
.ToList();
}
private static bool WentBackwards(QueryStat then, QueryStat now) =>
now.Executions < then.Executions || now.WorkerTimeUs < then.WorkerTimeUs || now.ElapsedTimeUs < then.ElapsedTimeUs
|| now.LogicalReads < then.LogicalReads || now.PhysicalReads < then.PhysicalReads || now.LogicalWrites < then.LogicalWrites;
private static QueryStatDelta Difference(QueryStat now, QueryStat? then) => new()
{
Key = now.Key,
DatabaseName = now.DatabaseName,
Text = now.Text,
Executions = now.Executions - (then?.Executions ?? 0),
WorkerTimeUs = now.WorkerTimeUs - (then?.WorkerTimeUs ?? 0),
ElapsedTimeUs = now.ElapsedTimeUs - (then?.ElapsedTimeUs ?? 0),
LogicalReads = now.LogicalReads - (then?.LogicalReads ?? 0),
PhysicalReads = now.PhysicalReads - (then?.PhysicalReads ?? 0),
LogicalWrites = now.LogicalWrites - (then?.LogicalWrites ?? 0),
};
}
public sealed record QueryStatDelta
{
public required string Key { get; init; }
public string? DatabaseName { get; init; }
/// The statement. Customer data: shown only in the local web UI, never sent to the hub.
public string? Text { get; init; }
public required long Executions { get; init; }
public required long WorkerTimeUs { get; init; }
public required long ElapsedTimeUs { get; init; }
public required long LogicalReads { get; init; }
public required long PhysicalReads { get; init; }
public required long LogicalWrites { get; init; }
}