sqlinsight-pro/src/SQLInsight.Rules/Databases/DatabaseRules.cs
Claude edb7410f76
Some checks are pending
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
CI / Build og unit tests (push) Waiting to run
Fase 2: Azure SQL Managed Instance, docs and rule catalog test
- Managed Instance reports 12.0.2000.8 but is evergreen: treated as the
  newest engine with all capabilities; new SelfManagedInfrastructure
  capability so backup, CHECKDB, files, tempdb, memory, patch level and
  lifecycle rules report 'handled by Azure' instead of false findings
- Microsoft Entra ID login (managed identity, service principal, default
  credential chain) via SqlConnection.AccessTokenCallback
- Least-privilege script recognizes Managed Instance; deadlock collector
  falls back to the ring buffer when event files cannot be read
- Web UI shows the right release for Managed Instance
- README and installation docs: notifications and Managed Instance
- Catalog test: 61 rules with unique, well-formed ids

Integration suite green on SQL Server 2017 and 2022.

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

510 lines
27 KiB
C#

using System.Globalization;
using SQLInsight.Core.Data;
using SQLInsight.Core.Findings;
using SQLInsight.Core.Instances;
using SQLInsight.Rules.Engine;
namespace SQLInsight.Rules.Databases;
internal static class Databases
{
public static IEnumerable<DatabaseInfo> Online(RuleContext context) =>
context.Data.Get<DatabaseSnapshot>().Databases.Where(d => d.State == "ONLINE");
public static string Subject(DatabaseInfo database) => $"database: {database.Name}";
public static TimeSpan Age(RuleContext context, DateTime? time) => context.Now.UtcDateTime - (time ?? DateTime.MinValue);
}
/// <summary>
/// A database setting with a value that is a known problem.
/// </summary>
public sealed class DatabaseSettingRule(
string id,
string title,
Func<DatabaseInfo, bool> isProblem,
Severity severity,
Func<DatabaseInfo, string> findingTitle,
string explanation,
string recommendation,
Func<DatabaseInfo, string> fixScript,
Reference reference) : Rule
{
public override string Id => id;
public override string Title => title;
public override RuleEvaluation Evaluate(RuleContext context) =>
Result(Databases.Online(context).Where(isProblem).Select(d => CreateFinding(
severity,
findingTitle(d),
explanation,
recommendation,
subject: Databases.Subject(d),
fixScript: fixScript(d),
references: reference)));
}
public static class SimpleDatabaseRules
{
private static string Name(DatabaseInfo d) => "[" + d.Name.Replace("]", "]]", StringComparison.Ordinal) + "]";
public static IEnumerable<IRule> Create() =>
[
new DatabaseSettingRule(
"SQLI-DB-002", "AUTO_SHRINK er slået til",
d => d.IsAutoShrink, Severity.High,
d => $"AUTO_SHRINK er slået til på {d.Name}",
"Auto shrink flytter sider rundt for at frigive plads, hvilket fragmenterer indexes fuldstændigt og koster "
+ "CPU og I/O. Filerne vokser bagefter igen med autogrowth, så cyklussen gentager sig.",
"Slå AUTO_SHRINK fra. Hvis der reelt skal frigives plads, gøres det én gang manuelt og efterfulgt af index-vedligehold.",
d => $"ALTER DATABASE {Name(d)} SET AUTO_SHRINK OFF;",
Docs.Learn("AUTO_SHRINK", "relational-databases/policy-based-management/set-the-auto-shrink-database-option-to-off")),
new DatabaseSettingRule(
"SQLI-DB-003", "AUTO_CLOSE er slået til",
d => d.IsAutoClose, Severity.Medium,
d => $"AUTO_CLOSE er slået til på {d.Name}",
"Databasen lukkes, når den sidste forbindelse slipper den. Næste forbindelse skal åbne filerne og genopbygge "
+ "cachen, og plan cache for databasen tømmes hver gang. Det giver ujævne svartider og mange logbeskeder.",
"Slå AUTO_CLOSE fra.",
d => $"ALTER DATABASE {Name(d)} SET AUTO_CLOSE OFF;",
Docs.Learn("AUTO_CLOSE", "relational-databases/policy-based-management/set-the-auto-close-database-option-to-off")),
new DatabaseSettingRule(
"SQLI-DB-004", "Sidekontrol er ikke CHECKSUM",
d => d.PageVerify is not null && d.PageVerify != "CHECKSUM", Severity.Medium,
d => $"{d.Name} bruger PAGE_VERIFY {d.PageVerify}",
"Med CHECKSUM beregner SQL Server en kontrolsum for hver side, der skrives, og opdager dermed korruption fra disk, "
+ "controller eller driver, så snart siden læses. TORN_PAGE_DETECTION fanger kun en brøkdel, NONE intet.",
"Skift til CHECKSUM. Det gælder kun sider, der skrives efterfølgende, så kør også DBCC CHECKDB regelmæssigt.",
d => $"ALTER DATABASE {Name(d)} SET PAGE_VERIFY CHECKSUM;",
Docs.Learn("ALTER DATABASE SET options", "t-sql/statements/alter-database-transact-sql-set-options")),
new DatabaseSettingRule(
"SQLI-DB-005", "TRUSTWORTHY er slået til",
d => d.IsTrustworthy && !string.Equals(d.Name, "msdb", StringComparison.OrdinalIgnoreCase), Severity.Medium,
d => $"TRUSTWORTHY er slået til på {d.Name}",
"TRUSTWORTHY får SQL Server til at stole på kode i databasen, når den efterligner (impersonerer) brugere på "
+ "serverniveau. Ejes databasen af en sysadmin, kan en bruger med db_owner blive sysadmin.",
"Slå TRUSTWORTHY fra, og brug certifikat- eller modulsignering til de få moduler, der har brug for adgang uden for databasen.",
d => $"ALTER DATABASE {Name(d)} SET TRUSTWORTHY OFF;",
Docs.Learn("TRUSTWORTHY database property", "relational-databases/security/trustworthy-database-property")),
];
}
/// <summary>
/// Databases that are not usable.
/// </summary>
public sealed class DatabaseStateRule : Rule
{
public override string Id => "SQLI-DB-001";
public override string Title => "Database er ikke tilgængelig";
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
return Result(context.Data.Get<DatabaseSnapshot>().Databases
.Where(d => d.State is "SUSPECT" or "RECOVERY_PENDING" or "EMERGENCY" or "OFFLINE")
.Select(d => CreateFinding(
d.State == "OFFLINE" ? Severity.Low : Severity.Critical,
$"{d.Name} er {d.State}",
d.State switch
{
"SUSPECT" => "SQL Server kunne ikke gennemføre recovery, typisk på grund af korruption eller en manglende/ødelagt fil. Databasen kan ikke bruges.",
"RECOVERY_PENDING" => "Recovery kunne ikke starte, typisk fordi en fil mangler, er låst eller disken er fuld. Databasen kan ikke bruges.",
"EMERGENCY" => "Databasen er sat i EMERGENCY-tilstand, normalt som led i en nødreparation. Kun sysadmin kan læse den.",
_ => "Databasen er taget offline. Det kan være bevidst, men den bliver hverken backet op eller tjekket.",
},
d.State == "OFFLINE"
? "Bekræft, at det er bevidst. Ellers: sæt den online eller fjern den."
: "Se SQL Server error log for årsagen. Gendan fra backup, hvis filerne er beskadigede. Kør ikke DBCC CHECKDB med REPAIR_ALLOW_DATA_LOSS uden at forstå konsekvensen: den sletter data.",
[new Evidence("Tilstand", d.State)],
subject: Databases.Subject(d))));
}
}
/// <summary>
/// Compatibility level below the engine's native level.
/// </summary>
public sealed class CompatibilityLevelRule : Rule
{
public override string Id => "SQLI-DB-006";
public override string Title => "Database kører med et ældre compatibility level";
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
var native = context.Instance.Version.Major * 10;
return Result(Databases.Online(context)
.Where(d => !d.IsSystemDatabase && d.CompatibilityLevel < native)
.Select(d => CreateFinding(
Severity.Low,
$"{d.Name} kører med compatibility level {d.CompatibilityLevel} (instansen understøtter {native})",
"Databasen bruger query optimizer og T-SQL-adfærd fra en ældre version og går glip af forbedringer som "
+ "adaptive joins, batch mode på rowstore og bedre kardinalitetsestimering.",
"Hæv compatibility level kontrolleret: slå Query Store til, saml en baseline, hæv niveauet, og brug Query Store til "
+ "at finde og låse eventuelle regredierede planer.",
[new Evidence("Compatibility level", d.CompatibilityLevel.ToString(CultureInfo.InvariantCulture)),
new Evidence("Instansens niveau", native.ToString(CultureInfo.InvariantCulture))],
subject: Databases.Subject(d),
references: Docs.Learn("Compatibility level", "relational-databases/databases/view-or-change-the-compatibility-level-of-a-database"))));
}
}
/// <summary>
/// Databases without a recent full backup.
/// </summary>
public sealed class FullBackupRule : Rule
{
public static readonly TimeSpan MaxAge = TimeSpan.FromDays(7);
public override string Id => "SQLI-DB-007";
public override string Title => "Database mangler en nylig fuld backup";
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.SelfManagedInfrastructure];
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
var findings = new List<Finding>();
foreach (var d in Databases.Online(context))
{
if (d.IsReplicaOrMirror || string.Equals(d.Name, "model", StringComparison.OrdinalIgnoreCase)
|| Databases.Age(context, d.CreateDate) < TimeSpan.FromDays(1))
{
continue;
}
var age = Databases.Age(context, d.LastFullBackup);
if (d.LastFullBackup is not null && age <= MaxAge)
{
continue;
}
var severity = d.LastFullBackup is null ? Severity.Critical : Severity.High;
var agNote = string.Empty;
if (d.IsInAvailabilityGroup)
{
severity = Severity.Medium;
agNote = " Databasen er i en availability group, så backups kan være taget på en anden replika; tjek den.";
}
findings.Add(CreateFinding(
severity,
d.LastFullBackup is null ? $"{d.Name} har aldrig haft en fuld backup" : $"{d.Name}: seneste fulde backup er {Format.Age(age)} gammel",
"Uden en nylig fuld backup kan databasen ikke gendannes efter en diskfejl, korruption eller en fejlagtig sletning, "
+ "og hver differentiel og log-backup er afhængig af den." + agNote,
"Planlæg fulde backups mindst ugentligt (dagligt for vigtige databaser) til et andet medie end databasefilerne, "
+ "og test en gendannelse jævnligt. En backup, der aldrig er testet, er ikke en backup.",
[new Evidence("Seneste fulde backup", Format.Date(d.LastFullBackup)), new Evidence("Recovery model", d.RecoveryModel)],
subject: Databases.Subject(d),
references: Docs.Learn("Backup and restore", "relational-databases/backup-restore/back-up-and-restore-of-sql-server-databases")));
}
return Result(findings);
}
}
/// <summary>
/// FULL/BULK_LOGGED databases without log backups: the log grows forever and point-in-time recovery is lost.
/// </summary>
public sealed class LogBackupRule : Rule
{
public static readonly TimeSpan MaxAge = TimeSpan.FromHours(24);
public override string Id => "SQLI-DB-008";
public override string Title => "Database i FULL recovery uden log-backups";
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.SelfManagedInfrastructure];
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
return Result(Databases.Online(context)
.Where(d => d.RecoveryModel is "FULL" or "BULK_LOGGED" && !d.IsReplicaOrMirror && !d.IsReadOnly
&& !string.Equals(d.Name, "model", StringComparison.OrdinalIgnoreCase)
&& d.LastFullBackup is not null // Before the first full backup the log truncates as in SIMPLE.
&& (d.LastLogBackup is null || Databases.Age(context, d.LastLogBackup) > MaxAge))
.Select(d => CreateFinding(
d.IsInAvailabilityGroup ? Severity.Medium : Severity.High,
d.LastLogBackup is null
? $"{d.Name} er i {d.RecoveryModel} recovery, men har aldrig haft en log-backup"
: $"{d.Name}: seneste log-backup er {Format.Age(Databases.Age(context, d.LastLogBackup))} gammel",
$"I {d.RecoveryModel} recovery frigives plads i transaktionsloggen først, når loggen er backet op. Uden log-backups "
+ "vokser loggen, til disken er fuld, og al data siden seneste fulde backup går tabt ved et nedbrud.",
"Tag log-backups med et interval, der passer til det datatab, forretningen kan tåle (typisk 5-15 minutter). "
+ "Er point-in-time restore ikke nødvendigt, så skift til SIMPLE recovery i stedet.",
[new Evidence("Recovery model", d.RecoveryModel), new Evidence("Seneste log-backup", Format.Date(d.LastLogBackup)),
new Evidence("Seneste fulde backup", Format.Date(d.LastFullBackup))],
subject: Databases.Subject(d),
references: Docs.Learn("Transaction log backups", "relational-databases/backup-restore/transaction-log-backups-sql-server"))));
}
}
/// <summary>
/// DBCC CHECKDB not run recently.
/// </summary>
public sealed class CheckDbRule : Rule
{
public static readonly TimeSpan MaxAge = TimeSpan.FromDays(7);
public override string Id => "SQLI-DB-009";
public override string Title => "DBCC CHECKDB er ikke kørt for nylig";
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.SelfManagedInfrastructure, Capability.LastGoodCheckDbTime];
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
var findings = new List<Finding>();
foreach (var d in Databases.Online(context))
{
if (d.LastGoodCheckDb is not { } last || Databases.Age(context, d.CreateDate) < MaxAge)
{
continue;
}
var never = last.Year <= 1900;
var age = Databases.Age(context, last);
if (!never && age <= MaxAge)
{
continue;
}
findings.Add(CreateFinding(
never || age > TimeSpan.FromDays(30) ? Severity.High : Severity.Medium,
never ? $"DBCC CHECKDB har aldrig kørt fejlfrit på {d.Name}" : $"{d.Name}: seneste fejlfri DBCC CHECKDB er {Format.Age(age)} gammel",
"Korruption opdages først, når en beskadiget side læses, og kan brede sig til backups i ugevis. Jo længere siden "
+ "seneste CHECKDB, jo længere tilbage skal man måske gendanne, og jo større datatab.",
"Kør DBCC CHECKDB WITH NO_INFOMSGS, ALL_ERRORMSGS mindst ugentligt, gerne på en gendannet kopi, hvis vinduet er for kort. "
+ "Behold backups længere end intervallet mellem to CHECKDB.",
[new Evidence("Seneste fejlfri CHECKDB", never ? "aldrig" : Format.Date(last))],
subject: Databases.Subject(d),
references: Docs.Learn("DBCC CHECKDB", "t-sql/database-console-commands/dbcc-checkdb-transact-sql")));
}
return Result(findings);
}
}
/// <summary>
/// Pages SQL Server has recorded as corrupt.
/// </summary>
public sealed class SuspectPagesRule : Rule
{
public override string Id => "SQLI-DB-010";
public override string Title => "Korrupte sider registreret i suspect_pages";
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.DynamicManagementViews];
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
return Result(context.Data.Get<SuspectPageSnapshot>().Pages
.Where(p => p.IsUnresolved)
.GroupBy(p => p.DatabaseName, StringComparer.OrdinalIgnoreCase)
.Select(g => CreateFinding(
Severity.Critical,
$"{g.Key}: {g.Count()} korrupte sider",
"SQL Server har mødt sider med I/O-fejl, forkert kontrolsum eller torn pages (fejl 823/824/825). Det er næsten "
+ "altid et tegn på fejl i lagersystemet, og mere korruption følger ofte efter.",
"Kør DBCC CHECKDB straks for at se omfanget. Gendan de berørte sider eller hele databasen fra backup (page restore er "
+ "muligt i FULL recovery). Undersøg disk, controller og drivere, og tjek Windows' systemlog.",
g.Take(20).Select(p => new Evidence($"Fil {p.FileId}, side {p.PageId}", $"hændelse {p.EventType}, {p.ErrorCount} fejl, senest {Format.Date(p.LastUpdate)}")),
subject: $"database: {g.Key}",
references: Docs.Learn("suspect_pages", "relational-databases/backup-restore/manage-the-suspect-pages-table-sql-server"))));
}
}
/// <summary>
/// Too many virtual log files slow down recovery, restores and log backups.
/// </summary>
public sealed class VirtualLogFileRule : Rule
{
public const int Threshold = 1000;
public override string Id => "SQLI-DB-011";
public override string Title => "Transaktionsloggen har for mange VLF'er";
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.SelfManagedInfrastructure, Capability.LogInfoDmv];
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
return Result(Databases.Online(context)
.Where(d => d.VirtualLogFileCount > Threshold)
.Select(d => CreateFinding(
d.VirtualLogFileCount > 10 * Threshold ? Severity.High : Severity.Medium,
$"{d.Name} har {Format.Number(d.VirtualLogFileCount!.Value)} VLF'er i transaktionsloggen",
"Loggen er vokset i mange små trin. Hver vækst opretter nye virtuelle logfiler, og mange VLF'er gør crash recovery, "
+ "restore, log-backups, replikering og AG-synkronisering markant langsommere.",
"Tag en log-backup (eller CHECKPOINT i SIMPLE), shrink logfilen, og lad den vokse igen i få, store trin til den "
+ "nødvendige størrelse. Sæt en fast autogrowth på fx 1024 MB.",
[new Evidence("VLF'er", Format.Number(d.VirtualLogFileCount!.Value))],
subject: Databases.Subject(d),
references: Docs.Learn("Transaction log architecture", "relational-databases/sql-server-transaction-log-architecture-and-management-guide"))));
}
}
/// <summary>
/// Autogrowth settings that cause many or unpredictable growths.
/// </summary>
public sealed class FileGrowthRule : Rule
{
public override string Id => "SQLI-DB-012";
public override string Title => "Uhensigtsmæssig autogrowth";
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.SelfManagedInfrastructure];
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
var findings = new List<Finding>();
foreach (var d in Databases.Online(context))
{
foreach (var f in d.Files.Where(f => f.Growth > 0 && f.SizeMb >= 1024))
{
string? problem = null;
if (f.IsPercentGrowth)
{
problem = $"vokser med {f.Growth} %, dvs. ca. {Format.Number(f.SizeMb * f.Growth / 100)} MB næste gang og mere hver gang";
}
else if (f.Growth < 64)
{
problem = $"vokser kun {f.Growth} MB ad gangen på en fil på {Format.Number(f.SizeMb)} MB";
}
if (problem is null)
{
continue;
}
findings.Add(CreateFinding(
Severity.Low,
$"{d.Name}/{f.LogicalName} {problem}",
"Procentvis vækst bliver større og langsommere, jo større filen er. Små faste trin giver mange vækster, "
+ "fragmentering og, for logfiler, mange VLF'er. Logfiler (og datafiler uden instant file initialization) "
+ "skal nulstilles ved vækst, så brugerne venter imens.",
"Sæt en fast vækst i MB, fx 256-1024 MB for datafiler og 512-1024 MB for logfiler, og størrelsesplanlæg "
+ "filerne, så autogrowth kun er et sikkerhedsnet.",
[new Evidence("Fil", f.PhysicalName), new Evidence("Størrelse (MB)", Format.Number(f.SizeMb)),
new Evidence("Autogrowth", f.IsPercentGrowth ? $"{f.Growth} %" : $"{f.Growth} MB")],
subject: $"file: {d.Name}/{f.LogicalName}",
fixScript: $"ALTER DATABASE [{d.Name.Replace("]", "]]", StringComparison.Ordinal)}] MODIFY FILE (NAME = N'{f.LogicalName.Replace("'", "''", StringComparison.Ordinal)}', FILEGROWTH = {(f.Type == "LOG" ? 1024 : 512)}MB);"));
}
}
return Result(findings);
}
}
/// <summary>
/// Files that will soon hit their maximum size.
/// </summary>
public sealed class FileMaxSizeRule : Rule
{
// The log file default "max" of 2 TB is effectively unlimited.
private const long EffectivelyUnlimitedMb = 2 * 1024 * 1024;
public override string Id => "SQLI-DB-013";
public override string Title => "Databasefil nærmer sig sin maksimale størrelse";
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.SelfManagedInfrastructure];
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
return Result(Databases.Online(context).SelectMany(d => d.Files
.Where(f => f.MaxSizeMb > 0 && f.MaxSizeMb < EffectivelyUnlimitedMb && f.SizeMb >= f.MaxSizeMb * 0.9)
.Select(f => CreateFinding(
f.SizeMb >= f.MaxSizeMb ? Severity.Critical : Severity.High,
$"{d.Name}/{f.LogicalName} er {Format.Number(100.0 * f.SizeMb / f.MaxSizeMb)} % af sin maksimale størrelse",
"Filen kan ikke vokse ud over MAXSIZE. Når den er fuld, fejler alle ændringer (fejl 1105 for data, 9002 for log), "
+ "selvom der er plads på disken.",
"Hæv eller fjern MAXSIZE, eller frigør plads i filen. For logfiler: find ud af, hvad der forhindrer genbrug "
+ "(log_reuse_wait_desc i sys.databases).",
[new Evidence("Størrelse (MB)", Format.Number(f.SizeMb)), new Evidence("Maks. (MB)", Format.Number(f.MaxSizeMb))],
subject: $"file: {d.Name}/{f.LogicalName}"))));
}
}
/// <summary>
/// Query Store switched itself to read-only or error.
/// </summary>
public sealed class QueryStoreStateRule : Rule
{
public override string Id => "SQLI-DB-014";
public override string Title => "Query Store indsamler ikke længere data";
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.QueryStore];
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
return Result(Databases.Online(context)
.Where(d => d.QueryStore is { } qs && qs.DesiredState == "READ_WRITE" && qs.ActualState != "READ_WRITE")
.Select(d =>
{
var qs = d.QueryStore!;
var full = (qs.ReadOnlyReason & 65536) != 0;
return CreateFinding(
qs.ActualState == "ERROR" ? Severity.High : Severity.Medium,
$"Query Store på {d.Name} er {qs.ActualState}" + (full ? " (fuld)" : string.Empty),
"Query Store er sat til READ_WRITE, men er selv gået over i " + qs.ActualState + ". Der indsamles ingen nye planer "
+ "eller statistikker, så planregressioner kan hverken opdages eller rettes med forcerede planer.",
full
? "Hæv MAX_STORAGE_SIZE_MB, eller sænk opbevaringstiden (STALE_QUERY_THRESHOLD_DAYS), og sæt QUERY_CAPTURE_MODE = AUTO. Sæt derefter OPERATION_MODE = READ_WRITE igen."
: "Se readonly_reason i sys.database_query_store_options. Ved ERROR: kør sp_query_store_consistency_check eller slå Query Store fra og til.",
[new Evidence("Tilstand", $"ønsket {qs.DesiredState}, faktisk {qs.ActualState}"),
new Evidence("Størrelse (MB)", $"{qs.CurrentStorageMb} af {qs.MaxStorageMb}"),
new Evidence("readonly_reason", qs.ReadOnlyReason.ToString(CultureInfo.InvariantCulture))],
subject: Databases.Subject(d),
references: Docs.Learn("Query Store", "relational-databases/performance/monitoring-performance-by-using-the-query-store"));
}));
}
}
/// <summary>
/// tempdb with too few, or unevenly sized, data files.
/// </summary>
public sealed class TempdbFilesRule : Rule
{
public override string Id => "SQLI-DB-015";
public override string Title => "tempdb har for få eller ulige store datafiler";
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.SelfManagedInfrastructure];
public override RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
var files = context.Data.Get<DatabaseSnapshot>().TempdbFiles.Where(f => f.Type == "ROWS").ToList();
var cpus = context.Data.Get<ServerSnapshot>().LogicalCpuCount;
if (files.Count == 0)
{
return RuleEvaluation.Passed();
}
var recommended = Math.Min(8, cpus);
var findings = new List<Finding>();
var evidence = files.Select(f => new Evidence(f.LogicalName, $"{Format.Number(f.SizeMb)} MB, vækst {(f.IsPercentGrowth ? $"{f.Growth} %" : $"{f.Growth} MB")}")).ToList();
if (files.Count < recommended && files.Count < 4)
{
findings.Add(CreateFinding(
Severity.Medium,
$"tempdb har {files.Count} datafil(er) på en server med {cpus} logiske CPU'er",
"Hver datafil har egne allokeringssider (PFS, GAM, SGAM). Med få filer konkurrerer sessioner om de samme sider, "
+ "hvilket viser sig som PAGELATCH-ventetider på tempdb, når der oprettes mange midlertidige objekter.",
$"Brug {recommended} lige store datafiler (én pr. logisk CPU op til 8). Tilføj flere i trin af 4, hvis der stadig er contention.",
evidence,
subject: "database: tempdb",
references: Docs.Learn("tempdb", "relational-databases/databases/tempdb-database")));
}
if (files.Count > 1 && (files.Select(f => f.SizeMb).Distinct().Count() > 1 || files.Select(f => (f.Growth, f.IsPercentGrowth)).Distinct().Count() > 1))
{
findings.Add(CreateFinding(
Severity.Low,
"tempdb's datafiler er ikke ens",
"SQL Server fordeler allokeringer efter ledig plads (proportional fill). Er filerne forskellige i størrelse eller "
+ "vækst, rammer det meste den største fil, og fordelen ved flere filer forsvinder.",
"Giv alle tempdb-datafiler samme størrelse og samme faste autogrowth.",
evidence,
subject: "database: tempdb",
references: Docs.Learn("tempdb", "relational-databases/databases/tempdb-database")));
}
return Result(findings);
}
}