Extended Events:
Das mächtigste Diagnosewerkzeug — das kaum jemand richtig kennt
8.1 Profiler ist tot — und das ist gut so
Wer SQL Server Performance-Probleme diagnostiziert, denkt oft reflexartig an den SQL Server Profiler. Das Werkzeug ist seit SQL Server 2012 als deprecated markiert, in SQL Server 2019 bereits aus der SSMS-Standardinstallation entfernt, und trotzdem greifen erstaunlich viele DBAs noch danach. Das ist verständlich — Profiler ist vertraut, grafisch, und hat funktioniert. Aber es ist auch gefährlich.
Der Grund: SQL Trace (die Grundlage von Profiler) ist synchron. Jedes Event wird im SQL-Server-Thread, der das Event ausgelöst hat, direkt verarbeitet und an den Trace-Puffer übergeben. Bei hoher Last bedeutet das: bis zu 30% zusätzliche CPU-Belastung durch das Monitoring selbst. Man schaut auf ein brennendes System — und gießt nebenbei noch Öl ins Feuer.
Extended Events (XE) ist das moderne Pendant. Die Architektur ist von Grund auf asynchron: Events werden in einem lockfreien Ringpuffer gesammelt und von einem separaten Consumer-Thread verarbeitet. Der Overhead liegt bei äquivalenten Events unter 2% CPU. Das ist nicht nur eine Verbesserung — das ist eine andere Kategorie.
|
Kriterium |
SQL Trace / Profiler |
Extended Events |
|---|---|---|
|
CPU-Overhead |
Bis zu 30% bei hoher Last |
Unter 2% bei äquivalenten Events |
|
Architektur |
Synchron im Query-Thread |
Asynchron, lockfreier Puffer |
|
Granularität |
Grobkörnig (Event-Ebene) |
Sehr fein (einzelne Felder per Action) |
|
Filterung |
Post-Collection (auf Client) |
Pre-Collection (Predicate im Server) |
|
Targets |
Datei oder Netzwerk |
Datei, RAM, Histogram, Pair Matching |
|
Deployment |
Deprecated (SQL 2012+) |
Aktiv entwickelt, Zukunftsstandard |
|
Mindest-Overhead pro Session |
Immer hoch |
Konfigurierbar bis nahe Null |
Tab. 8.1: SQL Trace vs. Extended Events
8.2 Architektur: Events, Predicates, Actions, Targets
Extended Events folgt einem klaren Konzept mit vier Bausteinen. Wer diese vier Bausteine versteht, kann jede XE-Session bauen — ohne GUI, direkt in T-SQL.

Abb. 8.1: XE-Architektur: Events → Predicates → Actions → Targets
Events: Was feuert wann?
Ein Event ist ein vordefinierter Messpunkt im SQL Server Code — eine Stelle im Code, an der SQL Server sagt: "hier passiert gerade etwas Interessantes". Es gibt über 1.000 Events in SQL Server. Jedes Event hat vordefiniete Felder (Payload) — zum Beispiel duration, logical_reads, sql_text, database_id.
-- Alle verfügbaren Events und ihre Felder erkunden
-- Nützlich, wenn man nicht weiß, wie ein Event heißt
SELECT p.name AS Paket, e.name AS Event, e.description
FROM sys.dm_xe_packages p
JOIN sys.dm_xe_objects e ON p.guid = e.package_guid
WHERE e.object_type = 'event'
AND e.name LIKE '%statement%' -- z.B. alle Statement-Events suchen
ORDER BY p.name, e.name;
-- Felder eines bestimmten Events anzeigen
-- Was kann sql_statement_completed alles messen?
SELECT c.name AS Feld, c.type_name AS Typ, c.description
FROM sys.dm_xe_object_columns c
WHERE c.object_name = 'sql_statement_completed'
AND c.column_type = 'data' -- nur Datenfelder, nicht Actions
ORDER BY c.name;
Predicates: Filtere früh, filtere scharf
Predicates sind Filter, die direkt am Event ausgewertet werden — im SQL-Server-Prozess, bevor das Event überhaupt in den Puffer geschrieben wird. Das ist der entscheidende Performance-Vorteil gegenüber Profiler: Events die den Filter nicht passieren, verursachen praktisch keinen Overhead. Ein Predicate wie "duration > 1.000.000" (= über 1 Sekunde in Mikrosekunden) lässt 99% aller Statements durch — nur die langsamen werden aufgezeichnet. Die 99% kosten nichts.
Actions: Sammle was du brauchst
Actions sind zusätzliche Datenpunkte, die beim Event-Feuern gesammelt werden — Felder die nicht zum Standard-Payload des Events gehören. Typische Actions: sql_text (der vollständige SQL-Text, nicht nur ein Fragment), query_hash (zur Gruppierung ähnlicher Abfragen), callstack (für Low-Level-Diagnose). Jede Action kostet ein bisschen Overhead — also nur sammeln was wirklich gebraucht wird.
Targets: Wohin mit den Daten?
Das Target bestimmt, wohin die gefilterten und mit Actions angereicherten Events geschrieben werden. Die Wahl des Targets beeinflusst Persistenz, Performance und Abfragebarkeit der gesammelten Daten wesentlich.
|
Target |
Speicher |
Persistenz |
Wann nutzen |
|---|---|---|---|
|
ring_buffer |
RAM |
Verliert Daten bei Restart |
Kurze Live-Diagnosen, Entwicklung |
|
event_file |
Disk (.xel-Dateien) |
Persistent |
Produktion — der Standard für alles |
|
histogram |
RAM (aggregiert) |
Verliert Daten bei Restart |
Verteilungsanalyse, Häufigkeiten |
|
pair_matching |
RAM |
Verliert Daten bei Restart |
Lock-Paar-Analyse, Leak-Detection |
|
etw_classic_sync_target |
ETW-Trace |
Windows only |
Integration in Windows-Tracing |
Tab. 8.2: XE Target-Typen
8.3 Eine vollständige Session erstellen
Theorie beiseite — hier ist eine vollständige XE-Session, die langsame Abfragen auf einem Produktionssystem aufzeichnet. Die Session verwendet event_file für Persistenz und filtert auf Abfragen über einer Sekunde Laufzeit.
-- Session für langsame Abfragen (>1 Sekunde) erstellen
-- Diese Session ist produktionssicher: Predicate filtert 99%+ aller Events heraus
CREATE EVENT SESSION [SlowQueries] ON SERVER
-- Event 1: Einzelne SQL-Statements
-- duration ist in Mikrosekunden: 1.000.000 µs = 1 Sekunde
ADD EVENT sqlserver.sql_statement_completed(
ACTION(
sqlserver.sql_text, -- vollständiger SQL-Text
sqlserver.query_hash, -- Hash für Gruppierung ähnlicher Queries
sqlserver.database_name, -- welche Datenbank
sqlserver.client_hostname, -- von welchem Client
sqlserver.username -- welcher Nutzer
)
WHERE duration > 1000000 -- Filter: nur über 1 Sekunde
AND sqlserver.database_id > 4 -- Filter: keine Systemdatenbanken (1-4)
),
-- Event 2: RPC-Aufrufe (Stored Procedures, parametrierte Abfragen)
-- Wird oft vergessen, ist aber für Applikationen wichtiger als sql_statement_completed
ADD EVENT sqlserver.rpc_completed(
ACTION(
sqlserver.sql_text,
sqlserver.query_hash,
sqlserver.database_name,
sqlserver.client_hostname
)
WHERE duration > 1000000
AND sqlserver.database_id > 4
)
-- Target: Datei auf schnellem Storage
-- max_file_size: maximale Größe einer .xel-Datei in MB
-- max_rollover_files: wie viele Dateien werden behalten
ADD TARGET package0.event_file(
SET filename = N'D:\SQLLogs\XE\SlowQueries.xel',
max_file_size = 256, -- 256 MB pro Datei
max_rollover_files = 10 -- 10 Dateien = max 2560 MB Geschichte
)
-- Session-Optionen
WITH (
MAX_MEMORY = 8192 KB, -- Puffergröße im RAM
EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, -- besser kein Overhead als kein Event
MAX_DISPATCH_LATENCY = 5 SECONDS, -- max. Verzögerung bis zum Schreiben ins Target
STARTUP_STATE = ON -- automatisch nach SQL Server Neustart starten
);
-- Session starten
ALTER EVENT SESSION [SlowQueries] ON SERVER STATE = START;
8.4 Die wichtigsten Events — eine Referenz
Über 1.000 Events existieren in SQL Server — hier sind die, die du wirklich brauchst:
|
Event |
Was es misst |
Wichtigste Felder |
Typischer Einsatz |
|---|---|---|---|
|
sql_statement_completed |
Einzelne SQL-Statements |
duration, logical_reads, physical_reads, row_count |
Slow Query Tracing |
|
rpc_completed |
Stored Procs / parametrierte Queries |
duration, logical_reads, statement |
Applikations-Performance |
|
sql_batch_completed |
Gesamter SQL-Batch |
duration, batch_text |
Ad-hoc SQL, Makros |
|
lock_deadlock |
Deadlock-Ereignisse |
resource_description, deadlock_id |
Deadlock-Diagnose |
|
lock_acquired / lock_released |
Einzelne Lock-Operationen |
resource_type, mode, duration |
Lock-Analyse (Vorsicht: hohes Volumen!) |
|
page_split |
B-Tree-Seitenspaltungen |
database_id, file_id, page_id |
Index-Fragmentierung erkennen |
|
sort_warning |
Sortierungen mit Spill in TempDB |
sort_type, worktable_type |
Memory-Spills erkennen (→ Kap. 12) |
|
query_memory_grant_blocking |
Warten auf Memory Grant |
granted_memory_kb, ideal_memory_kb |
Memory-Grant-Engpässe |
|
xml_deadlock_report |
Vollständiger Deadlock-Graph als XML |
xml_report (kompletter Graph) |
Deadlock-Details (→ Kap. 14) |
|
error_reported |
SQL Server Fehler |
error_number, severity, message |
Fehler-Monitoring |
Tab. 8.3: Die wichtigsten XE Events
|
Hinweis: lock_acquired / lock_released mit Vorsicht einsetzen |
|---|
|
Diese Events feuern bei jedem einzelnen Lock — auf einem aktiven System können das Millionen Events pro Sekunde sein. Immer mit strengem Predicate einsetzen (bestimmte Datenbank, bestimmter Login, bestimmte Ressource) und niemals auf Produktion ohne Filterung laufen lassen. Für Deadlock-Diagnose ist xml_deadlock_report die richtige Wahl — der gibt den ganzen Graphen auf einmal, ohne das Lock-Volumen-Problem. |
8.5 Drei Sessions die du immer brauchst
Session 1: Deadlock-Erkennung
Deadlocks sind eines der häufigsten Locking-Probleme — und sie lassen sich hervorragend mit XE diagnostizieren. Diese Session ist besser als das Trace Flag 1222, weil sie Deadlock-Graphen als lesbares XML liefert und persistent in einer Datei speichert. Mehr zu Deadlocks und ihrer Analyse in Kapitel 14.
-- Deadlock-Session: fängt jeden Deadlock vollständig auf
-- Overhead: minimal — Deadlocks sind seltene Ereignisse
CREATE EVENT SESSION [Deadlocks] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report -- vollständiger Deadlock-Graph als XML
ADD TARGET package0.event_file(
SET filename = N'D:\SQLLogs\XE\Deadlocks.xel',
max_file_size = 64,
max_rollover_files = 5
)
WITH (STARTUP_STATE = ON);
ALTER EVENT SESSION [Deadlocks] ON SERVER STATE = START;
Session 2: Memory Spills (Sort, Hash, Exchange)
Wenn Abfragen in TempDB spillen, kostet das IO und Performance. Diese Session macht solche Spills sichtbar — als Grundlage für Memory-Grant-Optimierungen (Kapitel 12).
-- Spill-Session: erkennt Sort-Spills, Hash-Spills und Exchange-Spills
-- Spills bedeuten: Memory Grant war zu klein → SQL hat in TempDB ausgelagert
CREATE EVENT SESSION [QuerySpills] ON SERVER
-- Sort-Spill: ORDER BY, GROUP BY hat nicht in den Grant gepasst
ADD EVENT sqlserver.sort_warning(
ACTION(sqlserver.sql_text, sqlserver.query_hash, sqlserver.database_name)
),
-- Hash-Spill: Hash Join oder Hash Aggregat hat nicht gepasst
ADD EVENT sqlserver.hash_warning(
ACTION(sqlserver.sql_text, sqlserver.query_hash, sqlserver.database_name)
),
-- Exchange-Spill: Parallelismus-Buffer übergelaufen
ADD EVENT sqlserver.exchange_spill_event(
ACTION(sqlserver.sql_text, sqlserver.database_name)
)
ADD TARGET package0.ring_buffer( -- ring_buffer reicht für reaktive Diagnose
SET max_memory = 51200
)
WITH (STARTUP_STATE = ON);
ALTER EVENT SESSION [QuerySpills] ON SERVER STATE = START;
Session 3: Fehlende Indizes im Betrieb aufspüren
SQL Server generiert intern Missing-Index-Hinweise, wenn er bei einem Lookup merkt, dass ein bestimmter Index die Abfrage massiv beschleunigen würde. Diese Informationen sind auch in sys.dm_db_missing_index_details verfügbar — aber nur so lange der Speicher nicht recycelt wird. Mit XE kann man sie persistent festhalten.
-- Missing-Index-Hinweise persistent aufzeichnen
-- Nützlich für Nacht-Wartungsfenster: am nächsten Morgen auswerten was fehlt
CREATE EVENT SESSION [MissingIndexes] ON SERVER
ADD EVENT sqlserver.missing_column_statistics( -- fehlende Statistiken
ACTION(sqlserver.sql_text, sqlserver.database_name)
),
ADD EVENT sqlserver.missing_join_predicate( -- kartesische Produkte
ACTION(sqlserver.sql_text, sqlserver.database_name)
)
ADD TARGET package0.event_file(
SET filename = N'D:\SQLLogs\XE\MissingIndexes.xel',
max_file_size = 64, max_rollover_files = 3
)
WITH (STARTUP_STATE = ON);
ALTER EVENT SESSION [MissingIndexes] ON SERVER STATE = START;
8.6 Vier weitere nützliche Session-Vorlagen
Die drei Sessions aus Abschnitt 8.5 decken die häufigsten Diagnose-Szenarien ab. Es gibt aber weitere Situationen, in denen eine fertige Session-Vorlage bares Gold wert ist — besonders, wenn das Problem gerade brennt und keine Zeit für langes Nachdenken bleibt.
Session 4: Parameter Sniffing und Recompiles aufspüren
Wenn Stored Procedures plötzlich langsam werden — mal schnell, mal quälend langsam, je nach Parameter — ist Parameter Sniffing oft der Schuldige. SQL Server erstellt beim ersten Aufruf einen Ausführungsplan der für den damaligen Parameter optimal ist. Bei ungleichmäßig verteilten Daten passt dieser Plan für andere Parameter überhaupt nicht mehr. Das sql_statement_recompile-Event verrät, wann und warum Pläne neu kompiliert werden — und ist damit der erste Schritt zur Diagnose. Eine tiefergehende Behandlung des gesamten Themas Parameter Sniffing, Plan Cache und Workarounds folgt in Kapitel 18.
-- Recompile-Session: zeichnet auf wann und warum Pläne neu kompiliert werden
-- Hohe Recompile-Rate = CPU-Last auch ohne teure Einzelabfragen (→ Kap. 18)
CREATE EVENT SESSION [Recompiles] ON SERVER
ADD EVENT sqlserver.sql_statement_recompile(
ACTION(
sqlserver.sql_text, -- welches Statement wird recompiliert
sqlserver.database_name, -- in welcher Datenbank
sqlserver.query_hash, -- für Gruppierung ähnlicher Statements
sqlserver.username -- welcher Login löst die Recompiles aus
)
-- recompile_cause: 1=Schema geändert, 2=Statistiken geändert,
-- 11=Parameter Sniffing, 13=Recompile-Hint
WHERE recompile_cause != 0 -- alle Ursachen aufzeichnen
)
ADD TARGET package0.event_file(
SET filename = N'D:\SQLLogs\XE\Recompiles.xel',
max_file_size = 128,
max_rollover_files = 5
)
WITH (
MAX_MEMORY = 4096 KB,
STARTUP_STATE = ON
);
ALTER EVENT SESSION [Recompiles] ON SERVER STATE = START;
-- Auswertung: welche Statements recompilen am häufigsten?
SELECT
EventXml.value('(event/data[@name="recompile_cause"]/text)[1]', 'NVARCHAR(256)')
AS Ursache,
EventXml.value('(event/action[@name="sql_text"]/value)[1]', 'NVARCHAR(MAX)')
AS SqlText,
EventXml.value('(@timestamp)[1]', 'DATETIME2') AS Zeitstempel
FROM (
SELECT CAST(event_data AS XML) AS EventXml
FROM sys.fn_xe_file_target_read_file(
'D:\SQLLogs\XE\Recompiles*.xel', NULL, NULL, NULL
)
) AS d
ORDER BY Zeitstempel DESC;
Session 5: Fehlgeschlagene Logins und Security-Events
Fehlgeschlagene Login-Versuche landen zwar im SQL Server Error Log — aber das Error Log ist schwerfällig, schwer filterbar, und rollt irgendwann über. Mit einer XE-Session lassen sich fehlgeschlagene Logins strukturiert und dauerhaft aufzeichnen, direkt auswertbar per T-SQL. Nützlich für Security-Audits, Brute-Force-Erkennung, oder schlicht, wenn eine Applikation sich plötzlich nicht mehr anmelden kann und niemand weiß warum.
-- Login-Überwachung: fehlgeschlagene Authentifizierungen aufzeichnen
-- error_reported mit Severity 14 = Login failed (falsches Passwort, kein Zugriff)
CREATE EVENT SESSION [FailedLogins] ON SERVER
ADD EVENT sqlserver.error_reported(
ACTION(
sqlserver.client_hostname, -- von welchem Rechner kommt der Versuch
sqlserver.client_app_name, -- welche Applikation
sqlserver.username, -- welcher Login-Name wurde versucht
sqlserver.nt_username -- Windows-Nutzer falls Windows Auth
)
WHERE severity = 14 -- Severity 14 = Login-Fehler
OR error_number = 18456 -- 18456 = "Login failed for user"
)
ADD TARGET package0.event_file(
SET filename = N'D:\SQLLogs\XE\FailedLogins.xel',
max_file_size = 64,
max_rollover_files = 10 -- längere Geschichte für Sicherheitsauswertungen
)
WITH (
MAX_MEMORY = 2048 KB, -- Login-Fehler sind selten → kleiner Puffer reicht
STARTUP_STATE = ON
);
ALTER EVENT SESSION [FailedLogins] ON SERVER STATE = START;
Session 6: Autogrowth-Events — wenn Dateien im falschen Moment wachsen
Autogrowth ist einer der am meisten unterschätzten Performance-Killer. Wenn eine Datenbankdatei wächst, hält SQL Server alle Schreibvorgänge in dieser Datei an bis die Vergrößerung abgeschlossen ist. Bei kleinen Wachstumsschritten (der SQL-Server-Standard von 1 MB oder 10% ist erschreckend oft noch aktiv) kann das Dutzende Male pro Stunde passieren. Bei Sparfuchs & Partner haben wir 847 solcher Events in nur 120 Minuten gesehen — das ist kein Ausnahmefall, das ist ein Server der permanent gegen die Wand fährt. Das database_file_size_change-Event macht solche Wachstumsereignisse sichtbar und messbar. Die Ursachen — falsche Wachstums-Einstellungen und fehlende Instant File Initialization — werden in Kapitel 6 (VLFs und Log-Konfiguration) und Kapitel 10 (IO-Performance und IFI) ausführlich behandelt.
-- Autogrowth-Session: jedes Dateiwachstum aufzeichnen
-- Wann, welche Datei, wie groß, wie lang hat es gedauert?
CREATE EVENT SESSION [Autogrowth] ON SERVER
ADD EVENT sqlserver.database_file_size_change(
ACTION(
sqlserver.database_name, -- welche Datenbank hat gewachsen
sqlserver.sql_text -- welche Query hat das Wachstum ausgelöst
)
-- is_automatic = 1 bedeutet Autogrowth, 0 = manuelle Vergrößerung
WHERE is_automatic = 1 -- nur automatisches Wachstum interessiert uns
)
ADD TARGET package0.event_file(
SET filename = N'D:\SQLLogs\XE\Autogrowth.xel',
max_file_size = 64,
max_rollover_files = 5
)
WITH (
MAX_MEMORY = 2048 KB,
STARTUP_STATE = ON
);
ALTER EVENT SESSION [Autogrowth] ON SERVER STATE = START;
-- Auswertung: Autogrowth-Häufigkeit und Dauer
SELECT
EventXml.value('(event/action[@name="database_name"]/value)[1]', 'NVARCHAR(256)')
AS Datenbank,
EventXml.value('(event/data[@name="file_name"]/value)[1]', 'NVARCHAR(512)')
AS Dateiname,
EventXml.value('(event/data[@name="size_change_kb"]/value)[1]', 'BIGINT')
AS WachstumKB,
-- Dauer in ms: wie lange war die Datei blockiert?
EventXml.value('(event/data[@name="duration"]/value)[1]', 'BIGINT') / 1000
AS DauerMS,
EventXml.value('(@timestamp)[1]', 'DATETIME2') AS Zeitstempel
FROM (
SELECT CAST(event_data AS XML) AS EventXml
FROM sys.fn_xe_file_target_read_file(
'D:\SQLLogs\XE\Autogrowth*.xel', NULL, NULL, NULL
)
) AS d
ORDER BY Zeitstempel DESC;
Session 7: Blockierungen — wer blockiert wen wie lange?
Blocking ist ein klassisches OLTP-Problem: Transaction A hält einen Lock, Transaction B wartet darauf, und der Nutzer schaut auf eine eingefrorene Applikation. Das blocked_process_report-Event liefert genau dafür die Lösung — aber nur, wenn die Servereinstellung "blocked process threshold" auf einen Wert größer 0 gesetzt ist. Der Threshold gibt in Sekunden an, ab wann ein blockiertes Prozess als "blocked" gemeldet wird. Sinnvoller Wert: 5 Sekunden für OLTP-Systeme. Alles darunter verursacht zu viel Overhead, alles darüber erkennt kurze Blockierungen nicht mehr. Kapitel 14 behandelt die vollständige Blocking- und Deadlock-Analyse — dieses Event ist der Ausgangspunkt für diese Diagnose.
-- Voraussetzung: blocked process threshold konfigurieren
-- Ohne diese Einstellung feuert blocked_process_report nie
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'blocked process threshold', 5; -- Meldeschwelle: 5 Sekunden
RECONFIGURE;
-- Blocking-Session: wer blockiert wen, wie lange, mit welchen Queries?
CREATE EVENT SESSION [Blocking] ON SERVER
ADD EVENT sqlserver.blocked_process_report(
ACTION(
sqlserver.database_name, -- in welcher Datenbank passiert das Blocking
sqlserver.client_hostname -- von welchem Client kommt der blockierte Prozess
)
-- Kein Predicate nötig: das Event feuert schon gefiltert ab dem konfigurierten Threshold
)
ADD TARGET package0.event_file(
SET filename = N'D:\SQLLogs\XE\Blocking.xel',
max_file_size = 128,
max_rollover_files = 5
)
WITH (
MAX_MEMORY = 4096 KB,
MAX_DISPATCH_LATENCY = 5 SECONDS, -- schnell schreiben damit nichts verloren geht
STARTUP_STATE = ON
);
ALTER EVENT SESSION [Blocking] ON SERVER STATE = START;
-- Auswertung: Blocking-Ereignisse mit Details zu Blocker und Blocked Process
;WITH BlockingData AS (
SELECT CAST(event_data AS XML) AS EventXml
FROM sys.fn_xe_file_target_read_file(
'D:\SQLLogs\XE\Blocking*.xel', NULL, NULL, NULL
)
)
SELECT
EventXml.value('(@timestamp)[1]', 'DATETIME2') AS Zeitstempel,
-- Dauer in Sekunden bis zum Report
EventXml.value(
'(event/data[@name="blocked_process"]/value/blocked-process-report/@waittime)[1]',
'INT') / 1000 AS WartezeitSek,
-- SQL-Text des blockierten Prozesses
EventXml.value(
'(event/data[@name="blocked_process"]/value/blocked-process-report/blocked-process/process/inputbuf)[1]',
'NVARCHAR(MAX)') AS BlockedSQL,
-- SQL-Text des blockierenden Prozesses (der den Lock hält)
EventXml.value(
'(event/data[@name="blocked_process"]/value/blocked-process-report/blocking-process/process/inputbuf)[1]',
'NVARCHAR(MAX)') AS BlockingSQL
FROM BlockingData
ORDER BY Zeitstempel DESC;
|
Hinweis: Sieben Sessions — welche immer, welche bei Bedarf? |
|---|
|
Immer aktiv (STARTUP_STATE = ON, geringer Overhead): Deadlocks, SlowQueries, system_health (automatisch). |
|
Bei Bedarf aktivieren (nur, wenn das Problem bekannt oder vermutet wird): Recompiles, QuerySpills, FailedLogins, Autogrowth, Blocking. |
|
Faustregel: Die "immer aktiv"-Sessions zusammen kosten unter 0,5% CPU auf einem typischen OLTP-Server. Die restlichen Sessions nur so lange laufen lassen wie nötig — dann stoppen und Ergebnisse auswerten. |
8.7 XE-Daten auswerten: SSMS und T-SQL
Gesammelte XE-Daten nützen nichts, wenn man nicht weiß, wie man sie liest. Es gibt zwei Wege: die SSMS-GUI (komfortabel für interaktive Analyse) und T-SQL (flexibel für automatisierte Auswertungen).

Abb. 8.2: XE Session Workflow: Von der Definition zum Lesen der Daten
Weg 1: SSMS Live-Ansicht
In SSMS: Management → Extended Events → Sessions → Rechtsklick auf die Session → "Watch Live Data". Das öffnet eine Live-Ansicht der einfließenden Events. Praktisch für reaktive Diagnose, wenn man ein Problem gerade reproduzieren kann. Nicht für Dauerbetrieb — die Live-Ansicht erzeugt selbst etwas Overhead.
Weg 2: .xel-Datei in SSMS öffnen
File → Open → File → .xel-Datei auswählen. SSMS liest die Events aus der Datei und stellt sie in einer gefilterbaren Tabelle dar. Spalten lassen sich ein- und ausblenden, Werte sind nach Klick auf "Group By" aggregierbar. Für manuelle Analyse ausreichend.
Weg 3: T-SQL — flexibel und automatisierbar
Für automatisierte Auswertungen, Berichte oder Integration in Monitoring-Systeme liest man XE-Daten direkt per T-SQL. Das XML muss dabei "geshreddet" werden — XE speichert Events intern als XML-Dokumente.
-- XE event_file Daten per T-SQL auslesen und aufbereiten
-- sys.fn_xe_file_target_read_file liest .xel-Dateien und gibt XML zurück
-- Variante 1: Alle Events aus der SlowQueries-Session
;WITH XeData AS (
SELECT CAST(event_data AS XML) AS EventXml
FROM sys.fn_xe_file_target_read_file(
'D:\SQLLogs\XE\SlowQueries*.xel', -- Wildcard für alle rollover-Dateien
NULL, NULL, NULL
)
)
SELECT
-- Event-Zeitstempel
EventXml.value('(event/@timestamp)[1]', 'DATETIME2') AS Zeitstempel,
-- Dauer in Millisekunden umrechnen (intern sind es Mikrosekunden)
EventXml.value('(event/data[@name="duration"]/value)[1]', 'BIGINT') / 1000
AS DauerMS,
-- Logical Reads: Puffer-Lesevorgänge (teuer, aber kein IO)
EventXml.value('(event/data[@name="logical_reads"]/value)[1]', 'BIGINT')
AS LogicalReads,
-- SQL-Text aus der Action (nicht im Standard-Payload)
EventXml.value('(event/action[@name="sql_text"]/value)[1]', 'NVARCHAR(MAX)')
AS SqlText,
-- Datenbankname
EventXml.value('(event/action[@name="database_name"]/value)[1]', 'NVARCHAR(256)')
AS Datenbank,
-- Query Hash für Gruppierung
EventXml.value('(event/action[@name="query_hash"]/value)[1]', 'NVARCHAR(64)')
AS QueryHash
FROM XeData
WHERE EventXml.value('(event/data[@name="duration"]/value)[1]', 'BIGINT') > 1000000
ORDER BY DauerMS DESC;
-- Variante 2: ring_buffer Target auslesen (für In-Memory-Sessions)
-- Nützlich, wenn keine persistente Datei verwendet wird
;WITH RingData AS (
SELECT CAST(t.target_data AS XML) AS TargetXml
FROM sys.dm_xe_sessions s
JOIN sys.dm_xe_session_targets t ON s.address = t.event_session_address
WHERE s.name = 'QuerySpills' -- Session-Name anpassen
AND t.target_name = 'ring_buffer'
)
SELECT
n.value('(@timestamp)[1]', 'DATETIME2') AS Zeitstempel,
n.value('(action[@name="sql_text"]/value)[1]', 'NVARCHAR(MAX)') AS SqlText,
n.value('(action[@name="database_name"]/value)[1]', 'NVARCHAR(256)') AS Datenbank
FROM RingData
CROSS APPLY TargetXml.nodes('RingBufferTarget/event') AS x(n)
ORDER BY Zeitstempel DESC;
XE vs. Query Store: Was wann nutzen?
Eine Frage kommt regelmäßig auf: Brauche ich noch XE-Sessions für langsame Abfragen, wenn der Query Store aktiviert ist? Die Antwort ist: beide ergänzen sich, keiner ersetzt den anderen. Das Verhältnis lässt sich so beschreiben — XE ist das Frühwarnsystem, Query Store ist das Gedächtnis des Servers.
XE feuert in Echtzeit. Wenn gerade eine Abfrage über 10 Sekunden läuft, weiß XE das sofort — mit vollständigem SQL-Text, Client-Hostname, und der exakten Dauer. XE sieht auch Dinge die der Query Store nicht sieht: Deadlocks, Login-Fehler, Spills, Autogrowth-Events. Kurz: alles was auf Server-Ebene passiert und nicht direkt mit einem Ausführungsplan zusammenhängt.
Der Query Store hingegen ist das Langzeit-Gedächtnis für Plan-Performance. Er speichert historische Laufzeiten, erkennt Plan-Regressionen (gestern lief die Abfrage 200 ms, heute plötzlich 8 Sekunden — warum?), und erlaubt das Erzwingen eines bestimmten Plans (Forced Plans). Für diese Aufgaben ist XE ungeeignet — kein Mensch shreddet stundenlang XML um historische Trends zu erkennen, wenn der Query Store das out-of-the-box liefert. Kapitel 19 widmet sich dem Query Store vollständig.
|
Aufgabe |
XE |
Query Store |
|---|---|---|
|
Aktuelle langsame Abfragen (live) |
Ja — sofort, mit vollem Kontext |
Ja — aber mit Verzögerung |
|
Historische Performance-Trends |
Möglich, aber aufwändig |
Ja — direkt und komfortabel |
|
Plan-Regressionen erkennen |
Nein |
Ja — Kernaufgabe |
|
Forced Plans verwalten |
Nein |
Ja — Kernaufgabe |
|
Deadlocks aufzeichnen |
Ja — vollständiger Graph |
Nein |
|
Spills erkennen |
Ja — mit spezieller Session |
Indirekt (memory_grant_info) |
|
Login-Fehler / Security-Events |
Ja |
Nein |
|
Autogrowth / IO-Events |
Ja |
Nein |
|
Recompile-Ursachen |
Ja — mit recompile_cause |
Nein |
Tab. 8.4: XE vs. Query Store — Stärken und Grenzen
|
Tipp: Die ideale Kombination |
|---|
|
XE und Query Store sind kein Entweder-oder. Empfehlung für alle Produktionssysteme: Query Store aktivieren (Kapitel 19 erklärt die optimalen Einstellungen) und gleichzeitig die drei Pflicht-Sessions SlowQueries, Deadlocks und system_health laufen lassen. Kostet zusammen unter 1% CPU-Overhead und liefert lückenloses Diagnose-Material für praktisch jedes Performance-Problem. |
8.8 system_health: Die gratis Baseline-Session
SQL Server liefert eine XE-Session kostenlos und immer aktiv: die system_health-Session. Sie läuft seit SQL Server 2008 R2 im Hintergrund und sammelt Informationen, die für Post-Mortem-Analysen unersetzlich sind. Wer ein Problem hat das gestern Nacht aufgetreten ist, schaut zuerst in system_health.
Was system_health aufzeichnet:
-- Deadlocks aus der system_health Session lesen
-- Kein Setup nötig — diese Daten sind immer da
;WITH SystemHealth AS (
SELECT CAST(t.target_data AS XML) AS TargetXml
FROM sys.dm_xe_session_targets t
JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
WHERE s.name = 'system_health'
AND t.target_name = 'ring_buffer'
)
SELECT
n.value('(@timestamp)[1]', 'DATETIME2') AS Zeitstempel,
n.query('.') AS DeadlockXml -- vollständiger Deadlock-Graph
FROM SystemHealth
CROSS APPLY TargetXml.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS x(n)
ORDER BY Zeitstempel DESC;
-- Schwere Fehler aus system_health (Severity >= 20)
;WITH SystemHealth AS (
SELECT CAST(t.target_data AS XML) AS TargetXml
FROM sys.dm_xe_session_targets t
JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
WHERE s.name = 'system_health'
AND t.target_name = 'ring_buffer'
)
SELECT
n.value('(@timestamp)[1]', 'DATETIME2') AS Zeitstempel,
n.value('(data[@name="error_number"]/value)[1]', 'INT') AS Fehlernummer,
n.value('(data[@name="severity"]/value)[1]', 'INT') AS Schweregrad,
n.value('(data[@name="message"]/value)[1]', 'NVARCHAR(MAX)') AS Meldung
FROM SystemHealth
CROSS APPLY TargetXml.nodes('RingBufferTarget/event[@name="error_reported"]') AS x(n)
WHERE n.value('(data[@name="severity"]/value)[1]', 'INT') >= 17
ORDER BY Zeitstempel DESC;
|
Tipp: system_health .xel Dateien für längere Geschichte |
|---|
|
Der ring_buffer der system_health Session ist begrenzt und verliert ältere Daten. Für längere Geschichte: SQL Server schreibt seit 2012 auch in .xel-Dateien (Pfad: Standard-Logverzeichnis, Dateiname system_health*.xel). Diese Dateien lassen sich wie jede andere XE-Session mit sys.fn_xe_file_target_read_file auslesen und gehen typischerweise 4–8 Stunden zurück. |
Sessions verwalten: Start, Stop, automatisches Cleanup
Mit der Zeit sammeln sich XE-Sessions an. Vielleicht hat jemand für eine Diagnose eine temporäre Session erstellt und nie wieder gelöscht. Vielleicht laufen Sessions die niemand mehr kennt. Hier ist der Überblick wie man Sessions sauber verwaltet.
-- Übersicht aller definierten Sessions — laufende und gestoppte
-- is_running = 1: Session ist aktiv und sammelt Events
-- is_running = 0: Session ist definiert aber gestoppt
SELECT name, is_running, startup_state,
create_time, total_buffer_size / 1024 AS PufferKB
FROM sys.server_event_sessions ses
LEFT JOIN sys.dm_xe_sessions xes ON ses.name = xes.name
ORDER BY is_running DESC, name;
-- Session stoppen (Events werden nicht mehr gesammelt, Definition bleibt erhalten)
-- Nützlich für temporäre Diagnose-Sessions
ALTER EVENT SESSION [Recompiles] ON SERVER STATE = STOP;
-- Session permanent löschen (Definition wird entfernt)
-- Vorsicht: noch nicht gesicherte Daten im ring_buffer sind verloren
DROP EVENT SESSION [Recompiles] ON SERVER;
-- Session ändern ohne neu erstellen: max_rollover_files erhöhen
-- ALTER EVENT SESSION funktioniert auch auf laufenden Sessions
ALTER EVENT SESSION [SlowQueries] ON SERVER
ALTER TARGET package0.event_file -- bestehendes Target ändern
WITH (SET max_rollover_files = 20); -- von 10 auf 20 erhöhen
-- STARTUP_STATE nachträglich setzen: Session soll nach Neustart automatisch starten
ALTER EVENT SESSION [Blocking] ON SERVER
WITH (STARTUP_STATE = ON);
-- Wichtig: STARTUP_STATE = ON bedeutet, die Session startet beim nächsten SQL Server Neustart.
-- Sessions die mit STATE = START manuell gestartet wurden, starten nach Neustart NICHT
-- automatisch neu — es sei denn STARTUP_STATE = ON ist gesetzt.
STARTUP_STATE = ON ist für alle Produktions-Sessions Pflicht. Eine Session die nach einem Neustart nicht mehr läuft, hat genau dann keine Daten, wenn man sie am dringendsten braucht — nämlich, wenn das System nach einem Neustart wieder Probleme macht. Das ist nicht hypothetisch: in der Praxis ist "die XE-Session war nach dem Failover nicht mehr aktiv" eine der häufigsten Ursachen dafür, dass Post-Mortem-Analysen ins Leere laufen.
8.9 Diagnose: Extended Events für die Praxis
Symptome
|
Hinweis: Woran merkst du, dass XE dir fehlt? |
|---|
|
• Ein Performance-Problem ist aufgetreten, aber du weißt nicht welche Abfragen betroffen waren. |
|
• Deadlocks passieren, aber du kannst den Graphen nicht rekonstruieren, weil kein Trace lief. |
|
• Nutzer berichten "es war gestern Nacht langsam" — und du hast nichts aufgezeichnet. |
|
• Du nutzt noch Profiler und merkst hohe CPU-Last während des Tracings. |
|
• Memory Spills in sys.dm_exec_query_stats sichtbar, aber nicht wann und bei welcher Abfrage. |
So misst du das
|
Tipp: Minimale Grundausstattung an XE-Sessions |
|---|
|
Folgende drei Sessions sollten auf jedem Produktions-SQL-Server immer laufen: |
|
1. SlowQueries — sql_statement_completed + rpc_completed über 1 Sekunde, event_file, STARTUP_STATE=ON. |
|
2. Deadlocks — xml_deadlock_report, event_file, STARTUP_STATE=ON. |
|
3. system_health — läuft automatisch, nichts zu tun. |
|
Diese drei Sessions zusammen erzeugen unter 0,5% CPU-Overhead auf einem typischen OLTP-Server. |
-- Welche XE-Sessions laufen gerade? Sind sie aktiv?
SELECT name, create_time, total_buffer_size / 1024 AS PufferKB, total_buffer_lost_buffers,
dropped_event_count -- > 0 bedeutet: Events gehen verloren (Puffer zu klein)
FROM sys.dm_xe_sessions
ORDER BY name;
-- Wurden Events verworfen? (Hinweis auf Überlast oder zu kleinen Puffer)
-- dropped_event_count > 0 → MAX_MEMORY erhöhen oder Predicate verschärfen
SELECT s.name, t.target_name, t.total_bytes_written, t.total_files_written
FROM sys.dm_xe_sessions s
JOIN sys.dm_xe_session_targets t ON s.address = t.event_session_address
WHERE s.name NOT IN ('system_health', 'AlwaysOn_health');
Typische Fehlinterpretationen
|
Warnung: Was Einsteiger häufig falsch machen |
|---|
|
• "XE ist zu kompliziert — ich nutze Profiler." Der Overhead von Profiler auf einem belasteten System kann das Problem verschlimmern. XE ist lernbar, Profiler ist gefährlich. |
|
• ring_buffer für Produktion nutzen, ohne zu merken, dass Daten verloren gehen. Bei einem Neustart ist alles weg. Event_file ist für Produktion Pflicht. |
|
• Alle möglichen Actions sammeln ohne Predicates setzen. Das erzeugt massiven Overhead für Daten die man nie braucht. Weniger ist mehr: nur was gebraucht wird, nur, wenn Predicate passt. |
|
• sql_text als Action vergessen und dann Events ohne SQL-Text in der Datei haben. sql_text muss explizit als Action angegeben werden — es ist nicht Teil des Standard-Payloads. |
Erste Gegenmaßnahmen
|
Tipp: Wenn gerade ein Problem brennt |
|---|
|
1. Zuerst system_health prüfen — vielleicht ist das Problem schon aufgezeichnet. |
|
2. SlowQueries-Session starten falls noch nicht aktiv. |
|
3. Bei Deadlock-Verdacht: xml_deadlock_report-Session starten, Problem reproduzieren. |
|
4. Bei Spill-Verdacht: QuerySpills-Session starten, belastende Abfrage ausführen, ring_buffer auslesen. |
|
5. Kein Profiler. Wirklich nicht. Auch nicht kurz. |
Zusammenfassung
Extended Events ist das Werkzeug, das SQL Server in eine Black Box verwandelt die plötzlich alles preisgibt. Wer XE nicht kennt, tappt im Dunkeln. Wer es kennt, sieht was der Server wirklich tut — mit unter 2% Overhead und ohne Profiler-Risiko.
Die wichtigsten Erkenntnisse:
Ausblick auf Kapitel 9: Mit Extended Events haben wir das Aufzeichnungswerkzeug. Jetzt brauchen wir das Diagnose-Konzept: Wait Statistics. Kapitel 9 zeigt, wie SQL Server mit Wait Types transparent macht, womit er seine Zeit verbringt — und wie man daraus eine strukturierte Diagnose ableitet statt blind nach Ursachen zu suchen. Wait Statistics ist der Einstieg in die eigentliche Performance-Analyse und der rote Faden durch den gesamten Teil III.
Kapitel 9
