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
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
130 lines
5.1 KiB
C#
130 lines
5.1 KiB
C#
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);
|
||
}
|
||
}
|