-- =================================================================================================================== -- SQLI-QS-003: Parameterfølsom forespørgsel (parameter sniffing) -- =================================================================================================================== -- Regel-id: SQLI-QS-003 -- Titel: Parameterfølsom forespørgsel (parameter sniffing) -- Alvor: Medium -- Versioner: SQL Server 2016 og nyere (Query Store). -- -- Hvad tjekkes, og hvorfor: -- Om en forespørgsel har meget svingende svartider eller skifter mellem meget forskellige planer (parameter -- sniffing). -- -- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt): -- For hver query i seneste døgn, planer med mindst 10 udførelser. Fund hvis (a) mindst 2 planer, hvor den -- langsomste i gennemsnit er mindst 10 x den hurtigste og mindst 100 ms, eller (b) den mest brugte plan har mindst -- 100 udførelser, max-varighed >= 1 s, max/gns. >= 20 og max stdev/gns. >= 2. Højst 10 fund, mest samlet varighed -- først. -- -- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi): -- 100 udførelser og 1 s (ParameterSensitivityRule-konstanter) samt faktorerne 10, 20 og 2 (fast i koden). -- -- 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 : ParameterSensitivityRule.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