Wissen

Praxis-Artikel und Buchkapitel zu SQL-Performance, Sicherheit und Hochverfügbarkeit – alle frei verfügbar.

Beratung

Festpreis-Analyse mit Bericht und Handlungsempfehlung – oder strategische Begleitung bei Architektur, Migration und Hochverfügbarkeit.

Fachbücher

Die fünfbändige Reihe „Ulis SQL-Bibliothek“ – Band 1 verfügbar. Leseprobe herunterladen!

Tools

UB.SimSQL: SQL-Server-Lastsimulator mit regelbasierten Konfigurationsempfehlungen. Lokal, ohne Cloud, ohne Abo.

Schulungen

Online-Workshops zu Performance, Sicherheit und Entwicklung – kompakt, hands-on, ohne MOC-Folienschlacht.

Extended Events: – SQL Server Performance

von

Dieser Artikel ist ein Kapitel aus:
SQL Server Performance & Troubleshooting
Praxisleitfaden, ca. 600 Seiten

[ Hier bei Amazon bestellen ]
[ Mehr zum Buch ]

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:

  • Alle SQL Server Fehler mit Schweregrad ≥ 20 (fatale Fehler)
  • Alle Deadlock-Graphen (seit SQL Server 2012)
  • Scheduler-Hänger (> 15 Sekunden ohne Yield — CPU-Sättigungsindikator)
  • Unerwartete Speicherzustände (Out-of-Memory-Situationen)
  • Verbindungseinschränkungen (max connections überschritten)
  • Security-Fehler (Login-Fehler, Berechtigungsprobleme)
  • -- 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:

  • SQL Trace / Profiler verursacht bis zu 30% CPU-Overhead — XE unter 2%. Das ist kein marginaler Unterschied.
  • Vier Bausteine: Events (Auslöser), Predicates (Filter im Server), Actions (zusätzliche Datenpunkte), Targets (Ausgabe).
  • Predicates sind der Schlüssel zu produktionssicherem Tracing: was den Filter nicht passiert, kostet nichts.
  • event_file für Produktion, ring_buffer für kurze Live-Diagnosen — nie umgekehrt.
  • Sieben Session-Vorlagen decken die häufigsten Diagnose-Szenarien ab: SlowQueries, Deadlocks, Spills, Recompiles, FailedLogins, Autogrowth, Blocking.
  • STARTUP_STATE = ON für alle Produktions-Sessions — sonst läuft nach dem nächsten Neustart nichts mehr.
  • system_health ist der erste Blick bei jeder Post-Mortem-Analyse — Deadlocks, Fehler, Scheduler-Hänger.
  • XE und Query Store ergänzen sich: XE ist das Frühwarnsystem für Live-Events, Query Store das Gedächtnis für Plan-Performance und historische Trends (→ Kap. 19).
  • XE-Daten per T-SQL auswerten ermöglicht automatisierte Analysen und Integration in eigene Monitoring-Lösungen.
  •  

    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