A DBA should be able to review every statement SQLInsight sends to a monitored server before granting it access. "SQL Scripts/<rule-id>.sql" holds, per rule (82 built-in, 16 community), a Danish header (what and why, how C# judges the data, thresholds with config keys, severity, exact GRANTs, versions, collector file and member) and the collector's SQL verbatim, every version variant as its own batch. "_collectors/<Collector>.sql" holds each collector's queries once. The only statement that is not strictly read-only is documented in the scripts: the deadlock collector's session temp table (#system_health). Inline SQL in the collectors moved to internal constants and Build*Sql methods (no text changed), so SqlScriptTests can check that the scripts still contain the SQL the code sends, that every rule has a script, that every collector query is registered, and that nothing writes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
115 lines
7.1 KiB
Transact-SQL
115 lines
7.1 KiB
Transact-SQL
-- ===================================================================================================================
|
|
-- 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
|