Deadlocks: read system_health files without a temp table
The 2012+ query used SELECT ... INTO #system_health, a write to tempdb. One SELECT with ROW_NUMBER() now returns the deadlock rows and the newest row (the read position) in the same pass. SQL Scripts and the read-only test no longer allow any exception. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
631edc2adc
commit
66aa47ecb3
5 changed files with 43 additions and 56 deletions
|
|
@ -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)`
|
- **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`.
|
(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
|
Ingen undtagelser: heller ikke midlertidige tabeller i tempdb.
|
||||||
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.
|
|
||||||
- **Per-database-forespørgsler** (indexes, statistikker, Query Store, database-scoped community-regler) kører agenten i
|
- **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.
|
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.
|
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
|
- `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.
|
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.
|
- `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/<id>.sql` (se `CONTRIBUTING.md`). Ændres en collectors SQL, peger
|
En ny regel skal derfor have sin `SQL Scripts/<id>.sql` (se `CONTRIBUTING.md`). Ændres en collectors SQL, peger
|
||||||
testen på de filer, der skal opdateres.
|
testen på de filer, der skal opdateres.
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,8 @@
|
||||||
-- Delt script: SQL Scripts/_collectors/DeadlockCollector.sql
|
-- Delt script: SQL Scripts/_collectors/DeadlockCollector.sql
|
||||||
-- Regelkode: src/SQLInsight.Rules/Concurrency/ConcurrencyRules.cs : DeadlockRule.Evaluate
|
-- Regelkode: src/SQLInsight.Rules/Concurrency/ConcurrencyRules.cs : DeadlockRule.Evaluate
|
||||||
--
|
--
|
||||||
-- Read-only: IKKE STRENGT READ-ONLY. DeadlockCollector.BuildFileSql (SQL Server 2012+) opretter en midlertidig
|
-- Read-only: Ja: kun SELECT (sys.fn_xe_file_target_read_file og DMV'er) samt SET-sessionsindstillinger. Ingen
|
||||||
-- tabel i tempdb (SELECT ... INTO #system_health) og sletter den igen (DROP TABLE #system_health) i
|
-- temp-tabeller; læsepositionen findes med ROW_NUMBER() i samme forespørgsel.
|
||||||
-- 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.
|
|
||||||
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
|
-- 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.
|
-- (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
|
-- 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)
|
-- Variant: SQL Server 2012 og nyere (system_health-filer; første læsning)
|
||||||
-- Kilde: DeadlockCollector.BuildFileSql
|
-- Kilde: DeadlockCollector.BuildFileSql
|
||||||
-- Kode: src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs : DeadlockCollector.BuildFileSql(lastFile: null, lastOffset: null)
|
-- Kode: src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs : DeadlockCollector.BuildFileSql(lastFile: null, lastOffset: null)
|
||||||
SELECT object_name, file_name, file_offset,
|
SELECT e.event_data, e.file_name, e.file_offset, e.rn
|
||||||
CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data
|
FROM (
|
||||||
INTO #system_health
|
SELECT CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data,
|
||||||
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL);
|
file_name, file_offset,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY file_name DESC, file_offset DESC) AS rn
|
||||||
SELECT event_data FROM #system_health WHERE event_data IS NOT NULL;
|
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL)
|
||||||
SELECT TOP (1) file_name, file_offset FROM #system_health ORDER BY file_name DESC, file_offset DESC;
|
) AS e
|
||||||
DROP TABLE #system_health;
|
WHERE e.event_data IS NOT NULL OR e.rn = 1;
|
||||||
GO
|
GO
|
||||||
|
|
||||||
-- Variant: SQL Server 2008 – 2008 R2, og når event-filerne ikke kan læses
|
-- Variant: SQL Server 2008 – 2008 R2, og når event-filerne ikke kan læses
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,8 @@
|
||||||
-- GRANT VIEW SERVER SECURITY STATE TO [agent]; -- SQL Server 2022+
|
-- GRANT VIEW SERVER SECURITY STATE TO [agent]; -- SQL Server 2022+
|
||||||
--
|
--
|
||||||
-- Bruges af: SQLI-DLK-001
|
-- Bruges af: SQLI-DLK-001
|
||||||
-- Read-only: IKKE STRENGT READ-ONLY. DeadlockCollector.BuildFileSql (SQL Server 2012+) opretter en midlertidig
|
-- Read-only: Ja: kun SELECT (sys.fn_xe_file_target_read_file og DMV'er) samt SET-sessionsindstillinger. Ingen
|
||||||
-- tabel i tempdb (SELECT ... INTO #system_health) og sletter den igen (DROP TABLE #system_health) i
|
-- temp-tabeller; læsepositionen findes med ROW_NUMBER() i samme forespørgsel.
|
||||||
-- 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.
|
|
||||||
-- Kørsel: Kør i SSMS som agentens login. Hver variant er sin egen batch (GO); en variant for en anden SQL Server-version
|
-- 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.
|
-- 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)
|
-- Variant: SQL Server 2012 og nyere (system_health-filer; første læsning)
|
||||||
-- Kilde: DeadlockCollector.BuildFileSql
|
-- Kilde: DeadlockCollector.BuildFileSql
|
||||||
-- Kode: src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs : DeadlockCollector.BuildFileSql(lastFile: null, lastOffset: null)
|
-- Kode: src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs : DeadlockCollector.BuildFileSql(lastFile: null, lastOffset: null)
|
||||||
SELECT object_name, file_name, file_offset,
|
SELECT e.event_data, e.file_name, e.file_offset, e.rn
|
||||||
CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data
|
FROM (
|
||||||
INTO #system_health
|
SELECT CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data,
|
||||||
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL);
|
file_name, file_offset,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY file_name DESC, file_offset DESC) AS rn
|
||||||
SELECT event_data FROM #system_health WHERE event_data IS NOT NULL;
|
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL)
|
||||||
SELECT TOP (1) file_name, file_offset FROM #system_health ORDER BY file_name DESC, file_offset DESC;
|
) AS e
|
||||||
DROP TABLE #system_health;
|
WHERE e.event_data IS NOT NULL OR e.rn = 1;
|
||||||
GO
|
GO
|
||||||
|
|
||||||
-- Variant: SQL Server 2008 – 2008 R2, og når event-filerne ikke kan læses
|
-- Variant: SQL Server 2008 – 2008 R2, og når event-filerne ikke kan læses
|
||||||
|
|
|
||||||
|
|
@ -51,16 +51,17 @@ public sealed class DeadlockCollector : Collector<DeadlockSnapshot>
|
||||||
? $"{Db.Literal(lastFile)}, {lastOffset.Value.ToString(CultureInfo.InvariantCulture)}"
|
? $"{Db.Literal(lastFile)}, {lastOffset.Value.ToString(CultureInfo.InvariantCulture)}"
|
||||||
: "NULL, NULL";
|
: "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 $"""
|
return $"""
|
||||||
SELECT object_name, file_name, file_offset,
|
SELECT e.event_data, e.file_name, e.file_offset, e.rn
|
||||||
CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data
|
FROM (
|
||||||
INTO #system_health
|
SELECT CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data,
|
||||||
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, {start});
|
file_name, file_offset,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY file_name DESC, file_offset DESC) AS rn
|
||||||
SELECT event_data FROM #system_health WHERE event_data IS NOT NULL;
|
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, {start})
|
||||||
SELECT TOP (1) file_name, file_offset FROM #system_health ORDER BY file_name DESC, file_offset DESC;
|
) AS e
|
||||||
DROP TABLE #system_health;
|
WHERE e.event_data IS NOT NULL OR e.rn = 1;
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,14 +85,17 @@ public sealed class DeadlockCollector : Collector<DeadlockSnapshot>
|
||||||
await using (var reader = await command.ExecuteReaderAsync(ct).ConfigureAwait(false))
|
await using (var reader = await command.ExecuteReaderAsync(ct).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (!reader.IsDBNull(0))
|
||||||
{
|
{
|
||||||
events.Add(reader.GetString(0));
|
events.Add(reader.GetString(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await reader.NextResultAsync(ct).ConfigureAwait(false) && await reader.ReadAsync(ct).ConfigureAwait(false))
|
if (Convert.ToInt64(reader.GetValue(3), CultureInfo.InvariantCulture) == 1)
|
||||||
{
|
{
|
||||||
file = reader.GetString(0);
|
file = reader.GetString(1);
|
||||||
offset = Convert.ToInt64(reader.GetValue(1), CultureInfo.InvariantCulture);
|
offset = Convert.ToInt64(reader.GetValue(2), CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ public sealed partial class SqlScriptTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Scripts_are_read_only_apart_from_the_documented_temp_table()
|
public void Scripts_are_read_only()
|
||||||
{
|
{
|
||||||
var problems = new List<string>();
|
var problems = new List<string>();
|
||||||
foreach (var file in AllScripts())
|
foreach (var file in AllScripts())
|
||||||
|
|
@ -142,12 +142,6 @@ public sealed partial class SqlScriptTests
|
||||||
var text = Read(file);
|
var text = Read(file);
|
||||||
var code = CommentsAndStrings().Replace(text, " ");
|
var code = CommentsAndStrings().Replace(text, " ");
|
||||||
code = AllowedStatements().Replace(code, " ");
|
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());
|
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]
|
[Theory]
|
||||||
[InlineData("SELECT name INTO dbo.x FROM sys.objects", true)]
|
[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("UPDATE dbo.t SET a = 1", true)]
|
||||||
[InlineData("EXEC sys.sp_configure N'x', 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)]
|
[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)]
|
[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();
|
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)]
|
[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();
|
private static partial Regex Writes();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue