-- =================================================================================================================== -- SQLI-QS-002: Forceret plan kan ikke bruges -- =================================================================================================================== -- Regel-id: SQLI-QS-002 -- Titel: Forceret plan kan ikke bruges -- Alvor: High -- Versioner: SQL Server 2016 og nyere (Query Store). -- -- Hvad tjekkes, og hvorfor: -- Om en plan, der er forceret i Query Store, ikke længere kan bruges (fx fordi et index er fjernet). -- -- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt): -- Ét fund pr. plan med is_forced_plan = 1 og force_failure_count > 0 (højst 10). -- -- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi): -- Ingen. -- -- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql; [agent] = agentens login): -- GRANT VIEW SERVER STATE TO [agent]; -- SQL Server 2005-2019 -- GRANT VIEW SERVER PERFORMANCE STATE TO [agent]; -- SQL Server 2022+ (erstatter VIEW SERVER STATE) -- GRANT VIEW SERVER SECURITY STATE TO [agent]; -- SQL Server 2022+ -- GRANT VIEW ANY DEFINITION TO [agent]; -- metadata; giver IKKE adgang til data -- GRANT CONNECT ANY DATABASE TO [agent]; -- SQL Server 2014+; giver IKKE adgang til data -- Før 2014 i stedet, i hver brugerdatabase: CREATE USER [agent] FOR LOGIN [agent]; -- -- Data kommer fra (collector, fil : medlemmer, interval): -- QueryStoreCollector -- Fil: src/SQLInsight.Collectors/Performance/QueryStoreCollector.cs -- Medlemmer: QueryStoreCollector.DatabasesSql, QueryStoreCollector.BuildStatsSql(database), -- QueryStoreCollector.BuildFailedForcedSql(database) -- Interval: 30 min som standard (SQLInsight:CollectorIntervals:query-store) -- Delt script: SQL Scripts/_collectors/QueryStoreCollector.sql -- Regelkode: src/SQLInsight.Rules/Performance/QueryStoreRules.cs : FailedForcedPlanRule.Evaluate -- -- Read-only: Ja: kun SELECT samt lokale variabler, SET-sessionsindstillinger. Intet ændrer data, indstillinger -- eller objekter. -- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch -- (GO); en variant for en anden SQL Server-version end din kan fejle - det er forventet. -- Per-database-forespørgsler er vist for databasen msdb, som findes overalt: ret navnet til den -- database, du vil se. -- =================================================================================================================== -- ------------------------------------------------------------------------------------------------------------------- -- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse -- ------------------------------------------------------------------------------------------------------------------- -- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner -- Kilde: SqlConnectionFactory.SessionSettings -- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; GO -- ------------------------------------------------------------------------------------------------------------------- -- QueryStoreCollector: Query Store i hver brugerdatabase, hvor det er slået til: de 100 tungeste queries over 8 dage -- og forcerede planer, der fejler. -- ------------------------------------------------------------------------------------------------------------------- -- Variant: SQL Server 2016 og nyere -- Kilde: QueryStoreCollector.DatabasesSql -- Kode: src/SQLInsight.Collectors/Performance/QueryStoreCollector.cs : QueryStoreCollector.DatabasesSql SELECT name FROM sys.databases WHERE is_query_store_on = 1 AND state = 0 AND database_id > 4 GO -- Variant: SQL Server 2016 og nyere: én gang pr. database med Query Store (vist for msdb) -- Kilde: QueryStoreCollector.BuildStatsSql -- Kode: src/SQLInsight.Collectors/Performance/QueryStoreCollector.cs : QueryStoreCollector.BuildStatsSql(database) DECLARE @now datetimeoffset = SYSDATETIMEOFFSET(); DECLARE @recent datetimeoffset = DATEADD(hour, -24, @now); DECLARE @history datetimeoffset = DATEADD(day, -8, @now); WITH stats AS ( SELECT p.query_id, p.plan_id, CASE WHEN i.start_time >= @recent THEN 1 ELSE 0 END AS is_recent, SUM(rs.count_executions) AS execs, SUM(rs.avg_duration * rs.count_executions) AS total_duration, SUM(rs.avg_cpu_time * rs.count_executions) AS total_cpu, SUM(rs.avg_logical_io_reads * rs.count_executions) AS total_reads, MAX(rs.stdev_duration) AS max_stdev_duration, MAX(rs.max_duration) AS max_duration, MIN(rs.min_duration) AS min_duration, MAX(rs.last_execution_time) AS last_execution FROM [msdb].sys.query_store_runtime_stats rs JOIN [msdb].sys.query_store_runtime_stats_interval i ON i.runtime_stats_interval_id = rs.runtime_stats_interval_id JOIN [msdb].sys.query_store_plan p ON p.plan_id = rs.plan_id WHERE i.start_time >= @history GROUP BY p.query_id, p.plan_id, CASE WHEN i.start_time >= @recent THEN 1 ELSE 0 END ), top_queries AS ( SELECT TOP (100) query_id FROM stats GROUP BY query_id ORDER BY SUM(total_duration) DESC ) SELECT s.query_id, s.plan_id, s.is_recent, s.execs, s.total_duration, s.total_cpu, s.total_reads, s.max_stdev_duration, s.max_duration, s.min_duration, CAST(SWITCHOFFSET(s.last_execution, '+00:00') AS datetime2(0)), p.is_forced_plan, LEFT(qt.query_sql_text, 2000), OBJECT_SCHEMA_NAME(q.object_id, DB_ID(N'msdb')) + N'.' + OBJECT_NAME(q.object_id, DB_ID(N'msdb')) FROM stats s JOIN top_queries t ON t.query_id = s.query_id JOIN [msdb].sys.query_store_plan p ON p.plan_id = s.plan_id JOIN [msdb].sys.query_store_query q ON q.query_id = s.query_id JOIN [msdb].sys.query_store_query_text qt ON qt.query_text_id = q.query_text_id; GO -- Variant: SQL Server 2016 og nyere: én gang pr. database med Query Store (vist for msdb) -- Kilde: QueryStoreCollector.BuildFailedForcedSql -- Kode: src/SQLInsight.Collectors/Performance/QueryStoreCollector.cs : QueryStoreCollector.BuildFailedForcedSql(database) SELECT p.query_id, p.plan_id, p.force_failure_count, p.last_force_failure_reason_desc, LEFT(qt.query_sql_text, 2000) FROM [msdb].sys.query_store_plan p JOIN [msdb].sys.query_store_query q ON q.query_id = p.query_id JOIN [msdb].sys.query_store_query_text qt ON qt.query_text_id = q.query_text_id WHERE p.is_forced_plan = 1 AND p.force_failure_count > 0; GO