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.

Query Performance & Ausführungspläne: – 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 ]

Table of Contents
2
3

Query Performance & Ausführungspläne:

Was der Optimizer denkt — und warum er manchmal irrt

Du hast schon mal eine Query gehabt, die gestern noch in 50 ms lief und heute 40 Sekunden braucht. Alles ist gleich geblieben — gleiche Daten, gleicher Server, gleiche Abfrage. Was ist passiert? Willkommen in der Welt der Ausführungspläne, wo SQL Server jeden Morgen neu entscheidet, wie er deine Queries ausführt — und manchmal einfach falsch liegt.

Dieses Kapitel ist das Herzstück der Query-Diagnose. Wir schauen uns an, wie der Query Optimizer arbeitet, wie man Ausführungspläne liest (und dabei keine falschen Schlüsse zieht), warum manche Join-Typen deine CPU in Brand setzen, und wie du mit konkreten Werkzeugen die teuersten Queries deines Systems aufspürst. CPU als Diagnoseachse zieht sich durch das gesamte Kapitel — denn die meisten Query-Probleme enden irgendwann als CPU-Problem.

Querverweise vorab: Die Wait Statistics aus Kapitel 9 — insbesondere CXPACKET und SOS_SCHEDULER_YIELD — sind eng mit dem verknüpft, was wir hier sehen werden. Die MAXDOP-Konfiguration aus Kapitel 5 bestimmt, wie aggressiv SQL Server parallelisiert. Und was wir hier über Ausführungspläne lernen, ist die Grundlage für Kapitel 16 (Statistiken und Plan Regression) und Kapitel 18 (Parameter Sniffing). Alles hängt zusammen — dieses Kapitel ist der Knotenpunkt.

Der Query Optimizer: Guter Ratgeber mit schlechten Informationen

Bevor wir Ausführungspläne lesen können, müssen wir verstehen, wer sie produziert. Der Query Optimizer ist ein Cost-Based Optimizer — er berechnet für jede denkbare Art, eine Query auszuführen, geschätzte Kosten und wählt den günstigsten Plan. "Kosten" bedeutet dabei eine abstrakte Einheit aus CPU-Aufwand, IO-Operationen und Speicherbedarf, keine Millisekunden.

Das klingt vernünftig, und für den Normalfall funktioniert es auch gut. Das Problem: Der Optimizer arbeitet mit Schätzungen, nicht mit Gewissheiten. Er kennt die Statistiken deiner Tabellen — ungefähre Verteilungen der Spaltenwerte — aber nicht den exakten Datenbestand zum Zeitpunkt der Abfrage. Sind diese Statistiken veraltet oder ungenau, fällt der Optimizer Entscheidungen auf Basis falscher Annahmen. Und falsche Annahmen führen zu falschen Plänen.

Compiled Plan, Cached Plan, Executed Plan — drei verschiedene Realitäten

Hier lauert eine häufige Verwechslung. SQL Server unterscheidet drei Zustände eines Ausführungsplans:

  • Compiled Plan: Der Plan, den der Optimizer beim ersten Ausführen erstellt. Enthält geschätzte Zeilenzahlen und Kosten. Wird im Plan Cache gespeichert.
  • Cached Plan: Derselbe Compiled Plan, der bei Wiederverwendung aus dem Cache geladen wird. Gilt auch bei anderen Parameterwerten — was zu Parameter Sniffing führt (Kapitel 18).
  • Actual Execution Plan: Was wirklich passiert ist. Enthält tatsächliche Zeilenzahlen, tatsächliche Iterationen, tatsächlichen Ressourcenverbrauch. Nur im SSMS sichtbar, wenn "Include Actual Execution Plan" aktiviert ist.
  • Der Unterschied zwischen Estimated und Actual ist die wichtigste Information im Ausführungsplan. Eine große Diskrepanz — Optimizer schätzt 10 Zeilen, tatsächlich werden 1.000.000 verarbeitet — ist fast immer ein Signal für veraltete Statistiken. Was damit zu tun ist, erklärt Kapitel 16 in aller Tiefe.

     

    Hinweis: Compiled vs. Actual: der Unterschied zählt

    SSMS zeigt standardmäßig den Estimated Plan — der entspricht dem Compiled Plan und enthält keine echten Laufzeitdaten. Erst "Include Actual Execution Plan" (Strg+M) zeigt, was wirklich passiert ist.

    Für die Diagnose immer den Actual Plan nehmen. Der Estimated Plan lügt — nicht böswillig, aber systematisch.

     

    Was der Optimizer nicht kann

    Der Optimizer ist kein Wunderkind. Er sieht nicht, ob gerade 200 andere Queries gleichzeitig laufen. Er weiß nicht, wie warm der Buffer Pool ist. Er kennt keine Echtzeit-IO-Latenz. Und er modelliert Korrelationen zwischen Spalten nur begrenzt — wenn zwei Spalten zusammen gefiltert werden und ihre Werte voneinander abhängen (z.B. Stadt und Postleitzahl), unterschätzt er regelmäßig die Selektivität.

    Das Ergebnis: Pläne sind immer Approximationen. Meistens gute Approximationen — aber, wenn sie schief liegen, liegen sie oft dramatisch schief. Ein Nested Loop über eine Tabelle mit 10 Millionen Zeilen statt einem Hash Join: Faktor 1000 in der Ausführungszeit ist keine Seltenheit.

    Ausführungspläne lesen: Von rechts nach links, von oben nach unten

    Der grafische Ausführungsplan in SSMS sieht zunächst einschüchternd aus — eine Kaskade von Icons, verbunden durch Pfeile unterschiedlicher Dicke. Wenn du die Grundregeln kennst, wird er zum wichtigsten Diagnosewerkzeug überhaupt.

    Die Grundregeln

  • Lesen von rechts nach links: Die Datenquellen (Table Scan, Index Seek) sind ganz rechts. Der Ergebnis-Operator (SELECT, INSERT, UPDATE) ist ganz links. Die Ausführung fließt von rechts nach links.
  • Lesen von oben nach unten bei Paralleloperationen: Obere Äste werden zuerst ausgeführt — wichtig bei Hash Joins, wo der Build-Input oben liegt.
  • Pfeilbreite = Datenmenge: Ein dicker Pfeil bedeutet viele Zeilen zwischen zwei Operatoren. Ein sehr dicker Pfeil wo keiner sein sollte ist sofort verdächtig.
  • Prozentzahlen = geschätzte Kosten: Die Zahl an jedem Operator zeigt seinen Anteil an den Gesamtkosten. Ein Operator mit 80% ist der Kandidat für die Optimierung.
  • Warnings = sofort anschauen: Gelbe Ausrufezeichen an Operatoren sind keine Hinweise, die man ignorieren kann. Sie bedeuten fast immer ein handfestes Problem.
  • Für den Plan als XML: SET STATISTICS XML ON zeigt denselben Plan als XML-Dokument — wichtig für automatisierte Analyse oder, wenn SSMS nicht verfügbar ist.

    Die wichtigsten Plan-Operatoren

     

    Operator

    Was er tut

    Wann er auftritt

    Warnsignal

    Table Scan

    Liest gesamte Tabelle ohne Index (Heap)

    Keine Indizes, oder kein nutzbarer Index vorhanden

    Immer bei großen Tabellen

    Index Scan

    Liest gesamten Index (alle Blattseiten)

    Index vorhanden, aber nicht selektiv genug für Seek

    Bei selektiver Filterung

    Index Seek

    Springt direkt zur gesuchten Position im Index (B-Tree-Navigation)

    Selektive Filterung mit passendem Index

    Kein Warnsignal — das ist das Ziel

    Key Lookup

    Holt fehlende Spalten aus dem Clustered Index nach

    Non-Clustered Index deckt nicht alle benötigten Spalten

    Sehr häufig bei hohem Datenvolumen

    Nested Loops

    Iteriert über äußere Menge, sucht für jede Zeile in innerer Menge

    Kleine äußere Menge + Index auf innere Menge

    Wenn äußere Menge groß ist

    Hash Join

    Baut Hash-Tabelle aus kleinerer Menge, sondiert mit größerer

    Große Mengen ohne passenden Index

    Wenn Speicher für Build-Phase zu klein

    Merge Join

    Führt zwei sortierte Eingaben zusammen

    Beide Seiten bereits sortiert (Index)

    Wenn eine Seite unerwartet sortiert werden muss

    Sort

    Sortiert Datenmenge

    ORDER BY, GROUP BY, Merge Join-Vorbereitung

    Wenn Spill in TempDB möglich

    Spool

    Speichert Zwischenergebnis für Wiederverwendung

    Korrelierte Subqueries, recursive CTEs

    Lazy Spool mit vielen Rewinds

    Tabelle 15.1: Häufige Plan-Operatoren und ihre Bedeutung

     

    Warnings im Ausführungsplan — was sie wirklich bedeuten

    Warnungen im Ausführungsplan sind kein kosmetisches Problem. Sie sind Fehlermeldungen des Optimizers — Hinweise, dass etwas nicht so gelaufen ist wie geplant oder, dass Informationen fehlen. Die häufigsten:

  • Fehlende Statistiken: "Statistics Missing" — der Optimizer hatte keine Informationen über die Datenverteilung. Ergebnis: Schätzung war geraten, Plan möglicherweise komplett falsch. Lösung: UPDATE STATISTICS.
  • Spill to TempDB: "Warning: Operator used tempdb to spill data" — ein Operator (Hash Join, Sort, Exchange) hatte nicht genug Arbeitsspeicher (Memory Grant zu klein). Verweist auf Kapitel 12 (Memory Grants und Spills) und Kapitel 13 (TempDB).
  • Implicit Conversion: "Type conversion in expression may affect CardinalityEstimate" — ein Datentyp-Mismatch zwingt SQL Server zur impliziten Konvertierung. Mehr dazu im Abschnitt "Implicit Conversion" weiter unten.
  • Residual Predicate: Filter nach dem Index Seek, weil der Index das Prädikat nur teilweise abdeckt. Index vorhanden, aber nicht optimal zugeschnitten.
  • Die drei Join-Typen — und warum sie deine CPU interessieren

    Joins sind der CPU-Verbrauch-Treiber Nummer eins in analytischen Workloads. Welchen Join-Typ der Optimizer wählt, bestimmt maßgeblich, ob eine Query in 100 ms oder 10 Minuten fertig ist. Das Entscheidende: Der Optimizer wählt den Join-Typ auf Basis seiner Kardinalitätsschätzungen — und, wenn die falsch sind, wählt er den falschen Typ, mit entsprechenden CPU-Konsequenzen.

    Nested Loops: Gut gemeint, oft überschätzt

    Der Nested Loop Join iteriert für jede Zeile der äußeren (treibenden) Tabelle durch die innere Tabelle. Die Komplexität ist O(n × m) — wobei n die Größe der äußeren Menge und m die durchschnittliche Lookup-Kosten in der inneren Menge ist. Mit einem Index auf der inneren Tabelle reduziert sich m auf O(log m) pro Lookup.

    Nested Loops sind optimal, wenn: die äußere Menge klein ist (wenige hundert oder tausend Zeilen) und die innere Tabelle einen passenden Index hat. Dann ist jeder Lookup günstig, und der Join ist insgesamt sehr effizient.

    Das CPU-Problem entsteht, wenn der Optimizer die äußere Menge unterschätzt. Schätzt er 50 Zeilen, wählt er Nested Loops — tatsächlich kommen 500.000 Zeilen. Jetzt werden 500.000 Index-Lookups ausgeführt, die jeweils CPU und IO kosten. Das summiert sich zu einer CPU-Explosion, die im Task Manager wie ein simpler Lastspike aussieht. In den Wait Statistics (Kapitel 9) siehst du dann SOS_SCHEDULER_YIELD — die CPUs sind vollständig gesättigt, die Queries rennen, aber die Wartezeiten steigen trotzdem.

    Hash Join: Der Allrounder für große Mengen

    Hash Join funktioniert in zwei Phasen: Build Phase (kleinere Tabelle wird als Hash-Tabelle im Arbeitsspeicher aufgebaut) und Probe Phase (größere Tabelle wird durchlaufen, jede Zeile gegen die Hash-Tabelle geprüft). Die Komplexität ist O(n + m) — deutlich besser als Nested Loops bei großen Mengen.

    Hash Join braucht einen Memory Grant — SQL Server reserviert Speicher vor der Ausführung für die Build-Phase. Wenn der tatsächliche Speicherbedarf den Grant übersteigt, spilled die Hash-Tabelle nach TempDB. Das ist sehr teuer: Statt RAM-Operationen folgen IO-Operationen auf TempDB, und die Ausführungszeit kann sich ver-10-fachen. Das Warning "Operator used tempdb to spill data" im Ausführungsplan zeigt genau das.

    Hash Join ist ohne Vorbedingungen an die Sortierung der Eingaben: Er funktioniert auf unsortierten Mengen. Daher ist er der Standard-Join-Typ, wenn kein passender Index für Merge oder Nested Loops vorhanden ist. Das ist kein Fehler des Optimizers — oft ist Hash Join tatsächlich die beste verfügbare Option.

    Merge Join: Der Elegante — aber mit Vorbedingungen

    Merge Join führt zwei sortierte Eingaben zusammen, wie beim Mischen von zwei Kartenstapeln. Komplexität O(n + m) — genau wie Hash Join, aber ohne den Speicherbedarf der Build-Phase. Der Haken: Beide Eingaben müssen nach der Join-Spalte sortiert sein.

    In der Praxis bedeutet das: Merge Join ist optimal, wenn ein Index auf der Join-Spalte vorhanden ist, der die Daten bereits sortiert liefert. Dann entstehen keine zusätzlichen Sort-Operatoren, der Merge ist günstig, und der Speicherbedarf ist minimal. Ohne passendem Index muss der Optimizer zuerst sortieren — eine teure Operation, die möglicherweise einen Hash Join doch günstiger macht.

     

    Join-Typ

    Komplexität

    Vorbedingung

    CPU-Charakteristik

    Speicherbedarf

    Typischer Einsatz

    Nested Loops

    O(n × m)

    Index auf innerer Tabelle empfohlen

    Niedrig bei kleinen Mengen, Explosion bei falscher Schätzung

    Minimal

    Kleine äußere Menge (< 1.000 Zeilen), OLTP-Punkt-Lookups

    Hash Join

    O(n + m)

    Keine — funktioniert immer

    Moderat und vorhersehbar

    Hoch (Build-Phase), Spill möglich

    Große Mengen, kein passender Index, DWH-Abfragen

    Merge Join

    O(n + m)

    Beide Seiten sortiert nach Join-Spalte

    Niedrig, wenn Index vorhanden, teuer bei explizitem Sort

    Minimal

    Indizierte Join-Spalten, Range-Scans, geordnete Ausgabe

    Tabelle 15.2: Join-Typen im Vergleich — Komplexität, CPU und Speicher

     

     

    Warnung: Falsche Kardinalitätsschätzung → CPU-Explosion

    Der gefährlichste Fall: Optimizer schätzt 100 Zeilen, wählt Nested Loops. Tatsächlich kommen 1.000.000 Zeilen. Ergebnis: 1.000.000 Index-Lookups, jeder mit CPU- und IO-Kosten.

    Im Actual Execution Plan erkennbar an: Estimated Rows 100 vs. Actual Rows 1.000.000 beim Join-Operator. Ursache ist fast immer veraltete Statistik — Kapitel 16 erklärt die Diagnose und Lösung.

    In den Wait Statistics (Kapitel 9): Wenn SOS_SCHEDULER_YIELD dominiert und die verdächtige Query läuft, ist das das Muster. CPU ist 100%, aber die Wartezeiten steigen — nicht, weil gewohnt wird, sondern, weil die Arbeit selbst irrsinnig viel CPU kostet.

     

    Parallelismus: Wenn SQL Server alle CPUs gleichzeitig anfasst

    SQL Server kann Queries parallel ausführen — mehrere Threads arbeiten gleichzeitig an verschiedenen Teilen der Query. Das klingt wie eine gute Idee, und für die richtigen Workloads ist es das auch. Für die falschen Workloads ist Parallelismus die schnellste Methode, einen Server in die Knie zu zwingen.

    Wie parallele Pläne entstehen

    Der Optimizer berechnet für jede Query sowohl einen seriellen als auch einen parallelen Plan. Wenn die geschätzten Kosten des seriellen Plans den Schwellwert "Cost Threshold for Parallelism" überschreiten (Standard: 5 — absurd niedrig, wie wir in Kapitel 5 gesehen haben), wählt der Optimizer den parallelen Plan, sofern MAXDOP > 1 ist.

    Der Schwellwert von 5 wurde für Hardware aus den 1990er Jahren kalibriert. Auf moderner Hardware führt er dazu, dass triviale Queries parallelisiert werden, obwohl der Overhead des Parallelismus größer ist als sein Nutzen. Empfehlung aus Kapitel 5: Cost Threshold auf mindestens 50, besser 75-100 setzen.

    MAXDOP: Der Regler für den Parallelismus-Grad

    MAXDOP (Maximum Degree of Parallelism) begrenzt, wie viele Threads eine einzelne Query nutzen darf. Der Server-weite MAXDOP (sp_configure, Kapitel 5) ist die Obergrenze. Hinzu kommt der Query-Level-MAXDOP per OPTION(MAXDOP n)-Hint, der den Server-Wert für diese eine Query überschreibt.

    Auf einem 16-Kern-System mit 2 NUMA-Nodes: MAXDOP = 8 ist ein vernünftiger Ausgangswert — begrenzt auf einen NUMA-Node, vermeidet NUMA-Crossing-Overhead. Die vollständige MAXDOP-Berechnungsformel haben wir in Kapitel 1 (Hardware-Grundlagen) und Kapitel 3 (Virtualisierung) diskutiert.

    CXPACKET-Waits: Das Fingerabdruck des Parallelismus

    CXPACKET ist der Wait Type, der entsteht, wenn ein Thread auf andere Threads desselben parallelen Plans wartet. Er ist einer der am häufigsten missverstandenen Wait Types überhaupt. Wie wir in Kapitel 9 (Wait Statistics) gesehen haben: CXPACKET allein ist kein Problem. CXPACKET mit hohem Signal Wait-Anteil ist ein ernstes CPU-Problem.

    Die Unterscheidung ist kritisch: Wenn eine CPU-intensive Query parallelisiert und alle Threads gleichmäßig ausgelastet sind, entstehen CXPACKET-Waits als normales Synchronisationsartefakt. Wenn hingegen ein Thread fertig ist und auf andere wartet, weil die Arbeitsteilung ungleich ist (Skewed Parallelism), entstehen CXPACKET-Waits aus echter Ineffizienz. Letzteres erkennst du daran, dass die Thread-Zeiten stark variieren.

    Die Exchange-Operatoren im parallelen Plan

    In einem parallelen Ausführungsplan siehst du Operatoren, die es im seriellen Plan nicht gibt: Repartition Streams, Distribute Streams und Gather Streams. Diese Exchange-Operatoren sind die Synchronisationspunkte zwischen parallelen Threads.

  • Distribute Streams: Verteilt Zeilen von einem Thread auf mehrere Threads. Startet den parallelen Bereich des Plans.
  • Repartition Streams: Verteilt Zeilen zwischen parallelen Threads nach einem Schlüssel um (z.B. für Hash Join über mehrere Threads).
  • Gather Streams: Sammelt Ergebnisse mehrerer paralleler Threads in einem einzigen Thread. Beendet den parallelen Bereich.
  • Jeder Exchange-Operator hat Overhead — Synchronisation, Puffer zwischen Threads, Thread-Kontext-Wechsel. Bei kleinen Queries überwiegt dieser Overhead leicht den Nutzen der Parallelisierung. Das ist der Grund, warum OLTP-Workloads mit vielen kleinen Queries besser ohne Parallelismus laufen.

    Wann Parallelismus hilft und wann er schadet

     

    Szenario

    Parallelismus sinnvoll?

    Begründung

    Große Aggregation (SUM, COUNT über Millionen Zeilen)

    Ja

    Arbeit lässt sich gut aufteilen, Overhead gering relativ zum Nutzen

    Hash Join über zwei große Tabellen

    Ja

    Beide Phasen (Build und Probe) parallelisierbar

    Einzelne Zeile nach Primary Key suchen

    Nein

    Overhead des Parallelismus > gesamte Query-Kosten

    1.000 kleine OLTP-Queries gleichzeitig

    Nein

    Alle Queries um dieselben Threads konkurrieren, Gesamtdurchsatz sinkt

    Reporting-Query auf einer OLTP-Instanz

    Abhängig

    Kann OLTP-Queries ausbremsen — Resource Governor oder MAXDOP-Hint erwägen

    Batch-Verarbeitung nachts, Server sonst leer

    Ja

    Maximaler Durchsatz, keine Konkurrenz

    Tabelle 15.3: Parallelismus — wann sinnvoll, wann kontraproduktiv

     

     

    Tipp: MAXDOP-Hint für einzelne Queries

    Wenn eine schwere Reporting-Query regelmäßig CXPACKET-Waits verursacht und OLTP-Queries ausbremst, hilft OPTION(MAXDOP 4) direkt in der Query — ohne den Server-weiten MAXDOP anzutasten.

    Umgekehrt: Wenn eine Query nie parallelisiert werden soll (z.B. weil sie nachts läuft und der Server ausgelastet ist), erzwingt OPTION(MAXDOP 1) seriellen Betrieb.

    Diese Hints sind eine Überbrückung, keine Dauerlösung. Die richtige Antwort ist oft eine bessere Indexstruktur oder ein angepasster Cost Threshold for Parallelism.

     

    Key Lookup: Der Preis für unvollständige Indizes

    Der Key Lookup ist einer der häufigsten Performance-Killer in der Praxis — und einer, der im Ausführungsplan sofort sichtbar ist. Das Muster: SQL Server findet relevante Zeilen über einen Non-Clustered Index, muss dann aber für jede gefundene Zeile noch einmal in den Clustered Index nachschlagen, um fehlende Spalten zu holen.

    Warum Key Lookups entstehen

    Stell dir eine Tabelle mit 5 Millionen Kunden vor. Du hast einen Index auf Nachname + Vorname für Suchen. Eine Query sucht nach Kunden aus Berlin und gibt Nachname, Vorname, E-Mail und Telefonnummer aus. Der Index auf Nachname + Vorname ist für die Suche optimal — aber er enthält keine E-Mail und keine Telefonnummer. Also: Index Seek findet die passenden Zeilen, dann Key Lookup für jede Zeile, um die fehlenden Spalten aus dem Clustered Index zu holen.

    Für 10 gefundene Zeilen: 10 Key Lookups — harmlos. Für 100.000 gefundene Zeilen: 100.000 Key Lookups — jeder ein einzelner B-Tree-Zugriff auf den Clustered Index. Das können Millionen von logischen Reads sein, aus einem einzigen Query.

    Das Praxisbeispiel mit T-SQL

    -- Tabelle: Kunden (5 Mio. Zeilen)
    -- Index: IX_Kunden_Name auf (Nachname, Vorname)
    -- Clustered: PK_Kunden auf KundenID

     

    -- Diese Query produziert Key Lookups:
    -- Index Seek findet Zeilen nach Name,
    -- Key Lookup holt Email und Telefon aus dem Clustered Index nach.
    SELECT Nachname, Vorname, Email, Telefon
    FROM Kunden
    WHERE Stadt = N'Berlin';

     

    -- Im Ausführungsplan sichtbar:
    -- [Index Seek] --> (Key Lookup) --> [SELECT]
    -- Key Lookup trägt 80% der geschätzten Kosten.

     

    -- Lösung: Covering Index mit INCLUDE für die fehlenden Spalten.
    -- Die INCLUDE-Spalten werden nur auf Blattebene gespeichert,
    -- nicht in den inneren B-Tree-Knoten — schlanker Index,
    -- aber vollständige Abdeckung der Query.
    CREATE INDEX IX_Kunden_Stadt_Covering
    ON Kunden (Stadt)
    INCLUDE (Nachname, Vorname, Email, Telefon);
    -- Jetzt: Index Seek reicht aus, kein Key Lookup mehr.
    -- Logische Reads: von ~500.000 auf ~2.000 gesunken.

    Nach dem Erstellen des Covering Index verschwindet der Key Lookup aus dem Plan. Die Logischen Reads sinken typischerweise um 90-99%. Ausführliche Erklärungen zu Covering Indizes, INCLUDE-Klausel und Index-Design findest du in Kapitel 17 (Index-Strategie & Wartung).

     

    Hinweis: Ab wann ist ein Key Lookup ein Problem?

    Faustregel: Wenn der Key Lookup mehr als 1% der gefilterten Zeilen betrifft und die Query häufig ausgeführt wird, ist ein Covering Index eine Überlegung wert.

    Im Ausführungsplan: Wenn der Key Lookup-Operator > 30% der geschätzten Kosten trägt, sollte er als erstes betrachtet werden.

    Missing Index Hint im Plan prüfen: SQL Server zeigt oft direkt im grafischen Plan an, welcher Index helfen würde. Diese Hinweise sind nicht immer optimal, aber ein guter Ausgangspunkt.

     

    Implicit Conversion: Der stille Killer

    Es gibt Probleme, die laut auftreten und sofort sichtbar sind. Und dann gibt es Implicit Conversion — das Problem, das leise im Hintergrund einen Index nach dem anderen außer Kraft setzt, ohne, dass jemand bemerkt warum die Queries plötzlich langsam sind.

    Was Implicit Conversion ist

    Implicit Conversion entsteht, wenn SQL Server einen Datentyp-Mismatch zwischen einem Filterprädikat und der gefilterten Spalte ausgleichen muss. Beispiel: Eine Spalte ist als VARCHAR(100) definiert, das Filterprädikat übergibt NVARCHAR. SQL Server muss jetzt entweder den Parameter konvertieren (gut — einmal) oder die Spalte konvertieren (schlecht — für jede Zeile in der Tabelle).

    Das Problem: In vielen Fällen konvertiert SQL Server die Spalte, nicht den Parameter. Das bedeutet: Für jede Zeile in der Tabelle wird die Konvertierung berechnet, bevor der Filter angewendet werden kann. Ein Index auf dieser Spalte ist nun nutzlos — ein Index Seek setzt voraus, dass die Werte im Index direkt mit dem Prädikat vergleichbar sind. Stattdessen: vollständiger Index Scan oder schlimmer noch Table Scan.

    Die häufigsten Fälle

     

    Spaltentyp

    Parametertyp

    Ergebnis

    Typische Ursache

    VARCHAR

    NVARCHAR

    Index Scan statt Index Seek

    ORM übergibt immer NVARCHAR; .NET strings sind Unicode

    INT

    BIGINT oder VARCHAR

    Index Scan

    Applikation übergibt Wert als String oder mit falschem Typ

    DATE

    DATETIME oder DATETIME2

    Keine Konvertierung nötig — aber Achtung bei DATETIME2 vs. DATETIME

    Datumsberechnungen mit falscher Präzision

    DECIMAL(10,2)

    FLOAT

    Präzisionsverlust möglich, Index oft weiter nutzbar

    Berechnungen in der Applikation mit FLOAT-Arithmetik

    CHAR(10)

    VARCHAR(20)

    Meist kein Problem, aber Längenunterschiede prüfen

    Altes Schema mit CHAR, moderne App mit VARCHAR

    Tabelle 15.4: Häufige Implicit Conversion-Muster

     

    Trendforge Digital GmbH: ORM und die NVARCHAR-Falle

    Trendforge Digital hat eine sauber konfigurierte Instanz — gute Hardware, korrekte MAXDOP-Einstellungen, durchdachte Indexstrategien. Und trotzdem schleichen sich in der Produktion immer wieder Performance-Probleme ein. Das Diagnosemuster bei Trendforge in Kapitel 34: Im Ausführungsplan taucht regelmäßig das Warning "Type conversion in expression may affect CardinalityEstimate" auf, verbunden mit Index Scans auf einer 50-Millionen-Zeilen-Tabelle (Kundentransaktionen).

    Der Übeltäter: Entity Framework übergibt String-Parameter standardmäßig als NVARCHAR(4000). Die Spalte KundenNr ist VARCHAR(20). SQL Server kann den Index auf KundenNr nicht per Seek nutzen — und scannt stattdessen die gesamte Tabelle. Mit 50 Millionen Zeilen dauert das im schlechtesten Fall 8-12 Sekunden statt 0,3 ms per Seek. Faktor 30.000.

    Die Lösung bei Trendforge war auf zwei Ebenen: Einerseits explizite Typ-Annotationen im ORM (CHAR/VARCHAR statt Unicode für Legacy-Spalten), andererseits mittelfristig eine Datenbankschema-Bereinigung mit konsequentem NVARCHAR für alle Textspalten — damit die ORM-Defaults stimmen. Mehr zu ORM-Mustern und ihren Auswirkungen folgt in Kapitel 30.

    -- Implicit Conversion erkennen: Suche nach Warnings im Actual Execution Plan
    -- oder direkt per DMV:

     

    -- Queries mit Implicit Conversion im Plan Cache finden.
    -- plan_text enthält "CONVERT_IMPLICIT", wenn SQL Server
    -- eine implizite Typkonvertierung vornimmt.
    SELECT TOP 20
        qs.execution_count,
        qs.total_worker_time / qs.execution_count AS avg_cpu_us,
        qs.total_logical_reads / qs.execution_count AS avg_reads,
        SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
            ((CASE qs.statement_end_offset
                WHEN -1 THEN DATALENGTH(st.text)
                ELSE qs.statement_end_offset
            END - qs.statement_start_offset)/2)+1) AS query_text,
        qp.query_plan
    FROM sys.dm_exec_query_stats qs
    CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
    CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
    -- CONVERT_IMPLICIT im XML-Plan ist das Zeichen für implizite Konvertierung
    WHERE CAST(qp.query_plan AS NVARCHAR(MAX)) LIKE N'%CONVERT_IMPLICIT%'
    ORDER BY qs.total_logical_reads DESC;

    Verweise: Datentypen und ihre Performance-Implikationen behandelt Kapitel 20 (Datenbankdesign & Datentypen) umfassend. ORM-spezifische Muster und wie man Entity Framework dazu bringt, die richtigen Typen zu übergeben, sind Thema in Kapitel 30 (ORM & Applikationsdesign).

    sys.dm_exec_query_stats: Die teuersten Queries finden

    Wenn der Server langsam ist und du nicht weißt, womit er sich beschäftigt, ist sys.dm_exec_query_stats dein erster Anlaufpunkt. Diese DMV enthält aggregierte Laufzeitstatistiken für alle Queries im Plan Cache: wie oft ausgeführt, wie viel CPU insgesamt, wie viele logische Reads, wie viel Elapsed Time.

    Worauf du achtest: Total vs. Average

    Der häufigste Fehler bei der DMV-Analyse: Nur nach total_worker_time sortieren und die erste Query als Schuldige behandeln. Das greift zu kurz. Eine Query die 10 Millionen Mal pro Tag ausgeführt wird und jedes Mal 50 µs CPU kostet, erscheint in der total-Ansicht möglicherweise schlimmer als eine Query die einmal täglich läuft und 45 Minuten CPU frisst — obwohl letztere das eigentliche Problem ist.

    Die Regel: Immer beide Perspektiven prüfen. total / execution_count = Durchschnitt pro Ausführung. Hohe Gesamt-CPU bei niedrigem Durchschnitt → häufig ausgeführte Query mit Optimierungspotenzial. Hoher Durchschnitt → einzelne teure Query, die direkte Aufmerksamkeit braucht.

    -- Top 10 teuerste Queries nach CPU-Gesamtverbrauch.
    -- Gibt sowohl Total- als auch Durchschnittswerte aus,
    -- damit man nicht in die "total"-Falle tappt.
    SELECT TOP 10
        qs.execution_count,

     

        -- Gesamte CPU-Zeit in Millisekunden (worker_time ist in Mikrosekunden)
        qs.total_worker_time / 1000 AS total_cpu_ms,

     

        -- Durchschnittliche CPU-Zeit pro Ausführung in Millisekunden
        qs.total_worker_time / qs.execution_count / 1000 AS avg_cpu_ms,

     

        -- Gesamte logische Reads (Buffer Pool-Zugriffe)
        qs.total_logical_reads,

     

        -- Durchschnittliche logische Reads pro Ausführung
        qs.total_logical_reads / qs.execution_count AS avg_logical_reads,

     

        -- Gesamte Elapsed Time in Millisekunden
        qs.total_elapsed_time / 1000 AS total_elapsed_ms,

     

        -- Durchschnittliche Elapsed Time — bei Parallelismus
        -- ist elapsed < cpu_time möglich, weil mehrere Threads parallel arbeiten
        qs.total_elapsed_time / qs.execution_count / 1000 AS avg_elapsed_ms,

     

        -- Zeitpunkt der letzten Ausführung
        qs.last_execution_time,

     

        -- Query-Text aus dem SQL Handle
        SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
            ((CASE qs.statement_end_offset
                WHEN -1 THEN DATALENGTH(st.text)
                ELSE qs.statement_end_offset
            END - qs.statement_start_offset)/2)+1) AS query_text

     

    FROM sys.dm_exec_query_stats qs
    CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st

     

    -- Nach Gesamt-CPU sortieren — für den zweiten Blick
    -- die Sortierung auf avg_cpu_ms ändern
    ORDER BY qs.total_worker_time DESC;

    Den Ausführungsplan aus dem Cache holen

    Wenn du eine teure Query gefunden hast, willst du ihren Ausführungsplan sehen — nicht den Estimated Plan (der basiert auf den Statistiken zum Zeitpunkt der Kompilierung), sondern den gecachten Plan. Den holst du per sys.dm_exec_query_plan:

    -- Ausführungsplan für eine bestimmte Query aus dem Cache holen.
    -- plan_handle kommt aus sys.dm_exec_query_stats.
    SELECT
        qs.execution_count,
        qs.total_worker_time / 1000 AS total_cpu_ms,
        qs.total_logical_reads,

     

        -- query_plan ist XML — in SSMS klickbar, öffnet grafischen Plan
        qp.query_plan,

     

        -- Vollständiger Query-Text
        st.text AS full_batch_text

     

    FROM sys.dm_exec_query_stats qs
    CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
    CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
    WHERE qs.total_worker_time > 5000000 -- Nur Queries mit > 5 Sekunden Gesamt-CPU
    ORDER BY qs.total_worker_time DESC;
    -- Hinweis: Der Plan aus dem Cache ist der Estimated Plan —
    -- nicht der Actual Plan. Für Actual Plan muss die Query
    -- mit "Include Actual Execution Plan" in SSMS erneut ausgeführt werden.

     

    Tipp: Logical Reads als primäre Messgröße

    Logical Reads (Seitenlesungen aus dem Buffer Pool) sind die wichtigste Kennzahl für Query-Effizienz. Physical Reads (von Disk) hängen vom Cache-Zustand ab und variieren stark. Logical Reads sind deterministisch — gleiche Daten, gleiche Query, gleiche Logical Reads.

    Faustregel: Eine Query mit 100.000 Logical Reads pro Ausführung ist verdächtig. Eine mit 10.000.000 ist ein Problem. Eine mit > 100.000.000 ist ein Notfall.

    SET STATISTICS IO ON vor der Query zeigt Logical Reads pro Tabelle — unverzichtbar für die detaillierte Diagnose.

     

    SET STATISTICS IO und SET STATISTICS TIME: Die Lupe für einzelne Queries

    Während sys.dm_exec_query_stats einen Überblick über alle Queries gibt, brauchst du für die Tiefenanalyse einer einzelnen Query ein anderes Werkzeug: SET STATISTICS IO und SET STATISTICS TIME. Beide sind in SSMS einfach zu aktivieren und liefern direkt umsetzbare Informationen.

    -- STATISTICS IO und TIME für eine einzelne Query aktivieren.
    -- Gibt nach der Ausführung im Meldungs-Tab detaillierte
    -- IO- und Zeitstatistiken aus.
    SET STATISTICS IO ON;
    SET STATISTICS TIME ON;

     

    -- Deine Query hier:
    SELECT o.OrderID, c.CustomerName, SUM(od.Quantity * od.UnitPrice) AS OrderTotal
    FROM Orders o
    JOIN Customers c ON o.CustomerID = c.CustomerID
    JOIN OrderDetails od ON o.OrderID = od.OrderID
    WHERE o.OrderDate >= '2024-01-01'
    GROUP BY o.OrderID, c.CustomerName;

     

    SET STATISTICS IO OFF;
    SET STATISTICS TIME OFF;

    Die Ausgabe im SSMS-Meldungs-Tab zeigt für jede beteiligte Tabelle:

  • logical reads: Seitenlesungen aus dem Buffer Pool. Das ist die Zahl, die zählt.
  • physical reads: Seitenlesungen von Disk (nicht im Buffer Pool gefunden). Hängt vom Cache-Zustand ab.
  • read-ahead reads: Seiten, die der Storage Engine proaktiv vorab gelesen hat (Read-Ahead-Mechanismus).
  • lob logical reads: Logical Reads für LOB-Daten (TEXT, NTEXT, VARCHAR(MAX), XML).
  • Bei SET STATISTICS TIME:

  • CPU time: Summierte CPU-Zeit aller Threads (bei Parallelismus: Summe > Elapsed Time möglich).
  • elapsed time: Wanduhrenzeit der Ausführung — das ist die Zeit, die der Nutzer wahrgenommen hat.
  • Der Unterschied zwischen CPU time und elapsed time verrät dir mehr als beide Werte einzeln: Wenn CPU time >> elapsed time ist, läuft die Query parallel (mehrere Threads arbeiten gleichzeitig). Wenn elapsed time >> CPU time ist, wartet die Query auf etwas — IO, Locks, Netzwerk. In letzterem Fall sind die Wait Statistics (Kapitel 9) der nächste Schritt.

    Musterwerk GmbH: vorher/nachher mit STATISTICS IO

    Bei Musterwerk GmbH (Instanz MWSQL01) haben wir eine Produktionsquery analysiert, die regelmäßig für Beschwerden vom Vertriebsteam sorgte — Bestellübersicht lädt zu langsam. STATISTICS IO hat sofort gezeigt:

    -- Vorher: Query ohne Covering Index
    -- Table 'Orders': Scan count 1, logical reads 45.280
    -- Table 'OrderDetails': Scan count 1, logical reads 312.441
    -- Table 'Customers': Scan count 1, logical reads 18.923
    -- CPU time = 2.847 ms, elapsed time = 3.102 ms

     

    -- Nachher: Mit Covering Index auf OrderDetails(OrderID)
    -- INCLUDE (Quantity, UnitPrice)
    -- Table 'Orders': Scan count 1, logical reads 812
    -- Table 'OrderDetails': Scan count 1, logical reads 1.243
    -- Table 'Customers': Scan count 1, logical reads 18.923
    -- CPU time = 203 ms, elapsed time = 241 ms

     

    -- Ergebnis: 93% weniger Logical Reads auf OrderDetails,
    -- CPU-Zeit von 2,8 Sekunden auf 0,2 Sekunden.
    -- Der Index auf Customers wäre der nächste Schritt.

    Plan Forcing: Wenn der Optimizer mit Nachdruck überzeugt werden muss

    Es gibt Situationen, in denen du weißt, welcher Plan korrekt ist — und der Optimizer trotzdem stur einen schlechteren Plan wählt. Manchmal liegt das an veralteten Statistiken (Kapitel 16), manchmal an Parameter Sniffing (Kapitel 18), manchmal an echten Optimizer-Schwächen bei komplexen Queries. In diesen Fällen gibt es Plan Forcing.

    Plan Forcing ist eine Notlösung, keine Dauerstrategie. Wenn du einen Plan forcen musst, hast du eine Ursache, die du noch nicht behoben hast. Behandle Plan Forcing als temporäres Pflaster — und dokumentiere, warum du es eingesetzt hast.

    USE HINT: Der sanfte Weg

    Bevor du einen konkreten Plan erzwingst, probiere USE HINT. Diese Hints erlauben dir, einzelne Optimizer-Verhaltensweisen zu steuern, ohne einen festen Plan zu binden:

    -- USE HINT-Beispiele für gezielte Optimizer-Beeinflussung

     

    -- Legacy Cardinality Estimator erzwingen (SQL 2012-Verhalten)
    -- Einsatz: Wenn eine Query nach Upgrade auf SQL 2016+ plötzlich langsamer ist
    -- und du Zeit brauchst, das eigentliche Problem zu beheben.
    SELECT * FROM Orders WHERE CustomerID = 1234
    OPTION (USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

     

    -- Parallelen Plan verhindern (entspricht MAXDOP 1 für diese Query)
    SELECT * FROM Orders WHERE CustomerID = 1234
    OPTION (USE HINT('DISABLE_OPTIMIZED_PLAN_FORCING'));

     

    -- Hash Join erzwingen — wenn du weißt, dass Nested Loops falsch ist
    SELECT o.OrderID, c.CustomerName
    FROM Orders o
    INNER HASH JOIN Customers c ON o.CustomerID = c.CustomerID;
    -- HASH JOIN als Join-Hint direkt an der JOIN-Klausel

    Query Store für Plan Forcing: Der empfohlene Weg

    Der richtige Weg für dauerhaftes Plan Forcing ist der Query Store — er erlaubt Plan Forcing per GUI oder T-SQL, ohne den Query-Code zu ändern, und dokumentiert automatisch welcher Plan erzwungen wird. Kapitel 19 (Query Store) behandelt Plan Forcing im Detail, inklusive Automatic Tuning als Erweiterung.

    -- Plan Forcing über den Query Store.
    -- Voraussetzung: Query Store ist aktiv (ALTER DATABASE SET QUERY_STORE = ON)

     

    -- Schritt 1: Query ID und Plan ID im Query Store finden
    SELECT
        q.query_id,
        p.plan_id,
        rs.avg_duration / 1000 AS avg_dauer_ms,
        rs.count_executions,
        qt.query_sql_text
    FROM sys.query_store_query q
    JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
    JOIN sys.query_store_plan p ON q.query_id = p.query_id
    JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id
    ORDER BY rs.avg_duration DESC;

     

    -- Schritt 2: Guten Plan für diese Query erzwingen.
    -- plan_id ist die ID des Plans, der gut gelaufen ist.
    EXEC sp_query_store_force_plan
        @query_id = 42,   -- Query ID aus dem Query Store
        @plan_id  = 17;   -- Plan ID des gewünschten Plans

     

    -- Plan Forcing aufheben, wenn Problem behoben:
    EXEC sp_query_store_unforce_plan
        @query_id = 42,
        @plan_id  = 17;

    Diagnose: Query Performance in der Praxis

     

    Warnung: Symptome: Woran erkennst du Query-Performance-Probleme?

    Nutzer berichten über sporadisch langsame Abfragen — nicht immer, aber immer öfter.

    Im Wait Statistics Dashboard (Kapitel 9) dominieren CXPACKET in Kombination mit hohen Signal Waits → parallele Queries sättigen die CPUs.

    sys.dm_exec_query_stats zeigt eine Handvoll Queries mit exorbitant hohem total_worker_time — 80% der CPU-Last kommen von 3 Queries.

    Ausführungsplan-Warnings: "Type conversion" oder "No statistics for column" in mehreren Plänen im Cache.

    Key Lookup Operatoren mit > 50% der Plankosten in häufig ausgeführten Queries.

    Plötzlicher Anstieg der Ausführungszeit ohne Datenvolumenänderung → Plan Regression (Kapitel 16).

     

     

    Tipp: So misst du das: Top-10-Query-Analyse

    Schritt 1: Teuerste Queries nach CPU identifizieren (sys.dm_exec_query_stats mit total_worker_time-Sortierung).

    Schritt 2: Für verdächtige Queries den Actual Execution Plan in SSMS abrufen (Query mit Strg+M ausführen).

    Schritt 3: Plan auf Warnings prüfen (gelbe Ausrufezeichen an Operatoren).

    Schritt 4: Estimated Rows vs. Actual Rows vergleichen — große Diskrepanzen zeigen Statistikprobleme.

    Schritt 5: SET STATISTICS IO ON für detaillierte Logical Reads pro Tabelle.

    Code: Die vollständige Top-10-Query mit Plan-Abruf steht weiter oben in diesem Kapitel — direkt einsetzbar.

     

     

    Hintergrund: Typische Fehlinterpretationen im Ausführungsplan

    "Estimated = 1, Actual = 1.000.000 ist ein Bug" — Nein. Das ist eine veraltete Statistik. SQL Server hat mit den verfügbaren Informationen eine Schätzung getroffen. Kapitel 16 erklärt wie man Statistiken aktuell hält und wann der Cardinality Estimator systematisch falsch schätzt.

    "Parallelismus ist immer besser" — Falsch. Für OLTP-Queries mit kleinen Datenmengen kostet Parallelismus mehr als er spart. CXPACKET im Wait Stats bedeutet nicht automatisch "MAXDOP erhöhen" — manchmal bedeutet es genau das Gegenteil.

    "Table Scan ist immer schlecht" — Falsch. Eine 500-Zeilen-Lookup-Tabelle wird schneller gescannt als per Index gesucht. Ein Table Scan auf einer 10-GB-Tabelle für eine Punkt-Suche ist ein Problem. Kontext zählt.

    "Key Lookup = Index fehlt" — Oft, aber nicht immer. Manchmal ist es günstiger, einen Key Lookup zu tolerieren als einen breiten Covering Index zu pflegen, der alle Schreiboperationen verlangsamt.

    "Merge Join ist besser als Hash Join, weil er kein Memory braucht" — Stimmt nur, wenn die Daten ohnehin sortiert sind. Wenn SQL Server erst sortieren muss, ist Hash Join oft günstiger.

     

     

    Praxisbeispiel: Erste Gegenmaßnahmen bei Query-Performance-Problemen

    1. Missing Index Hints im Actual Execution Plan prüfen: SQL Server markiert direkt im Plan, wenn ein Index fehlt. Ist kein Ersatz für durchdachtes Index-Design, aber ein guter erster Schritt.

    2. SET STATISTICS IO für die verdächtige Query ausführen und Logical Reads identifizieren. > 100.000 Reads pro Ausführung → hohe Priorität.

    3. Actual Execution Plan mit Warnings prüfen: Implicit Conversion und fehlende Statistiken sind sofort behebbar.

    4. UPDATE STATISTICS für involvierte Tabellen, wenn Estimated vs. Actual Rows stark divergiert. In der Produktion immer in einem Wartungsfenster oder mit FULLSCAN-Option prüfen.

    5. Query Store aktivieren, wenn nicht bereits aktiv — er zeichnet Plan-Regressionen automatisch auf und ermöglicht schnelles Plan Forcing bei akuten Problemen.

    Was nicht funktioniert: MAXDOP einfach erhöhen in der Hoffnung "dann geht es schneller". Wenn der Plan fundamental falsch ist, beschleunigt mehr Parallelismus nur den falschen Plan.

     

    Zusammenfassung

    Query Performance und Ausführungspläne sind kein Hexenwerk — aber sie erfordern systematisches Vorgehen und die Bereitschaft, in die Details zu schauen. Die wichtigsten Erkenntnisse dieses Kapitels:

  • Der Query Optimizer ist ein Cost-Based-System — er trifft Entscheidungen auf Basis von Statistiken, nicht von Gewissheiten. Veraltete Statistiken führen zu falschen Plänen, falsche Pläne führen zu CPU-Explosionen.
  • Estimated vs. Actual Rows ist die wichtigste Information im Ausführungsplan. Eine große Diskrepanz ist fast immer ein Statistikproblem — behandelt in Kapitel 16.
  • Die drei Join-Typen haben unterschiedliche Stärken: Nested Loops für kleine Mengen mit Index, Hash Join für große Mengen ohne passenden Index, Merge Join, wenn die Daten ohnehin sortiert sind. Den falschen Join-Typ für die falsche Datenmenge zu wählen, kostet leicht Faktor 1.000 in der Ausführungszeit.
  • Parallelismus ist ein Werkzeug, kein Allheilmittel. CXPACKET-Waits können normales Parallelisierungsartefakt sein — oder ein Zeichen, dass zu viele Queries um zu wenige Threads konkurrieren. Kontext aus Kapitel 9 (Wait Statistics) ist entscheidend.
  • Key Lookups entstehen, wenn Non-Clustered Indizes nicht alle benötigten Spalten abdecken. Die Lösung ist ein Covering Index (INCLUDE-Klausel) — Kapitel 17 behandelt das im Detail.
  • Implicit Conversion macht Indizes unsichtbar für den Optimizer. Der häufigste Fall: ORM übergibt NVARCHAR, Spalte ist VARCHAR — Index wird nicht genutzt, Table Scan folgt.
  • sys.dm_exec_query_stats ist der Einstiegspunkt für die Identifikation teurer Queries. Immer Total- und Durchschnittswerte prüfen — nicht nur nach Gesamtwerten sortieren.
  • SET STATISTICS IO liefert Logical Reads pro Tabelle — die wichtigste Kennzahl für Query-Effizienz, deterministisch und unabhängig vom Cache-Zustand.
  • Dieses Kapitel hat den Boden bereitet für eines der komplexesten und zugleich praxisrelevantesten Themen in der SQL Server Performance: Was passiert, wenn der Optimizer systematisch falsch schätzt? Nicht zufällig, sondern wiederholt und vorhersehbar? Das ist das Thema von Kapitel 16 — Statistiken, Cardinality Estimator und Plan Regression. Wenn du nach dem Lesen dieses Kapitels einen Ausführungsplan mit Diskrepanzen zwischen Estimated und Actual Rows siehst und weißt, dass die Antwort in den Statistiken liegt — dann war dieses Kapitel erfolgreich.

     

    Ausblick: Kapitel 16 — Statistiken, Cardinality Estimator & Plan Regression

    Kapitel 16 geht tiefer: Wie funktioniert der Cardinality Estimator intern? Was hat sich mit SQL Server 2014 verändert (CE 120 vs. CE 70)? Warum schätzt SQL Server bei bestimmten Prädikaten systematisch zu niedrig — und was kann man dagegen tun? Und: Was ist Plan Regression, wie erkennst du sie, und wie nutzt du den Query Store um einen einmal guten Plan dauerhaft zu halten? Nach Kapitel 15 hast du gelernt wie man Pläne liest. Nach Kapitel 16 verstehst du, warum sie gut oder schlecht sind.

     

    Abb. 1: Plan-Operatoren im Überblick

     

    Abb. 2: Join-Algorithmen: Nested Loop, Hash, Merge

     

    Abb. 3: Parallelismus und Exchange-Operatoren

     

    Kapitel 16