sqlinsight-pro/tests/SQLInsight.Rules.Tests/QueryStoreRuleTests.cs
Claude e4910aeba5
Some checks are pending
CI / Build og unit tests (push) Waiting to run
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
Fase 2: Query Store analysis (plan regressions, failed forced plans, parameter sensitivity)
Collector reads the 100 heaviest queries per Query Store database, split
into the last 24 hours and the week before. Rules:
- SQLI-QS-001 plan regression: new plan >= 2x slower than the best plan
  from the week before and >= 60 s extra per day; scripts the force
- SQLI-QS-002 forced plan that fails (e.g. NO_INDEX); scripts the unforce
- SQLI-QS-003 parameter sensitivity: very different plans or volatile
  durations; suggests PSP optimization / Query Store hints on 2022+
Also: backups taken in the same second a database was created count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
2026-09-29 07:33:14 +00:00

130 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using SQLInsight.Core.Data;
using SQLInsight.Core.Findings;
using SQLInsight.Rules.Engine;
using SQLInsight.Rules.Performance;
using static SQLInsight.Rules.Tests.TestInstances;
namespace SQLInsight.Rules.Tests;
public class QueryStoreRuleTests
{
private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context));
private static QueryPlanStats Plan(long query, long plan, bool recent, long execs, double avgMs, double maxMs = 0, double stdevMs = 0) => new()
{
QueryId = query,
PlanId = plan,
IsRecent = recent,
Executions = execs,
TotalDurationUs = execs * avgMs * 1000,
TotalCpuUs = execs * avgMs * 800,
TotalLogicalReads = execs * 1000,
MaxStdevDurationUs = stdevMs * 1000,
MaxDurationUs = (maxMs == 0 ? avgMs : maxMs) * 1000,
MinDurationUs = avgMs * 500,
QueryText = "SELECT * FROM dbo.Orders WHERE CustomerId = @p0",
ObjectName = "dbo.GetOrders",
};
private static RuleContext Store(params QueryPlanStats[] plans) =>
With(new QueryStoreSnapshot { CapturedAt = Now, Databases = [new QueryStoreDatabase { Name = "Sales", Plans = plans }] });
[Fact]
public void New_slow_plan_is_a_regression_with_force_script()
{
var result = Run(new PlanRegressionRule(), Store(
Plan(7, 1, recent: false, execs: 5000, avgMs: 20),
Plan(7, 1, recent: true, execs: 200, avgMs: 20),
Plan(7, 2, recent: true, execs: 3000, avgMs: 400)));
var finding = Assert.Single(result.Findings);
Assert.Equal(Severity.High, finding.Severity); // 3000 × 380 ms = 1140 s extra
Assert.Equal("query: Sales/7", finding.Subject);
Assert.Contains("@query_id = 7, @plan_id = 1", finding.FixScript, StringComparison.Ordinal);
Assert.StartsWith("USE [Sales];", finding.FixScript, StringComparison.Ordinal);
}
[Fact]
public void Slowdown_too_small_to_matter_is_ignored()
{
// 3× slower, but only 20 executions × 40 ms = 0.8 s extra per day.
var result = Run(new PlanRegressionRule(), Store(
Plan(7, 1, recent: false, execs: 100, avgMs: 20),
Plan(7, 2, recent: true, execs: 20, avgMs: 60)));
Assert.Equal(RuleOutcome.Passed, result.Outcome);
}
[Fact]
public void Same_plan_getting_slower_is_not_a_plan_regression()
{
var result = Run(new PlanRegressionRule(), Store(
Plan(7, 1, recent: false, execs: 5000, avgMs: 20),
Plan(7, 1, recent: true, execs: 5000, avgMs: 200)));
Assert.Equal(RuleOutcome.Passed, result.Outcome);
}
[Fact]
public void Failed_forced_plan_is_high_with_unforce_script()
{
var context = With(new QueryStoreSnapshot
{
CapturedAt = Now,
Databases = [new QueryStoreDatabase { Name = "Sales", Plans = [], FailedForcedPlans = [new FailedForcedPlan(7, 3, 12, "NO_INDEX", "SELECT 1")] }],
});
var finding = Assert.Single(Run(new FailedForcedPlanRule(), context).Findings);
Assert.Equal(Severity.High, finding.Severity);
Assert.Contains("Genopret det index", finding.Recommendation, StringComparison.Ordinal);
Assert.Contains("sp_query_store_unforce_plan @query_id = 7, @plan_id = 3", finding.FixScript, StringComparison.Ordinal);
}
[Fact]
public void Plans_with_very_different_durations_indicate_parameter_sensitivity()
{
var finding = Assert.Single(Run(new ParameterSensitivityRule(), Store(
Plan(9, 1, recent: true, execs: 900, avgMs: 5),
Plan(9, 2, recent: true, execs: 100, avgMs: 2000))).Findings);
Assert.Contains("2 planer", finding.Explanation, StringComparison.Ordinal);
Assert.Contains("sp_query_store_set_hints", finding.FixScript, StringComparison.Ordinal); // 2022 test instance
}
[Fact]
public void Volatile_single_plan_indicates_parameter_sensitivity()
{
var result = Run(new ParameterSensitivityRule(), Store(Plan(9, 1, recent: true, execs: 5000, avgMs: 50, maxMs: 8000, stdevMs: 300)));
Assert.Equal(RuleOutcome.Findings, result.Outcome);
}
[Fact]
public void Stable_query_is_not_parameter_sensitive()
{
var result = Run(new ParameterSensitivityRule(), Store(Plan(9, 1, recent: true, execs: 5000, avgMs: 50, maxMs: 120, stdevMs: 10)));
Assert.Equal(RuleOutcome.Passed, result.Outcome);
}
[Fact]
public void No_hints_script_before_2022()
{
var context = WithVersion("15.0.4490.9", new QueryStoreSnapshot
{
CapturedAt = Now,
Databases = [new QueryStoreDatabase { Name = "Sales", Plans = [Plan(9, 1, true, 900, 5), Plan(9, 2, true, 100, 2000)] }],
});
Assert.Null(Assert.Single(Run(new ParameterSensitivityRule(), context).Findings).FixScript);
}
[Fact]
public void Query_store_rules_are_not_measurable_before_2016()
{
var result = Run(new PlanRegressionRule(), WithVersion("12.0.6024.0"));
Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
}
}