diff --git a/SQL Scripts/README.md b/SQL Scripts/README.md index 1765e6f..d3528b6 100644 --- a/SQL Scripts/README.md +++ b/SQL Scripts/README.md @@ -61,10 +61,7 @@ Bemærk: - **Read-only.** Alle scripts læser kun: SELECT mod katalog-views og DMV'er, `DBCC TRACESTATUS`, `DBCC SQLPERF(WAITSTATS)` (kun SQL Server 2000), `xp_readerrorlog`, `xp_msver`, `sys.fn_trace_gettable`, lokale variabler og `USE`. - **Én undtagelse:** `SQLI-DLK-001` / `_collectors/DeadlockCollector.sql` (SQL Server 2012+) opretter en midlertidig - tabel `#system_health` i tempdb og sletter den igen i samme batch. Den er privat for sessionen, og ingen brugerdata, - indstillinger eller permanente objekter ændres, men det er strengt taget en skrivning i tempdb. Det står også i - filernes header. + Ingen undtagelser: heller ikke midlertidige tabeller i tempdb. - **Per-database-forespørgsler** (indexes, statistikker, Query Store, database-scoped community-regler) kører agenten i hver online brugerdatabase. Her er de vist for `msdb`, som findes på alle instanser, så scriptet kan køres uændret. Ret `[msdb]` / `N'msdb'` til den database, du vil se. @@ -87,7 +84,7 @@ Bemærk: - `Every_collector_query_is_registered_and_in_a_collector_script`: hver SQL-konstant og hver `Build…Sql`-metode i collectorerne er registreret, og hver registreret forespørgsel står i et `_collectors/`-script. - `Community_rule_scripts_contain_the_rule_query_verbatim`: community-reglens `query` fra JSON står ordret i filen. -- `Scripts_are_read_only_apart_from_the_documented_temp_table`: ingen skrivende sætninger ud over undtagelsen ovenfor. +- `Scripts_are_read_only`: ingen skrivende sætninger, heller ikke `SELECT … INTO #temp`. En ny regel skal derfor have sin `SQL Scripts/.sql` (se `CONTRIBUTING.md`). Ændres en collectors SQL, peger testen på de filer, der skal opdateres. diff --git a/SQL Scripts/SQLI-DLK-001.sql b/SQL Scripts/SQLI-DLK-001.sql index b4d3cf8..dc18420 100644 --- a/SQL Scripts/SQLI-DLK-001.sql +++ b/SQL Scripts/SQLI-DLK-001.sql @@ -31,11 +31,8 @@ -- Delt script: SQL Scripts/_collectors/DeadlockCollector.sql -- Regelkode: src/SQLInsight.Rules/Concurrency/ConcurrencyRules.cs : DeadlockRule.Evaluate -- --- Read-only: IKKE STRENGT READ-ONLY. DeadlockCollector.BuildFileSql (SQL Server 2012+) opretter en midlertidig --- tabel i tempdb (SELECT ... INTO #system_health) og sletter den igen (DROP TABLE #system_health) i --- samme batch. Den er privat for sessionen; ingen brugerdata, indstillinger eller permanente objekter --- ændres, men det er en skrivning i tempdb. Ring buffer-varianten (2008/2008 R2) er read-only. Øvrigt: --- kun SELECT samt SET-sessionsindstillinger. +-- Read-only: Ja: kun SELECT (sys.fn_xe_file_target_read_file og DMV'er) samt SET-sessionsindstillinger. Ingen +-- temp-tabeller; læsepositionen findes med ROW_NUMBER() i samme forespørgsel. -- 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 @@ -59,14 +56,14 @@ GO -- Variant: SQL Server 2012 og nyere (system_health-filer; første læsning) -- Kilde: DeadlockCollector.BuildFileSql -- Kode: src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs : DeadlockCollector.BuildFileSql(lastFile: null, lastOffset: null) -SELECT object_name, file_name, file_offset, - CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data -INTO #system_health -FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL); - -SELECT event_data FROM #system_health WHERE event_data IS NOT NULL; -SELECT TOP (1) file_name, file_offset FROM #system_health ORDER BY file_name DESC, file_offset DESC; -DROP TABLE #system_health; +SELECT e.event_data, e.file_name, e.file_offset, e.rn +FROM ( + SELECT CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data, + file_name, file_offset, + ROW_NUMBER() OVER (ORDER BY file_name DESC, file_offset DESC) AS rn + FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL) +) AS e +WHERE e.event_data IS NOT NULL OR e.rn = 1; GO -- Variant: SQL Server 2008 – 2008 R2, og når event-filerne ikke kan læses diff --git a/SQL Scripts/_collectors/DeadlockCollector.sql b/SQL Scripts/_collectors/DeadlockCollector.sql index 0550628..ca93978 100644 --- a/SQL Scripts/_collectors/DeadlockCollector.sql +++ b/SQL Scripts/_collectors/DeadlockCollector.sql @@ -12,11 +12,8 @@ -- GRANT VIEW SERVER SECURITY STATE TO [agent]; -- SQL Server 2022+ -- -- Bruges af: SQLI-DLK-001 --- Read-only: IKKE STRENGT READ-ONLY. DeadlockCollector.BuildFileSql (SQL Server 2012+) opretter en midlertidig --- tabel i tempdb (SELECT ... INTO #system_health) og sletter den igen (DROP TABLE #system_health) i --- samme batch. Den er privat for sessionen; ingen brugerdata, indstillinger eller permanente objekter --- ændres, men det er en skrivning i tempdb. Ring buffer-varianten (2008/2008 R2) er read-only. Øvrigt: --- kun SELECT samt SET-sessionsindstillinger. +-- Read-only: Ja: kun SELECT (sys.fn_xe_file_target_read_file og DMV'er) samt SET-sessionsindstillinger. Ingen +-- temp-tabeller; læsepositionen findes med ROW_NUMBER() i samme forespørgsel. -- Kørsel: Kør i SSMS som agentens login. Hver variant er sin egen batch (GO); en variant for en anden SQL Server-version -- end din kan fejle - det er forventet. Agenten kører altid med sessionsindstillingerne i _collectors/InstanceProbe.sql. -- =================================================================================================================== @@ -24,14 +21,14 @@ -- Variant: SQL Server 2012 og nyere (system_health-filer; første læsning) -- Kilde: DeadlockCollector.BuildFileSql -- Kode: src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs : DeadlockCollector.BuildFileSql(lastFile: null, lastOffset: null) -SELECT object_name, file_name, file_offset, - CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data -INTO #system_health -FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL); - -SELECT event_data FROM #system_health WHERE event_data IS NOT NULL; -SELECT TOP (1) file_name, file_offset FROM #system_health ORDER BY file_name DESC, file_offset DESC; -DROP TABLE #system_health; +SELECT e.event_data, e.file_name, e.file_offset, e.rn +FROM ( + SELECT CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data, + file_name, file_offset, + ROW_NUMBER() OVER (ORDER BY file_name DESC, file_offset DESC) AS rn + FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL) +) AS e +WHERE e.event_data IS NOT NULL OR e.rn = 1; GO -- Variant: SQL Server 2008 – 2008 R2, og når event-filerne ikke kan læses diff --git a/src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs b/src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs index e338d1d..308f7f5 100644 --- a/src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs +++ b/src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs @@ -51,16 +51,17 @@ public sealed class DeadlockCollector : Collector ? $"{Db.Literal(lastFile)}, {lastOffset.Value.ToString(CultureInfo.InvariantCulture)}" : "NULL, NULL"; - // Only the deadlock rows carry their XML; the rest are kept just to know where reading stopped. + // One read of the files and no temp table: the deadlock rows carry their XML, and the newest row + // (rn = 1) is returned as well, only to know where reading stopped. return $""" - SELECT object_name, file_name, file_offset, - CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data - INTO #system_health - FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, {start}); - - SELECT event_data FROM #system_health WHERE event_data IS NOT NULL; - SELECT TOP (1) file_name, file_offset FROM #system_health ORDER BY file_name DESC, file_offset DESC; - DROP TABLE #system_health; + SELECT e.event_data, e.file_name, e.file_offset, e.rn + FROM ( + SELECT CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data, + file_name, file_offset, + ROW_NUMBER() OVER (ORDER BY file_name DESC, file_offset DESC) AS rn + FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, {start}) + ) AS e + WHERE e.event_data IS NOT NULL OR e.rn = 1; """; } @@ -85,13 +86,16 @@ public sealed class DeadlockCollector : Collector { while (await reader.ReadAsync(ct).ConfigureAwait(false)) { - events.Add(reader.GetString(0)); - } + if (!reader.IsDBNull(0)) + { + events.Add(reader.GetString(0)); + } - if (await reader.NextResultAsync(ct).ConfigureAwait(false) && await reader.ReadAsync(ct).ConfigureAwait(false)) - { - file = reader.GetString(0); - offset = Convert.ToInt64(reader.GetValue(1), CultureInfo.InvariantCulture); + if (Convert.ToInt64(reader.GetValue(3), CultureInfo.InvariantCulture) == 1) + { + file = reader.GetString(1); + offset = Convert.ToInt64(reader.GetValue(2), CultureInfo.InvariantCulture); + } } } diff --git a/tests/SQLInsight.Collectors.Tests/SqlScripts/SqlScriptTests.cs b/tests/SQLInsight.Collectors.Tests/SqlScripts/SqlScriptTests.cs index d402bca..56429dc 100644 --- a/tests/SQLInsight.Collectors.Tests/SqlScripts/SqlScriptTests.cs +++ b/tests/SQLInsight.Collectors.Tests/SqlScripts/SqlScriptTests.cs @@ -134,7 +134,7 @@ public sealed partial class SqlScriptTests } [Fact] - public void Scripts_are_read_only_apart_from_the_documented_temp_table() + public void Scripts_are_read_only() { var problems = new List(); foreach (var file in AllScripts()) @@ -142,12 +142,6 @@ public sealed partial class SqlScriptTests var text = Read(file); var code = CommentsAndStrings().Replace(text, " "); code = AllowedStatements().Replace(code, " "); - if (text.Contains("-- Kilde: DeadlockCollector.BuildFileSql", StringComparison.Ordinal)) - { - // Documented exception: a session-private temp table in tempdb (see the script's Read-only line). - code = DeadlockTempTable().Replace(code, " "); - } - problems.AddRange(Writes().Matches(code).Select(m => $"{Path.GetRelativePath(Folder, file)}: {m.Value.ToUpperInvariant()}").Distinct()); } @@ -156,6 +150,7 @@ public sealed partial class SqlScriptTests [Theory] [InlineData("SELECT name INTO dbo.x FROM sys.objects", true)] + [InlineData("SELECT name INTO #x FROM sys.objects", true)] [InlineData("UPDATE dbo.t SET a = 1", true)] [InlineData("EXEC sys.sp_configure N'x', 1", true)] [InlineData("SELECT is_auto_update_stats_on, create_date FROM sys.databases -- DROP TABLE x", false)] @@ -213,9 +208,6 @@ public sealed partial class SqlScriptTests [GeneratedRegex(@"\bEXEC\s+master\.dbo\.xp_(readerrorlog|msver)\b|\bDBCC\s+TRACESTATUS\b|\bDBCC\s+SQLPERF\s*\(\s*WAITSTATS\s*\)", RegexOptions.IgnoreCase)] private static partial Regex AllowedStatements(); - [GeneratedRegex(@"\bINTO\s+#system_health\b|\bDROP\s+TABLE\s+#system_health\b", RegexOptions.IgnoreCase)] - private static partial Regex DeadlockTempTable(); - [GeneratedRegex(@"\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE|DENY|BACKUP|RESTORE|RECONFIGURE|KILL|SHUTDOWN|BULK|OPENROWSET|OPENQUERY|OPENDATASOURCE|WAITFOR|INTO|EXEC|EXECUTE|DBCC|SETUSER)\b", RegexOptions.IgnoreCase)] private static partial Regex Writes(); }