Wait Statistics:
Was SQL Server wirklich tut, wenn er nichts tut
Das Schweigen des Servers — und was es uns verrät
Es gibt einen Moment in jedem Performance-Gespräch, der immer wieder kommt: "Der Server ist langsam." Auf die Frage, was genau langsam ist, folgt meistens ein Schulterziehen. Auf die Frage, worauf SQL Server gerade wartet, folgt meistens Schweigen. Dabei ist genau das der Schlüssel — und SQL Server kennt die Antwort. Er schreibt sie in Echtzeit auf.
Wait Statistics sind das Diagnose-Werkzeug, das du immer zuerst konsultierst. Nicht, weil alles andere unwichtig ist, sondern, weil Wait Statistics dir sagen, in welche Richtung du schauen sollst. Ohne dieses erste Orientierungssignal läufst du blind durch ein dunkles Rechenzentrum und hoffst, dass du zufällig über den richtigen Engpass stolperst. Das ist keine Methode. Das ist Kaffeesatzleserei mit ernstem Gesicht.
Das Grundprinzip ist simpel: Jeder Worker-Thread in SQL Server wartet irgendwann auf irgendetwas. Auf eine Seite, die von der Disk gelesen werden muss. Auf einen Lock, den ein anderer Thread hält. Auf einen freien CPU-Scheduler. Oder auf einen Client, der endlich seine Daten verarbeitet. SQL Server zählt diese Wartezeiten mit — für jeden Wait Type separat, kumulativ seit dem letzten Neustart. Das Ergebnis ist eine Landkarte des Leidens. Oder, wenn alles gut läuft, eine Landkarte der relativen Harmlosigkeit.
SQL Server kennt mehrere hundert Wait Types. Die allermeisten davon wirst du in der Praxis nie zu Gesicht bekommen — sie sind interne System-Wartetypen, die mit echter Performance-Diagnose nichts zu tun haben. Etwa 30 bis 40 Wait Types sind es, die in der Praxis den Unterschied machen. Und davon dominiert meistens einer oder zwei das Ranking. Das ist der Engpass. Den löst du.
|
Definition: Wait Type |
|---|
|
Ein Wait Type ist eine kategorisierte Beschreibung dafür, worauf ein Worker-Thread wartet. SQL Server kennt über 900 Wait Types — von PAGEIOLATCH_SH (warte auf physischen IO) bis SLEEP_TASK (ich schlafe bewusst, ignorier mich). Die Kunst besteht darin, die relevanten von den harmlosen zu unterscheiden. Und die dringenden von den chronischen. |
In diesem Kapitel lernst du, Wait Statistics zu lesen, zu interpretieren und — ganz wichtig — nicht falsch zu interpretieren. Denn ein hoher Wert ist nicht automatisch ein Problem. Und ein niedriger Wert ist nicht automatisch eine Entwarnung. Context is king, Baseline is queen. Beide braucht man.
Das konzeptuelle Fundament für alles, was in diesem Kapitel folgt, liegt in Kapitel 4: Dort haben wir den SQLOS-Scheduler, den Buffer Pool und die grundlegende Architektur von SQL Server behandelt. Wer noch nicht dort war, sollte das nachholen — der Wait-Mechanismus macht nur Sinn, wenn man versteht, wie SQL Server intern Threads und Ressourcen verwaltet. Und für die praktische, automatisierte Erhebung von Baseline-Daten über Wochen empfehle ich schon jetzt den Blick auf Kapitel 31, wo das PowerShell-Skript Collect-SqlPerf.ps1 vorgestellt wird. Das Tool nimmt uns einen Großteil der manuellen Snapshot-Arbeit ab.
NUMA-Topologie spielt bei der CPU-Diagnose via Wait Statistics ebenfalls eine wichtige Rolle — wer auf einem NUMA-System mehrere CPU-Knoten hat, wird unterschiedliche Scheduler-Lasten sehen. Das haben wir in Kapitel 1 (Hardware-Grundlagen) und Kapitel 3 (Virtualisierung) grundlegend behandelt, und in Kapitel 11 (Memory Management) kommen wir auf die Wechselwirkung zwischen NUMA und dem Buffer Pool zurück.
Signal Wait vs. Resource Wait: Der fundamentale Unterschied
Bevor wir in einzelne Wait Types eintauchen, müssen wir eine Unterscheidung verstehen, die alles andere strukturiert. SQL Server teilt Wartezeiten in zwei Kategorien auf — und diese Unterscheidung sagt mehr über die Problemursache als der Wait Type alleine.

Abb. 9.1: Signal Wait vs. Resource Wait — der fundamentale Unterschied
Resource Wait: Der externe Engpass
Ein Resource Wait entsteht, wenn ein Thread auf eine externe Ressource wartet, die noch nicht verfügbar ist. Die Ressource ist noch nicht da — der Thread kann nicht weiterarbeiten, egal wie viel CPU frei wäre. Typische Beispiele:
Resource Waits sind "ehrliche" Waits: Hier gibt es tatsächlich eine externe Ressource, die den Fortschritt blockiert. Das Problem liegt nicht bei der CPU, sondern beim IO, beim Netzwerk, beim Locking oder beim Speicher. Die Lösung liegt entsprechend auch dort — nicht beim CPU-Upgrade.
Signal Wait: Die unsichtbare CPU-Sättigung
Ein Signal Wait ist subtiler — und oft gefährlicher, weil er im Task Manager unsichtbar bleibt. Er entsteht, wenn ein Thread seine Arbeit eigentlich erledigt hat, die benötigte Ressource längst verfügbar ist, aber kein Scheduler-Slot frei ist. Der Thread ist runnable — bereit zum Laufen — aber kein CPU-Kern hat gerade Zeit für ihn.
Das passiert genau dann, wenn die CPU-Last so hoch ist, dass die SQLOS-Scheduler durchgehend besetzt sind. Der Thread landet in der Runnable Queue und wartet dort. Diese Wartezeit erscheint in der signal_wait_time_ms — und genau das ist der CPU-Druck-Indikator. Nicht die CPU-Auslastungsanzeige im Task Manager. Die zeigt nur tatsächlich auf der CPU verbrachte Zeit, nicht die Zeit in der Runnable Queue.
Was das in der Praxis bedeutet: Du kannst 75% CPU-Last im Performance Monitor sehen und trotzdem massiven CPU-Druck haben — weil die restlichen 25% "Leerlauf" eigentlich Threads sind, die in der Runnable Queue stecken und auf ihren Scheduler-Slot warten. Wait Statistics zeigen, was wirklich los ist.
|
Warnung: Signal Wait > 20% = CPU unter Druck |
|---|
|
Wenn der Anteil der Signal Waits an der Gesamt-Wartezeit über 20% steigt, ist das ein klares Zeichen für CPU-Sättigung. Nicht "CPU ist ein bisschen beschäftigt" — sondern "der Scheduler hat mehr Arbeit als er abarbeiten kann". Das ist der Moment, ab dem SQL Server anfängt, messbar langsamer zu werden, auch, wenn Top-Level-Metriken noch harmlos aussehen. Typische Richtwerte als Orientierung: unter 5% entspannt, 5–10% normaler Betrieb, 10–20% erhöhte Last (beobachten), 20–30% problematisch, über 30% Notfall. |
Wie man Signal Wait vs. Resource Wait berechnet
In sys.dm_os_wait_stats gibt es zwei Felder pro Wait Type: wait_time_ms und signal_wait_time_ms. Die Differenz ist die reine Resource-Wait-Zeit. Das Verhältnis verrät die CPU-Situation:
-- Signal Wait Anteil berechnen — der CPU-Gesundheitscheck
-- signal_wait_time_ms ist die Zeit, die im Runnable-State verbracht wurde
-- Wert > 20% → CPU unter Druck, Scheduler überlastet
SELECT
SUM(signal_wait_time_ms) AS SignalWait_ms,
SUM(wait_time_ms - signal_wait_time_ms) AS ResourceWait_ms,
SUM(wait_time_ms) AS GesamtWait_ms,
CAST(100.0 * SUM(signal_wait_time_ms)
/ NULLIF(SUM(wait_time_ms), 0) AS DECIMAL(5,2)) AS SignalWaitAnteil_Pct
FROM sys.dm_os_wait_stats
-- Idle-Waits ausfiltern, die das Bild verzerren würden
WHERE wait_type NOT IN (
'SLEEP_TASK', 'WAITFOR', 'BROKER_TO_FLUSH', 'BROKER_TASK_STOP',
'CLR_AUTO_EVENT', 'DISPATCHER_QUEUE_SEMAPHORE', 'FT_IFTS_SCHEDULER_IDLE_WAIT',
'HADR_WORK_QUEUE', 'ONDEMAND_TASK_QUEUE', 'REQUEST_FOR_DEADLOCK_SEARCH',
'RESOURCE_QUEUE', 'SERVER_IDLE_CHECK', 'SLEEP_DBSTARTUP', 'SLEEP_DBREPLICATION',
'SLEEP_MASTERDBREADY', 'SLEEP_MASTERMDREADY', 'SLEEP_MASTERUPGRADED',
'SLEEP_MSDBSTARTUP', 'SLEEP_TEMPDBSTARTUP', 'SNI_HTTP_ACCEPT',
'SP_SERVER_DIAGNOSTICS_SLEEP', 'SQLTRACE_BUFFER_FLUSH', 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG',
'XE_DISPATCHER_WAIT', 'XE_TIMER_EVENT'
);
Das Ergebnis dieser Query ist eine Sofortdiagnose in einer Zahl. Ein Signal-Wait-Anteil von 4% sagt: CPU entspannt, schau woanders. Ein Wert von 34% — wie wir ihn bei Sparfuchs & Partner (Kapitel 33) gesehen haben — sagt: CPU ist der primäre Engpass, und zwar nachhaltig.
sys.dm_os_wait_stats: Die Schaltzentrale der Diagnose
Die View sys.dm_os_wait_stats ist die zentrale DMV für die Wait-Analyse. Sie enthält kumulierte Wartezeiten seit dem letzten SQL Server-Start — alle Wait-Ereignisse seit dem Neustart summiert. Das macht sie zur Basis für Delta-Berechnungen: Wir wollen nie den absoluten Wert seit Serverstart, sondern die Veränderung in einem definierten Zeitfenster. Ein Server der seit 6 Monaten läuft, hat riesige absolute Werte — die meisten davon aus Zeiten die längst vergangen sind.
Folgende Query ist der Startpunkt für jede Wait-Analyse. Sie filtert Idle-Waits (die keinen echten Engpass darstellen), berechnet Prozentwerte und sortiert nach der höchsten Wartezeit:
-- Top-20 Wait Types, bereinigt um harmlose Idle-Waits
-- Grundlage für jede Performance-Analyse — diese Query täglich kennen!
WITH FilteredWaits AS (
SELECT
wait_type,
wait_time_ms,
signal_wait_time_ms,
waiting_tasks_count,
-- Durchschnittliche Wartezeit pro Ereignis
CASE WHEN waiting_tasks_count > 0
THEN wait_time_ms / waiting_tasks_count
ELSE 0 END AS avg_wait_ms
FROM sys.dm_os_wait_stats
-- Diese Wait Types sind systeminterne Idle-Wartezeiten
-- Sie sehen nach Problemen aus, sind aber komplett normal
WHERE wait_type NOT IN (
'SLEEP_TASK', 'WAITFOR', 'BROKER_TO_FLUSH', 'BROKER_TASK_STOP',
'CLR_AUTO_EVENT', 'DISPATCHER_QUEUE_SEMAPHORE', 'FT_IFTS_SCHEDULER_IDLE_WAIT',
'HADR_WORK_QUEUE', 'ONDEMAND_TASK_QUEUE', 'REQUEST_FOR_DEADLOCK_SEARCH',
'RESOURCE_QUEUE', 'SERVER_IDLE_CHECK', 'SLEEP_DBSTARTUP',
'SLEEP_DBREPLICATION', 'SLEEP_MASTERDBREADY', 'SLEEP_MASTERMDREADY',
'SLEEP_MASTERUPGRADED', 'SLEEP_MSDBSTARTUP', 'SLEEP_TEMPDBSTARTUP',
'SNI_HTTP_ACCEPT', 'SP_SERVER_DIAGNOSTICS_SLEEP', 'SQLTRACE_BUFFER_FLUSH',
'WAIT_XTP_OFFLINE_CKPT_NEW_LOG', 'XE_DISPATCHER_WAIT', 'XE_TIMER_EVENT'
)
AND wait_time_ms > 0
),
Summe AS (
SELECT SUM(wait_time_ms) AS total_ms FROM FilteredWaits
)
SELECT TOP 20
fw.wait_type,
fw.waiting_tasks_count,
fw.wait_time_ms,
fw.signal_wait_time_ms,
fw.wait_time_ms - fw.signal_wait_time_ms AS resource_wait_ms,
fw.avg_wait_ms,
-- Prozentualer Anteil am Gesamtwait — das ist die wichtigste Spalte
CAST(100.0 * fw.wait_time_ms / s.total_ms AS DECIMAL(5,2)) AS pct_total
FROM FilteredWaits fw
CROSS JOIN Summe s
ORDER BY fw.wait_time_ms DESC;

Abb. 9.2: Beispiel Top-10 Wait Types — MWSQL01 während der Stoßzeit
Delta-Technik: Was in diesem Zeitfenster passiert ist
Das Ergebnis der obigen Query ist kein Absolutwert-Ranking seit Serverstart, sondern ein Snapshot aller kumulierten Werte. Für echte Diagnose brauchst du Deltas: Nimm einen Snapshot, warte 15–30 Minuten (idealerweise während der Stoßzeit), nimm einen zweiten Snapshot, bilde die Differenz. Was in diesem Zeitfenster gewachsen ist — das ist das, worauf dein Server gerade am meisten wartet.
-- Snapshot-Technik: Zwei Messungen, dann Differenz berechnen
-- Schritt 1: Snapshot in eine temporäre Tabelle schreiben
IF OBJECT_ID('tempdb..#WaitSnapshot1') IS NOT NULL DROP TABLE #WaitSnapshot1;
SELECT
wait_type,
wait_time_ms,
signal_wait_time_ms,
waiting_tasks_count,
GETDATE() AS snapshot_time
INTO #WaitSnapshot1
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
'SLEEP_TASK', 'WAITFOR', 'CLR_AUTO_EVENT', 'DISPATCHER_QUEUE_SEMAPHORE',
'HADR_WORK_QUEUE', 'ONDEMAND_TASK_QUEUE', 'REQUEST_FOR_DEADLOCK_SEARCH',
'RESOURCE_QUEUE', 'SERVER_IDLE_CHECK', 'XE_DISPATCHER_WAIT', 'XE_TIMER_EVENT'
);
-- Schritt 2: Warte 15–30 Minuten — idealerweise während der Problemzeit
-- In Produktivumgebungen via SQL Agent Job steuern, nicht manuell sitzen
-- Schritt 3: Delta berechnen — das ist was in diesem Zeitfenster passiert ist
SELECT TOP 20
w2.wait_type,
w2.wait_time_ms - ISNULL(w1.wait_time_ms, 0) AS delta_wait_ms,
w2.signal_wait_time_ms - ISNULL(w1.signal_wait_time_ms, 0) AS delta_signal_ms,
w2.waiting_tasks_count - ISNULL(w1.waiting_tasks_count, 0) AS delta_count,
-- Durchschnittliche Wartezeit pro Ereignis im Messfenster
CASE WHEN (w2.waiting_tasks_count - ISNULL(w1.waiting_tasks_count, 0)) > 0
THEN (w2.wait_time_ms - ISNULL(w1.wait_time_ms, 0))
/ (w2.waiting_tasks_count - ISNULL(w1.waiting_tasks_count, 0))
ELSE 0 END AS avg_delta_wait_ms
FROM sys.dm_os_wait_stats w2
LEFT JOIN #WaitSnapshot1 w1 ON w1.wait_type = w2.wait_type
WHERE (w2.wait_time_ms - ISNULL(w1.wait_time_ms, 0)) > 0
ORDER BY delta_wait_ms DESC;
sys.dm_os_waiting_tasks: Die Echtzeit-Perspektive
Während sys.dm_os_wait_stats die historische Aggregation zeigt, liefert sys.dm_os_waiting_tasks einen Live-Blick: Welche Sessions warten gerade jetzt auf welche Ressource? Das ist besonders nützlich, wenn ein Benutzer anruft und sagt "es hängt gerade" — dann nicht erst Snapshots bauen, sondern direkt nachschauen:
-- Aktuelle Wartezeiten in Echtzeit — wer wartet gerade auf was?
-- Diese Query ausführen, wenn ein Problem akut ist, nicht danach
SELECT
wt.session_id,
wt.wait_type,
wt.wait_duration_ms, -- Wie lange wartet dieser Thread schon?
wt.blocking_session_id, -- NULL, wenn kein Blocking, sonst: der Schuldige
wt.resource_description, -- Welche Ressource konkret? (Seite, Lock-Object...)
s.status,
s.login_name,
s.program_name,
-- Letzter ausgeführter SQL-Text — Achtung: kann NULL sein
SUBSTRING(t.text, 1, 300) AS query_text
FROM sys.dm_os_waiting_tasks wt
JOIN sys.dm_exec_sessions s
ON wt.session_id = s.session_id
-- Optionaler Join auf SQL-Text — kann CROSS APPLY Performance kosten
OUTER APPLY sys.dm_exec_sql_text(
(SELECT sql_handle FROM sys.dm_exec_requests r
WHERE r.session_id = wt.session_id)
) t
WHERE wt.session_id > 50 -- System-Sessions ausblenden
ORDER BY wt.wait_duration_ms DESC;
|
Tipp: Delta-Snapshots mit Collect-SqlPerf.ps1 |
|---|
|
Das PowerShell-Skript Collect-SqlPerf.ps1 aus Kapitel 31 nimmt automatisiert Wait-Snapshots in konfigurierbaren Intervallen und speichert die Deltas in einer Tabelle. Damit baust du über Wochen eine Baseline auf, die dir zeigt, was "normal" für deine Workload ist — und was als Anomalie auffällt. Ohne dieses Langzeit-Bild ist jede Momentaufnahme wertlos. |
Die wichtigsten Wait Types im Detail
SQL Server kennt über 900 Wait Types. Die meisten davon wirst du nie sehen — oder wenn, dann so selten, dass sie das Ranking nicht dominieren. Wir konzentrieren uns auf die Wait Types, die in der Praxis den Unterschied machen — und die ausreichend verstanden werden müssen, um die richtige Diagnoserichtung einzuschlagen.
PAGEIOLATCH_SH und PAGEIOLATCH_EX: Der IO-Klassiker
PAGEIOLATCH-Waits entstehen, wenn SQL Server eine Datenbankseite (8 KB) aus dem Buffer Pool lesen will, die Seite aber noch nicht dort ist und von der Disk geladen werden muss. SH (Shared) bedeutet, jemand will die Seite lesen. EX (Exclusive) bedeutet, jemand will sie schreiben. Das ist physischer Disk-IO — und, wenn er langsam ist, siehst du hier die Wartezeit.
PAGEIOLATCH_SH als dominanter Wait Type ist meistens ein klarer Befund: Der Buffer Pool ist zu klein (Working Set passt nicht in den Cache), die Disk ist zu langsam, oder es gibt einen Full Table Scan der unnötigerweise riesige Mengen an Daten liest und den Cache überschwemmt. Konkrete Latenz-Richtwerte für physischen IO:
Die Diagnose führt fast immer nach Kapitel 10 (IO-Performance) oder Kapitel 11 (Memory Management) — je, nachdem ob das Problem das Disk-Subsystem selbst ist oder der Buffer Pool zu klein ist um das Working Set zu halten.
|
Hintergrund: PAGEIOLATCH vs. PAGELATCH — der entscheidende Unterschied |
|---|
|
PAGEIOLATCH (mit IO) = physischer Disk-IO. Die Seite ist nicht im Cache, SQL Server wartet auf die Disk. PAGELATCH (ohne IO) = In-Memory-Zugriff auf eine Seite, die bereits im Buffer Pool ist. Kein Disk-IO. Kann TempDB-Contention sein (PFS/GAM-Seiten) oder andere Hot-Pages. Disk-Upgrade hilft hier nicht — das ist ein vollkommen anderes Problem. Wer PAGELATCH_EX mit einem neuen SSD lösen will, hat das Problem falsch diagnostiziert. Der häufigste Grund für PAGELATCH_EX in TempDB ist zu wenige TempDB-Datendateien — nicht zu langsame Disk. Kapitel 13 behandelt das ausführlich. |
PAGELATCH_EX und PAGELATCH_SH: TempDB-Contention
PAGELATCH ohne das "IO" im Namen bedeutet: Die Seite ist im Buffer Pool, aber mehrere Threads konkurrieren gleichzeitig um Schreibzugriff auf sie. Das häufigste Szenario in der Praxis ist TempDB-Contention auf den PFS- und GAM-Seiten — spezielle Verwaltungsseiten, die SQL Server aktualisiert, wenn temporäre Objekte angelegt oder gelöscht werden.
Bei einem System mit vielen parallelen Sitzungen die alle TempDB nutzen — Sortierungen, Hashjoins, temporäre Tabellen, Tabellenvariablen — wird die PFS-Seite zum Flaschenhals. Die Lösung ist nicht mehr RAM, nicht schnellere Disk. Die Lösung ist mehr TempDB-Datendateien. Wie viele? Das schauen wir uns in Kapitel 13 (TempDB) genau an.
LCK_M_*: Die Locking-Familie
LCK_M_S, LCK_M_X, LCK_M_U und ihre Varianten (IS, IX, SIX…) sind Lock-Wait-Types. Ein Thread wartet auf einen Lock, den ein anderer Thread hält. Das ist Blocking — und es hat nichts mit IO oder CPU zu tun. Die detaillierte Diagnose folgt in Kapitel 14 (Blocking und Deadlocks). Hier nur die wichtigsten drei:
Wenn LCK_M_*-Wait-Types prominent sind, ist die erste Anlaufstelle sys.dm_exec_requests mit dem Feld blocking_session_id. Der Wert dort zeigt die Session, die den Lock hält. Was macht sie? Warum hält sie den Lock so lange? Meistens ist die Antwort: offene Transaktion, die sich nicht beeilt.
-- Blocking-Kette sichtbar machen — wer blockiert wen?
-- blocking_session_id = NULL heißt: dieser Thread ist selbst nicht geblockt
SELECT
r.session_id,
r.status, -- 'suspended' = wartet auf etwas
r.wait_type,
r.wait_time, -- ms bereits gewartet
r.blocking_session_id, -- 0 oder NULL = nicht geblockt
r.command,
DB_NAME(r.database_id) AS Datenbank,
SUBSTRING(t.text, 1, 200) AS query_text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
-- Nur Sessions die gerade geblockt sind
WHERE r.blocking_session_id IS NOT NULL
AND r.blocking_session_id > 0
ORDER BY r.wait_time DESC;
CXPACKET und CXCONSUMER: Parallelismus — aber welcher?
CXPACKET ist der Wait Type, der am häufigsten falsch interpretiert wird. Er entsteht bei parallelen Abfrageausführungen — Threads eines Parallel-Plans warten aufeinander. Das sieht dramatisch aus. Meistens ist es harmlos.
Seit SQL Server 2016 SP1 gibt es CXCONSUMER als separaten Wait Type. CXCONSUMER ist fast immer harmlos — er entsteht beim Konsumieren von Daten aus dem parallelen Dataflow. Consumer warten darauf, dass Producer ihnen Daten liefern, und das ist normales Parallelismus-Verhalten. CXPACKET dagegen kann auf echten Parallelismus-Skew hindeuten: Ein Thread hat deutlich mehr Arbeit als die anderen und alle müssen warten bis er fertig ist.
Parallelismus-Skew entsteht klassischerweise durch schlechte Kardinalitätsschätzungen (Kapitel 16) oder ungleiche Datendistribution. SQL Server verteilt Arbeit auf Basis seiner Schätzungen — wenn die Schätzung falsch ist, bekommt ein Thread überproportional viel Arbeit. Das ist dann ein Problem das in Kapitel 15 (Ausführungspläne) gelöst wird.
|
Hinweis: CXPACKET: Erst den Signal-Wait-Anteil prüfen |
|---|
|
CXPACKET alleine sagt wenig. Prüfe zuerst den Signal Wait-Anteil. Wenn der niedrig ist, läuft die CPU gut und der CXPACKET-Wait ist normaler Koordinations-Overhead. Erst, wenn Signal Waits gleichzeitig hoch sind, wird CXPACKET zum CPU-Problem — dann haben die Parallel-Threads tatsächlich keinen freien Scheduler. In diesem Fall hilft MAXDOP-Reduzierung: Weniger parallele Threads bedeuten weniger Scheduler-Bedarf. MAXDOP-Berechnung haben wir in Kapitel 5 (Serverkonfiguration) behandelt. |
SOS_SCHEDULER_YIELD: Der direkteste CPU-Hunger-Indikator
SOS_SCHEDULER_YIELD ist das direkteste Zeichen für CPU-Sättigung. Dieser Wait Type entsteht, wenn ein Worker-Thread seinen Zeitschlitz (Quantum) auf dem Scheduler aufgebraucht hat und freiwillig zurückgibt, um anderen Threads CPU-Zeit zu ermöglichen. Er tritt in die Runnable Queue — und wartet dort.
In einer entspannten Situation ist SOS_SCHEDULER_YIELD selten und die Wartezeiten sind kurz. Wenn dieser Wait Type in den Top-5 erscheint und die Wartezeiten hoch sind, ist die CPU nachhaltig überlastet. Der SQLOS-Scheduler (Kapitel 4) gibt jedem Thread ein Quantum von ca. 4 ms. Wenn ein Thread dieses Quantum aufbraucht, ohne fertig zu werden, muss er nachgeben. Bei einfachen, schnellen Abfragen kommt das selten vor — bei komplexen, CPU-intensiven Berechnungen sehr häufig.
Bei Sparfuchs & Partner (Kapitel 33) fanden wir SOS_SCHEDULER_YIELD auf Platz 2 der Wait-Statistik — direkt nach WRITELOG. 4 vCPUs, 360.000 Funktionsaufrufe pro Query. Die Funktion fn_GetSteuersatz wurde in 23 von 31 Stored Procedures eingesetzt, wurde als skalare UDF implementiert (also nicht parallelisierbar) und erzwang für jeden der 360.000 Aufrufe einen separaten Scheduler-Durchgang. Die CPU hatte buchstäblich keine Chance. Parameter Sniffing und skalare UDFs als CPU-Kostenfaktor kommen in Kapitel 18 und Kapitel 26 noch einmal zur Sprache.
THREADPOOL: Der Notfallknopf
THREADPOOL ist der Wait Type, bei dem alle anderen Probleme in den Hintergrund treten. Er entsteht, wenn keine freien Worker Threads mehr verfügbar sind. SQL Server versucht, eine neue Anfrage auszuführen, aber alle Worker sind beschäftigt. Die Anfrage wartet, bis ein Thread frei wird. Im schlimmsten Fall wird sie gar nicht mehr angenommen.
Das SQL Server Error Log zeigt in diesem Zustand Fehler 17189: "SQL Server konnte keinen neuen Thread anlegen." Neue Verbindungsversuche schlagen fehl. Das System ist de facto nicht mehr erreichbar. Ursache ist fast immer eine Blocking-Kaskade: Wenige Blocker halten viele Threads fest, die Threads stauen sich, der Pool läuft voll.
|
Warnung: THREADPOOL = sofortige Eskalation |
|---|
|
Wenn THREADPOOL in den Wait Statistics erscheint und wächst, ist das keine "beobachte ich mal"-Situation. Neue Anfragen werden möglicherweise nicht mehr angenommen. Erste Maßnahme: max worker threads temporär erhöhen (sp_configure max worker threads). Das ist ein Pflaster, keine Heilung. Danach sofort Blocking-Analyse: Wer hält die langen Locks? Was macht die Blocker-Session? Meistens ist die Antwort eine lange offene Transaktion oder ein fehlgeschlagener Client der seinen Commit nie gesendet hat. |
WRITELOG: Das Transaktionslog kämpft
WRITELOG entsteht, wenn SQL Server darauf wartet, dass Transaktions-Log-Einträge auf die Disk geschrieben werden. Transaktionen können erst als committed gelten, wenn der Log-Flush abgeschlossen ist (Write-Ahead Logging). Ein langsames Log-Subsystem erzeugt direkt WRITELOG-Waits — und langsame Transaktionen.
Typische Ursachen für hohe WRITELOG-Wartezeiten:
Der Zusammenhang zwischen WRITELOG und VLFs ist subtil aber real: Ein Transaktionslog mit tausenden VLFs (Virtual Log Files) erzeugt mehr internes Management-Overhead beim Log-Flush. In Kapitel 6 (Datenbankeinstellungen) haben wir die VLF-Thematik ausführlich behandelt — wer das noch nicht gelesen hat, bekommt dort den vollen Kontext für das Sparfuchs-Desaster.
|
Tipp: Commit-Latenz mit Extended Events messen |
|---|
|
Um die tatsächliche Commit-Latenz zu messen, kann man ein Extended Event auf sql_transaction mit opcode=commit setzen. Das zeigt, wie lange ein einzelner COMMIT tatsächlich dauert — inklusive WRITELOG-Zeit. Liegt die durchschnittliche Commit-Latenz über 5 ms, ist das Log-Subsystem der Engpass. Delayed Durability ist ein temporärer Ausweg für weniger kritische Workloads: Sie reduziert WRITELOG-Waits drastisch auf Kosten von Datenverlustrisiko bei Serverabsturz. Nur mit Bedacht einsetzen. |
ASYNC_NETWORK_IO: Der unterschätzte Kandidat
ASYNC_NETWORK_IO ist der Wait Type, bei dem SQL Server auf den Client wartet — nicht umgekehrt. Die Abfrage ist fertig, das Ergebnis steht bereit, aber der Client verarbeitet die Daten nicht schnell genug. SQL Server schickt einen Batch Ergebnisdaten, wartet bis der Client bereit ist für den nächsten Batch, und das dauert.
Wenn ASYNC_NETWORK_IO prominent erscheint: Überprüfe die Result-Set-Größen. Schickt deine Anwendung SELECT * auf große Tabellen? Gibt es N+1-Probleme die tausende Ergebnismengen produzieren? Manchmal ist ASYNC_NETWORK_IO auch ein Symptom für langsame Netzwerkverbindungen zum Applikationsserver oder für einen Applikationsserver der schlicht ausgelastet ist.
Bei Trendforge Digital GmbH (Kapitel 34) war ASYNC_NETWORK_IO einer der Top-Wait-Types — direkt verursacht durch Entity Framework N+1-Probleme. Für jedes Elternobjekt wurde eine separate Abfrage für die Kindobjekte ausgeführt. 500 Bestellungen → 500 separate Abfragen für die Bestellpositionen → 500 Mal ASYNC_NETWORK_IO. Der DBA konnte das Problem ohne Mitarbeit der Entwickler nicht lösen — eine typische Trendforge-Situation. Kapitel 30 (ORM & Applikationsdesign) behandelt N+1 ausführlich.
RESOURCE_SEMAPHORE: Memory Grants im Stau
RESOURCE_SEMAPHORE entsteht, wenn eine Abfrage auf ihren Memory Grant wartet — den Speicher, der für Sortierungen, Hash Joins und andere speicherhungrige Operationen reserviert wird. SQL Server verwaltet diese Grants aus einem begrenzten Pool (standardmäßig 25% des Buffer Pool). Wenn alle Grants vergeben sind, warten neue Abfragen in der Grant-Warteschlange.
Fehlende Indizes sind ein häufiger Auslöser: Ohne Index wählt SQL Server oft einen Hash Join, der mehr Memory benötigt als ein Nested Loop über einen Index. Bei Trendforge hatten fehlende Indizes genau diesen Effekt — Hash Joins überall, hohe Memory-Grant-Anfragen, RESOURCE_SEMAPHORE als Folge. Kapitel 12 (Memory Grants und Spills) behandelt das Thema vollständig.
IO_COMPLETION und DISKIO_SUSPEND: Async IO
IO_COMPLETION und der seltenere DISKIO_SUSPEND treten auf, wenn asynchrones IO noch nicht abgeschlossen ist. SQL Server nutzt für viele IO-Operationen Async-IO: Die Anfrage wird abgeschickt, der Thread macht etwas anderes, und wartet dann auf IO_COMPLETION, wenn er das Ergebnis braucht. Das ist kein Problemsignal per se — sondern normales Async-IO-Verhalten. Auffällig wird es, wenn die Wartezeiten sehr hoch sind oder IO_COMPLETION häufig bei TempDB-Operationen erscheint.
Baseline: Ohne Vergleichswert ist alles Rätselraten
Hier ist die unbequeme Wahrheit: Ein PAGEIOLATCH_SH-Wait von 45.000 ms sagt dir absolut gar nichts. Ob das gut oder schlecht ist, hängt vollständig davon ab, wie lange der Messzeitraum war, wie viele IO-Operationen stattgefunden haben, und wie das System normalerweise aussieht. Ohne Baseline ist Performance-Analyse Rätselraten mit ernstem Gesicht.
Das gilt für jeden einzelnen Messwert in diesem Buch. Ein avg_wait_ms von 8 ms bei PAGEIOLATCH_SH — gut oder schlecht? Auf einem NVMe-System: katastrophal. Auf einem SAN mit Spindeln: möglicherweise akzeptabel. Bei einer Datenbank, die normalerweise 0,3 ms hat und jetzt 8 ms zeigt: Alarmstufe Rot. Zahlen haben keine Bedeutung ohne Kontext. Kontext ist die Baseline.
Konkrete Richtwerte als Orientierungspunkte — nicht als absolute Grenzwerte:
|
Wait Type |
Gut |
Erhöht |
Kritisch |
Hinweis |
|---|---|---|---|---|
|
PAGEIOLATCH_SH (NVMe) |
< 1 ms avg |
1–5 ms |
> 10 ms |
Buffer Pool oder IO-Problem |
|
PAGEIOLATCH_SH (SSD) |
< 5 ms avg |
5–20 ms |
> 50 ms |
IO-Subsystem analysieren |
|
PAGEIOLATCH_SH (Spindel) |
< 20 ms avg |
20–50 ms |
> 100 ms |
Working Set vs. RAM |
|
WRITELOG |
< 2 ms avg |
2–10 ms |
> 20 ms |
Log-Volume, VLF-Anzahl |
|
LCK_M_* (avg) |
< 500 ms |
0,5–5 s |
> 10 s |
Blocking-Analyse, Kap. 14 |
|
SOS_SCHEDULER_YIELD (%) |
< 1% gesamt |
1–5% |
> 10% |
Signal Wait prüfen |
|
Signal Wait-Anteil |
< 5% |
5–20% |
> 20% |
CPU unter Druck |
Tabelle 9.1: Richtwerte für Wait-Statistiken — immer im Kontext der Baseline interpretieren
Die Baseline-Erhebung ist keine einmalige Aufgabe — es ist ein kontinuierlicher Prozess. Ein Server der im Januar unter normaler Last gemessen wird, zeigt andere Werte als derselbe Server im Dezember, wenn das Jahresabschlussgeschäft läuft. Saisonale Muster, Monatsende-Spitzen, wöchentliche Batch-Jobs: All das muss in die Baseline einfließen.
Praktische Empfehlung: Baue über mindestens 4 Wochen normale Betriebsdaten auf, bevor du Auffälligkeiten bewertest. Collect-SqlPerf.ps1 (Kapitel 31) nimmt stündliche Snapshots und speichert sie in einer Verlaufstabelle. Nach 4 Wochen hast du genug Daten um Trends, Ausreißer und echte Anomalien zu unterscheiden.
|
Tipp: Was ist "normal" für PAGEIOLATCH_SH? |
|---|
|
Das hängt vom Storage-Typ ab. Auf einem System mit NVMe-SSDs sind PAGEIOLATCH_SH avg_wait_ms unter 0,5 ms normal. Auf einem klassischen SAN mit Spindeln können 15 ms noch akzeptabel sein — wenn die Baseline das bestätigt. Neu ist die Frage: Liegt der aktuelle Wert signifikant über der Baseline? Wenn ein System normalerweise 0,8 ms zeigt und plötzlich 12 ms zeigt, ist das ein Alarm — auch, wenn 12 ms "objektiv" nicht dramatisch klingt. |
Langfristige Baseline-Strategie für produktive Umgebungen:
CPU als Diagnoseachse: Signal Waits, Scheduler und THREADPOOL
CPU ist kein einzelner Wait Type, sondern eine Diagnoseachse die sich durch mehrere Wait Types und DMVs zieht. Das ist wichtig zu verstehen: Wenn CPU das Problem ist, siehst du das nicht nur an einem einzigen Wait Type — du siehst es als Muster über mehrere Indikatoren hinweg. In Kapitel 15 (Ausführungspläne), Kapitel 16 (Statistiken und Plan Regression) und Kapitel 18 (Parameter Sniffing) kommen wir jeweils auf CPU als Thema zurück — weil schlechte Pläne, zu viele Recompilations und Parameter-Sniffing-Probleme alle auf CPU-Ebene sichtbar werden.

Abb. 9.3: CPU-Diagnoseachse: Von Signal Waits bis THREADPOOL
Der vollständige CPU-Diagnose-Workflow hat drei Stufen — von der aggregierten Sicht zur Session-genauen Analyse:
Stufe 1: Signal Wait-Anteil prüfen
Das Signal Wait-Verhältnis ist der erste Screening-Test. Unter 10%: CPU ist entspannt, die Wartezeiten kommen aus anderen Quellen. 10–20%: erste Zeichen von Druck, beobachten und Trend verfolgen. Über 20%: CPU unter ernsthafter Last, jetzt aktiv werden. Über 30%: Die Runnable Queue läuft voll, jede neue Anfrage spürt die CPU-Latenz unmittelbar.
Stufe 2: sys.dm_os_schedulers analysieren
Wenn der Signal Wait-Anteil erhöht ist, ist der nächste Schritt sys.dm_os_schedulers. Diese DMV zeigt den Zustand jedes einzelnen SQLOS-Schedulers:
-- Scheduler-Auslastung analysieren — wo steckt der CPU-Druck?
-- runnable_tasks_count > 1 pro Scheduler bedeutet Warteschlange
-- work_queue_count = wartende Tasks die noch keinen Scheduler haben
SELECT
scheduler_id,
cpu_id, -- Welcher physische CPU-Kern
status,
is_idle,
-- Wieviele Tasks warten gerade auf diesen Scheduler?
-- > 1 = Überlastung, dieser Scheduler hat mehr als er kann
runnable_tasks_count,
-- Wieviele Worker-Threads sind auf diesem Scheduler aktiv?
active_workers_count,
-- Wieviele Tasks warten noch ohne Scheduler-Zuweisung?
work_queue_count,
total_cpu_usage_ms,
total_scheduler_delay_ms -- Gesamte Verzögerungszeit durch Scheduler-Überlast
FROM sys.dm_os_schedulers
-- Nur Online-Scheduler, nicht interne System-Scheduler
WHERE status = 'VISIBLE ONLINE'
ORDER BY runnable_tasks_count DESC, work_queue_count DESC;
Ein runnable_tasks_count von 0 oder 1 pro Scheduler bedeutet: Der Scheduler ist entspannt, Threads bekommen sofort CPU-Zeit. Werte von 3, 5, 10 bedeuten: Es bildet sich eine Schlange. Bei NUMA-Systemen mit mehreren CPU-Knoten (Kapitel 1, Kapitel 3) kann man hier auch NUMA-Imbalance erkennen: Wenn ein NUMA-Knoten dauerhaft höhere runnable_tasks_count hat als der andere, ist die Workload ungleich verteilt.
Stufe 3: Runnable Requests identifizieren
Sobald klar ist, dass CPU der Engpass ist, hilft sys.dm_exec_requests um herauszufinden, welche spezifischen Abfragen die CPU fressen:
-- Wer wartet gerade auf CPU? Alle runnable Sessions
-- status = 'runnable' bedeutet: bereit, aber kein Scheduler frei
-- cpu_time zeigt den kumulativen CPU-Verbrauch dieser Session
SELECT
r.session_id,
r.status, -- 'runnable' = wartet auf CPU
r.command,
r.wait_type,
r.wait_time, -- ms bereits gewartet
r.cpu_time, -- ms CPU bereits verbraucht — hohe Werte = CPU-Fresser
r.total_elapsed_time,
r.logical_reads, -- Viele logical reads + wenig IO = CPU-intensive Scans
DB_NAME(r.database_id) AS Datenbank,
SUBSTRING(t.text, 1, 200) AS query_text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.status = 'runnable'
OR r.cpu_time > 10000 -- Auch laufende Sessions mit hohem CPU-Verbrauch
ORDER BY r.cpu_time DESC;
Die Kombination aus hohem cpu_time und vielen logical_reads ohne entsprechende physische IO-Waits ist ein klassisches Zeichen für CPU-intensive Table Scans — Abfragen die viele Seiten aus dem Buffer Pool lesen und durchsuchen. Kein Disk-IO, aber immenser CPU-Aufwand. Die Lösung liegt in den Ausführungsplänen (Kapitel 15) und häufig in fehlenden Indizes (Kapitel 17).
Scheduler Pressure: Wenn der Stau unsichtbar wird
Es gibt einen Effekt, der bei hoher CPU-Last besonders tückisch ist: Scheduler Pressure macht sich oft nicht in der CPU-Auslastungsanzeige bemerkbar. Du siehst 85% CPU-Last im Performance Monitor. Das klingt besorgniserregend, aber nicht katastrophal. Die Abfragen laufen aber dreimal langsamer als normal. Was ist passiert?
Das Problem ist, dass die CPU-Auslastungsanzeige im Task Manager oder Performance Monitor nur die tatsächlich auf der CPU verbrachte Zeit misst. Die Zeit, die Threads in der Runnable Queue warten — also Signal Waits — erscheint dort nicht als CPU-Last, sondern als "Leerlauf". Scheduler Pressure diagnostiziert man nicht über den Task Manager, sondern über Signal Wait-Anteil und sys.dm_os_schedulers.
Scheduler Pressure als eigenständiges Konzept: SQL Server braucht nicht nur freie CPU-Kapazität (kein 100%-CPU), um reibungslos zu funktionieren. Er braucht freie Scheduler-Slots. Der SQLOS-Scheduler (Kapitel 4) hat genau so viele Slots wie es logische CPUs gibt. Wenn alle Slots besetzt sind und neue Threads ankommen, entsteht Scheduler Pressure — auch, wenn die physische CPU-Auslastung noch unter 100% liegt. Ein Thread kann durchaus auf einen Scheduler warten, während ein anderer Scheduler denselben Thread nach 4 ms wieder freigibt.
|
Praxisbeispiel: Scheduler Pressure bei Musterwerk GmbH |
|---|
|
Bei Musterwerk GmbH (MWSQL01, Kapitel 32) sah die CPU-Last morgens um 9 Uhr nach 78% aus — was der DBA als "ein bisschen viel, aber okay" eingestuft hatte. Der Signal Wait-Anteil in sys.dm_os_wait_stats lag gleichzeitig bei 31%. runnable_tasks_count auf den meisten Schedulern war dauerhaft bei 3 bis 4. Ursache: Ein parametrisierter Report lief mit DOP 16 und schluckte die meisten Scheduler. MAXDOP auf 4 für die Reporting-Workload gesetzt — Signal Wait-Anteil sank auf 8%. CPU-Auslastung: immer noch 74%. Aber die OLTP-Abfragen hatten wieder freie Scheduler und die p95-Latenzen halbierten sich. |
MAXDOP als Gegenmaßnahme bei Scheduler Pressure: Wenn parallele Abfragen den Scheduler-Pool dominieren, hilft MAXDOP-Reduktion. Weniger parallele Threads pro Abfrage bedeuten weniger gleichzeitig benötigte Scheduler. Das ist nicht immer die beste Lösung — manchmal ist MAXDOP-Reduktion ein Pflaster, das die eigentliche Ursache verdeckt (schlechte Statistiken, fehlende Indizes). Aber als Sofortmaßnahme ist es effektiv. Die vollständige MAXDOP-Entscheidungslogik haben wir in Kapitel 5 behandelt und kommen in Kapitel 15 darauf zurück.
Wait Type Referenz: Die wichtigsten auf einen Blick
Die folgende Tabelle enthält die Wait Types, die in der Praxis am häufigsten auftreten — mit einer kurzen Einordnung der typischen Ursache und der ersten Diagnoserichtung. Anhang B enthält eine erweiterte Referenz aller relevanten Wait Types:
|
Wait Type |
Kategorie |
Typische Ursache |
Erste Diagnoserichtung |
|---|---|---|---|
|
PAGEIOLATCH_SH |
IO |
Seite muss von Disk gelesen werden |
Buffer Pool zu klein / Disk zu langsam → Kap 10, 11 |
|
PAGEIOLATCH_EX |
IO |
Seite muss auf Disk geschrieben werden |
Write-IO langsam / Checkpoint-Druck → Kap 10 |
|
PAGELATCH_EX |
Memory |
Hot Page im Buffer Pool (kein Disk-IO!) |
TempDB PFS/GAM Contention prüfen → Kap 13 |
|
PAGELATCH_SH |
Memory |
Shared Latch auf In-Memory-Seite |
Welche Seite? sys.dm_os_waiting_tasks |
|
LCK_M_X |
Locking |
Exclusive Lock gehalten von anderer Session |
Blocking-Analyse → Kap 14 |
|
LCK_M_S |
Locking |
Shared Lock blockiert durch X-Lock |
Blocking, Isolation Level prüfen → Kap 14 |
|
LCK_M_U |
Locking |
Update Lock — oft Vorstufe zum Deadlock |
Deadlock Graph → Kap 14 |
|
WRITELOG |
IO |
Transaktionslog-IO zu langsam |
Log-Volume prüfen, VLFs → Kap 10, 06 |
|
SOS_SCHEDULER_YIELD |
CPU |
Scheduler überlastet, Thread gibt auf |
Signal Wait-Anteil, Scheduler-Auslastung |
|
THREADPOOL |
CPU |
Kein freier Worker Thread |
max worker threads, Blocking-Kaskade |
|
CXPACKET |
CPU |
Parallelismus-Koordination |
Skew prüfen, MAXDOP, Signal Waits → Kap 15 |
|
CXCONSUMER |
CPU |
Parallelismus-Consumer wartet |
Meist harmlos, Skew ausschließen → Kap 15 |
|
ASYNC_NETWORK_IO |
Netzwerk |
Client liest Daten nicht schnell genug |
Result-Set-Größe, N+1 → Kap 30 |
|
RESOURCE_SEMAPHORE |
Memory |
Memory Grant-Warteschlange |
Fehlende Indizes, große Hash Joins → Kap 12 |
|
RESOURCE_SEMAPHORE_QUERY_COMPILE |
CPU |
Kompilierungsslot belegt |
Hohe Kompilierungsrate → Kap 18 |
|
OLEDB |
Netzwerk |
Linked Server oder OLE DB Call |
Linked Server Performance prüfen |
|
IO_COMPLETION |
IO |
Async IO noch nicht abgeschlossen |
Disk-IO allgemein, oft TempDB |
|
HADR_SYNC_COMMIT |
HA |
Synchrones AG-Commit |
Secondary-Commit-Latenz prüfen |
|
LATCH_EX |
Memory |
Exklusive Sperre auf interne Struktur |
Welche Latch-Klasse? dm_os_latch_stats |
|
DBMIRROR_EVENTS_QUEUE |
HA |
AG-Synchronisation |
HA-Latenz, Netzwerk zu Secondary |
Tabelle 9.2: Wichtige Wait Types mit Diagnoserichtung und Kapitelverweisen
Wait Statistics und Extended Events: Die Kombination
sys.dm_os_wait_stats gibt dir die aggregierte Sicht — welcher Wait Type dominiert, wie oft, wie lange im Schnitt. Was du damit nicht siehst: Welche spezifische Abfrage verursacht den Wait Type? Welche Datenbank? Welche Tabelle? Das sind die Fragen für Extended Events.
Das sqlos.wait_info-Ereignis zeichnet individuelle Wait-Ereignisse auf — jedes Mal, wenn ein Thread auf einen definierten Wait Type trifft und die Wartezeit überschritten ist. Das klingt harmlos, ist aber ein Hochvolumen-Event: Auf einem aktiven System können tausende Wait-Ereignisse pro Sekunde auftreten. Deshalb: Immer mit Filtern arbeiten, nie sqlos.wait_info ohne Wait-Type- und Dauer-Filter aktivieren. Eine detaillierte Einführung in Extended Events — Sessions, Targets, Filter — findest du in Kapitel 8.
-- Extended Event Session für Wait-Diagnose
-- Achtung: wait_info ist sehr volumenstark! Immer mit Filtern einsetzen.
-- Hier: nur PAGEIOLATCH-Waits über 10 ms, um IO-Probleme zu finden
CREATE EVENT SESSION [WaitDiagnose_IO] ON SERVER
ADD EVENT sqlos.wait_info(
WHERE
opcode = 1 -- 1 = End (Warteende erfassen, nicht den Beginn)
AND wait_type = 67 -- 67 = PAGEIOLATCH_SH (Tabelle wait_types für alle IDs)
AND duration > 10 -- Nur Waits über 10 ms — kurze rausfiltern
),
-- Optional: auch WRITELOG miterfassen für gleichzeitige Log-IO-Analyse
ADD EVENT sqlos.wait_info(
WHERE
opcode = 1
AND wait_type = 180 -- 180 = WRITELOG
AND duration > 5 -- Log-IO: schon 5 ms ist erhöht
)
ADD TARGET package0.ring_buffer(SET max_memory = 51200)
WITH (MAX_DISPATCH_LATENCY = 5 SECONDS);
-- Session starten
ALTER EVENT SESSION [WaitDiagnose_IO] ON SERVER STATE = START;
-- Ergebnisse lesen — zeigt welche Sessions die langen IOs haben
SELECT
xdr.value('@timestamp', 'datetime2') AS event_time,
xdr.value('(data[@name="wait_type"]/text)[1]', 'varchar(60)') AS wait_type,
xdr.value('(data[@name="duration"]/value)[1]', 'bigint') AS duration_ms,
xdr.value('(action[@name="session_id"]/value)[1]', 'int') AS session_id,
xdr.value('(action[@name="sql_text"]/value)[1]', 'nvarchar(1000)') AS sql_text
FROM (
SELECT CAST(target_data AS XML) AS target_xml
FROM sys.dm_xe_session_targets t
JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
WHERE s.name = 'WaitDiagnose_IO'
AND t.target_name = 'ring_buffer'
) AS data
CROSS APPLY target_xml.nodes('//RingBufferTarget/event') AS xe(xdr)
ORDER BY duration_ms DESC;
-- Session stoppen und löschen, wenn fertig
-- ALTER EVENT SESSION [WaitDiagnose_IO] ON SERVER STATE = STOP;
-- DROP EVENT SESSION [WaitDiagnose_IO] ON SERVER;
Die Kombination aus Wait Statistics (wo ist das Problem?) und Extended Events (welche Abfrage verursacht es?) ist das vollständige Diagnose-Bild. Wait Statistics zeigt dir das Symptom — Extended Events zeigt dir den Täter. Beide zusammen führen zur Lösung.
Wait Statistics in den Fallstudien — drei Muster, drei Lösungsansätze
Die drei Fallstudien in Teil V zeigen Wait Statistics in echten Szenarien — jedes mit einem anderen dominierenden Muster und einem anderen Lösungsansatz. Wenn du jetzt denkst "das klingt alles sehr theoretisch" — warte auf Kapitel 33. Sparfuchs & Partner wird dir jeden theoretischen Punkt sehr konkret veranschaulichen.
Musterwerk GmbH (MWSQL01, Kapitel 32): Der klassische Mixed-Workload
Musterwerk ist der "normale" Kunde — gute Hardware, grundsolide Konfiguration, aber ein paar blinde Flecken. Das Wait-Profil ist gemischt, kein einzelner Wait Type dominiert katastrophal, aber die Kombination zeigt typische Mixed-Workload-Probleme:
Die gute Nachricht bei Musterwerk: Alle Probleme sind lösbar ohne neue Hardware. Report-Job in ein anderes Zeitfenster verlegen, Resource Governor für Reporting-Workload (Kapitel 12), Transaktions-Timeouts im ERP konfigurieren. In Kapitel 32 gehen wir durch alle Schritte.
Sparfuchs & Partner (BUCHSQL01, Kapitel 33): Die komplette Sammlung
Sparfuchs & Partner ist das Extrembeispiel — jeder erdenkliche Fehler auf einmal. Das Wait-Profil ist kein "ein Problem dominiert", sondern ein komplettes Chaos bei dem man priorisieren muss:
Die Priorisierung der Maßnahmen bei Sparfuchs ist kritisch: Wenn man die CPU-Probleme löst ohne das IO-Problem zu beheben, wird das IO noch mehr zum Engpass. Wenn man das IO-Problem löst, wird die CPU mehr entlastet, weil IO-Wait-Zeit reduziert wird. Reihenfolge: zuerst Hardware (Storage, RAM), dann Konfiguration, dann Code. In Kapitel 33 erarbeiten wir den vollständigen Sanierungsplan.
Trendforge Digital GmbH (TFSQL01, Kapitel 34): Gute Hardware, kaputte Applikation
Trendforge ist das Gegenteil von Sparfuchs: Die Hardware ist gut, die SQL Server-Konfiguration ist korrekt, die Infrastruktur stimmt. Und trotzdem gibt es Performance-Probleme — weil die Applikation die Hardware kaputt macht. Das Wait-Profil verrät sofort wo das Problem liegt:
Die Herausforderung bei Trendforge: Der DBA kann das Problem mit Wait Statistics exakt diagnostizieren und benennen — aber ohne Mitarbeit der Entwickler nicht lösen. Fehlende Indizes kann der DBA noch hinzufügen. N+1-Probleme im ORM nicht. Das ist die politische Dimension von Performance-Analyse, die in Kapitel 34 ausführlich behandelt wird.
Diagnose: Wait Statistics strukturiert anwenden
Symptome
|
Hinweis: Woran erkennst du ein Wait Statistics-Problem? |
|---|
|
Nutzer berichten über sporadisch langsame Abfragen ohne erkennbares Muster — mal schnell, mal langsam, keine klare Korrelation |
|
CPU-Auslastung schwankt stark, ohne, dass der Task Manager einen klaren Täter zeigt — Signal Waits als unsichtbare Last |
|
Bestimmte Stoßzeiten (morgens 9 Uhr, Monatsende) sind systematisch langsamer als Normalzeiten |
|
Fehler 17189 im SQL Server Error Log: "Konnte keinen neuen Thread erstellen" — THREADPOOL-Problem |
|
sys.dm_exec_requests zeigt viele Sessions im Status "suspended" oder "runnable" gleichzeitig |
|
PAGEIOLATCH-Waits bei einem System, das eigentlich genug RAM haben sollte — Buffer Pool kalt nach Serverstart, oder Working Set zu groß |
|
WRITELOG-Waits bei normaler Transaktionslast — Log-Volume checken, VLF-Anzahl prüfen |
|
ASYNC_NETWORK_IO prominent — Client-seitige Verarbeitung zu langsam oder N+1-Probleme |
So misst du das
|
Tipp: Sofortdiagnose in 3 Schritten |
|---|
|
Schritt 1 — Signal Wait-Anteil: Führe die Signal/Resource-Wait-Query aus (s.o.). Über 20% = CPU-Problem, weiter mit sys.dm_os_schedulers. |
|
Schritt 2 — Top-Waits: Führe die Top-20-Wait-Query aus. Welcher Wait Type dominiert? Das gibt die Diagnoserichtung. |
|
Schritt 3 — Aktive Sessions: SELECT session_id, status, wait_type, wait_time, blocking_session_id FROM sys.dm_exec_requests WHERE status NOT IN ('background', 'sleeping') ORDER BY wait_time DESC. |
Typische Fehlinterpretationen
|
Warnung: Was harmlos aussieht aber keins ist — und umgekehrt |
|---|
|
CXPACKET hoch = Problem? Nicht automatisch. Erst Signal Wait-Anteil prüfen. Bei niedrigem Signal Wait ist CXPACKET normaler Parallelismus-Overhead. |
|
PAGEIOLATCH_SH hoch nach Serverstart? Normal — der Buffer Pool ist leer und wärmt sich auf. Erst nach der Warmlaufphase (30–60 min) messen. |
|
Kleine absolute Wait-Werte = kein Problem? Nur, wenn die Anzahl der Ereignisse auch klein ist. 1 ms × 500.000 Ereignisse = ernst nehmen. |
|
THREADPOOL kurz gesehen und dann nicht mehr? Schon einmal ist zu oft. Suche die Ursache im Error Log (17189) und in Blocking-Ketten. |
|
WRITELOG niedrig = Log-IO okay? Nur, wenn gleichzeitig avg_wait_ms niedrig ist. Ein Wait Type mit wenigen Ereignissen aber 80 ms Durchschnitt ist ernst. |
Erste Gegenmaßnahmen
|
Tipp: Triage — nicht Volltherapie |
|---|
|
IO-dominiert (PAGEIOLATCH_SH/EX): Buffer Pool voll? sys.dm_os_performance_counters: Page Life Expectancy prüfen. Unter 300 Sekunden: Buffer Pool zu klein. Max Server Memory richtig gesetzt? Kapitel 11. |
|
CPU-dominiert (SOS_SCHEDULER_YIELD, Signal Wait > 25%): sys.dm_exec_requests nach cpu_time sortieren. Top-Abfragen identifizieren. MAXDOP für Reporting-Workload prüfen. |
|
Locking (LCK_M_*): sys.dm_exec_requests nach blocking_session_id filtern. Wer ist der Blocker? Was macht er? KILL nur als absoluter letzter Ausweg und mit Vorsicht. |
|
THREADPOOL: Sofort max worker threads temporär erhöhen (sp_configure). Dann Blocking-Kette suchen die den Pool leert. Ursache immer finden. |
|
WRITELOG: Log-Volume IO-Latenz prüfen (sys.dm_io_virtual_file_stats). VLF-Anzahl prüfen (DBCC LOGINFO). Log-Backup ausführen, wenn nötig. Kapitel 10. |
Zusammenfassung
Wait Statistics sind das erste Werkzeug in jedem Performance-Troubleshooting — nicht, weil alles andere unwichtig ist, sondern, weil sie die Richtung vorgeben. Ohne diese Orientierung läufst du blind. Mit ihr weißt du nach zwei Minuten Diagnose, ob du als nächstes nach Kapitel 10, 11, 12, 14 oder 15 greifen sollst.
Im nächsten Kapitel gehen wir tiefer in das IO-Subsystem — mit konkreten Latenzwerten aus sys.dm_io_virtual_file_stats, der Frage warum Autogrowth-Events die Latenz explodieren lassen, und dem Sparfuchs-Szenario: p95-Schreib-Latenzen von 312 ms auf einer einzigen Spindel, die OS, Data, Log, TempDB und Backup gleichzeitig bedient.
Kapitel 10
