Kurz gesagt
- Das Werkzeug spart Sammelarbeit. sp_BlitzPlanCompare bringt Parameter, Indexe, Statistiken, Einstellungen, Pläne und Wartezustände zweier Server in eine priorisierte Liste.
- Es hat im Lab die richtigen Spuren gefunden: unterschiedliche Compile-Werte, geänderte Statistikköpfe, den ersten Operator mit abweichender Zeilenmenge (500 statt 3.000) und eine aktuelle Sperre.
- Eine Spur ist noch keine Ursache. Ein unbenutzter Index bekam Priorität 1, ebenso ein erzwungener Plan einer ganz anderen Prozedur.
- Die Ursache belegt erst die Gegenprobe. Beim selben Laufzeitwert las der Plan, der für einen seltenen Kunden kompiliert war, rund 270.000 Seiten, der andere rund 2.800.
MerksatzEin Unterschied ist eine Spur, keine Ursache. Zur Ursache wird er erst durch eine Gegenprobe.
Gleiche Abfrage, anderes Tempo
Der Anruf kommt meist so: „Im Test läuft das in einer Sekunde, in Produktion dauert es eine Minute.“ Der SQL-Text ist derselbe. Wo fängt man an? Bei den Indexen, bei den Statistiken, bei den Parametern, bei den Servereinstellungen oder bei einer Sperre, die gerade jemand hält?
Wer das von Hand untersucht, sammelt Informationen an vielen Stellen: Plan Cache, Indexdefinitionen, Statistikköpfe, Datenbankoptionen, Wartezustände. Und dann legt man alles nebeneinander. Genau diese Sammelarbeit nimmt sp_BlitzPlanCompare ab. Brent Ozar hat das Werkzeug im Oktober 2026 als Teil des kostenlosen First Responder Kit vorgestellt (Brents Vorstellung). Danke dafür an Brent und alle, die am First Responder Kit mitarbeiten.
Wir wollten mehr wissen als „es kommt eine Liste“. Deshalb haben wir in einem eigenen Lab gezielt Unterschiede erzeugt und geprüft, welche das Werkzeug findet und wie man sie liest. Dabei stoßen wir auf drei verbreitete Fehlannahmen:
„Was ganz oben steht, ist die Ursache.“ Die Priorität sortiert Spuren. Sie prüft nicht, ob ein Unterschied die langsame Ausführung erklärt.
aufgelöst in: Priorität ist keine Diagnose ↓„Gleiche Abfrage, gleicher Plan, gleiche Arbeit.“ Derselbe Plan kann je nach Parameter sehr unterschiedlich viel lesen.
aufgelöst in: Fall 1 ↓„Langsamer heißt schlechterer Plan.“ Ein guter Plan kann trotzdem warten, zum Beispiel auf eine Sperre.
aufgelöst in: Fall 4 ↓
Jeder der folgenden Fälle löst eine dieser Annahmen auf.
Das Modell: vier Ebenen eines fairen Vergleichs
Bevor wir Befunde lesen, brauchen wir eine Ordnung. Ein Ausführungsplan ist der Arbeitsweg, den SQL Server für eine Abfrage wählt: zum Beispiel Zeilen über einen Index suchen, Tabellen verknüpfen, summieren. Der Optimizer wählt diesen Weg beim Kompilieren, mit den Informationen, die er in diesem Moment hat. Später wird der Plan ausgeführt, unter den Bedingungen, die dann gerade herrschen. Das sind zwei verschiedene Zeitpunkte.
Daraus ergeben sich vier Ebenen, auf denen sich eine schnelle und eine langsame Seite unterscheiden können:
- 1Kontext Vergleichen wir wirklich dasselbe Statement, in derselben Datenbank, mit denselben Sitzungseinstellungen?
- 2Kompilierung Hatte der Optimizer auf beiden Seiten dieselben Zutaten, also Parameterwerte, Indexe, Statistiken, Schätzmodell und Optionen?
- 3Plan Welchen Arbeitsweg hat er jeweils gewählt, und wie viele Zeilen sind tatsächlich durch die einzelnen Schritte geflossen?
- 4Laufbedingungen Unter welchen Umständen lief der Plan: Sperren, Speicherzuteilung, CPU, I/O?
Die Ebenen bauen aufeinander auf: Die Kompilierung bestimmt den Plan, der Plan bestimmt die Arbeit. Die Laufbedingungen wirken von der Seite auf die Dauer, ohne den Plan zu ändern. sp_BlitzPlanCompare sammelt Informationen aus allen vier Ebenen. Unsere Aufgabe bleibt, jeden Befund seiner Ebene zuzuordnen und zu fragen, ob er die beobachtete Langsamkeit erklären kann. Microsofts Leitfaden für den Vergleich zweier Server folgt derselben Logik.
Vier Ebenen, zwei Seiten
Jede Ebene wird waagerecht verglichen. Tippen Sie auf eine Ebene für Beispiele und die passenden Fälle.
schnellz. B. Test
langsamz. B. Produktion
ArbeitDauer
ArbeitDauer
1Kontext Dasselbe Statement?
Beispiele: derselbe SQL-Text, dieselbe Datenbank, dieselben Sitzungseinstellungen wie ARITHABORT.
Im Artikel: Andere Datenbanknamen · Zwei Cachekopien
2Kompilierung Dieselben Zutaten?
Beispiele: Parameterwerte beim Kompilieren, Indexe, Statistiken, Schätzmodell (Cardinality Estimator), Datenbankoptionen.
Im Artikel: Fall 1 · Parameter · Fall 2 · Statistik · Unbenutzter Index
3Plan Derselbe Weg, dieselben Mengen?
Beispiele: Index Seek mit Key Lookup oder Scan, geschätzte und tatsächliche Zeilen je Schritt.
Im Artikel: Fall 3 · Zeilenmengen · Lookups oder Scan
4Laufbedingungen Dieselben Umstände?
Beispiele: Sperren, Speicherzuteilung (Memory Grant), CPU, I/O.
Im Artikel: Fall 4 · Blocking · Drei Uhren
Der erste Lauf: Snapshot mitnehmen, drüben vergleichen
Die Prozedur liegt im Repository des First Responder Kit unter OptionalScripts. Für den einfachsten Weg wird sie auf beiden Instanzen installiert. Auf der ersten Seite nennen wir die Abfrage, um die es geht, hier eine Stored Procedure:
-- Seite 1 (z. B. Produktion): Snapshot erzeugen
-- Vor dem Einsatz auf Produktion: Code lesen und mit dem Betrieb abstimmen (Hinweis unten)
EXEC master.dbo.sp_BlitzPlanCompare
@StoredProcName = N'dbo.usp_Orders',
@DatabaseName = N'PlanCompareLab';
Die Ergebnisspalte CallStack enthält einen fertigen Aufruf für die andere Seite. Darin steckt ein XML-Snapshot mit Plan und Metadaten. Diesen Aufruf führen wir auf der zweiten Instanz aus. Sie vergleicht den mitgebrachten Snapshot („Remote“) mit ihrem eigenen Stand („Local“) und liefert zwei Ergebnisse: die priorisierte Liste der Unterschiede und die beiden verglichenen Pläne. Eine Verbindung zwischen den Servern ist dafür nicht nötig. Wer bereits einen Linked Server mit RPC OUT hat, kann beide Seiten auch in einem Aufruf vergleichen; beide Wege haben im Lab funktioniert.
Ein praktischer Stolperstein: Der Aufruf ist ein langer Text. SSMS kürzt lange Textzellen in der Ergebnisansicht. Ein abgeschnittener Snapshot ist ein Übertragungsfehler, kein Befund. Vor dem Kopieren deshalb die Grenze für Textergebnisse prüfen und dem in der Hilfe des Werkzeugs beschriebenen Weg folgen.
Der Snapshot reist
langsam Seite 1 · z. B. Produktion
Schritt 1 sp_BlitzPlanCompare für dbo.usp_Orders aufrufen
Spalte CallStack: fertiger Aufruf mit Snapshot
Wird über den Linked Server direkt gelesen
In SSMS abgeschnitten? Dann ist das ein Übertragungsfehler, kein Befund.
schnell Seite 2 · z. B. Test
Schritt 2 Aufruf aus CallStack hier ausführenEin Aufruf vergleicht beide Seiten
Fall 1 · 2KompilierungDer erste Parameter prägt den Plan
Aufbau Eine Tabelle mit 100.000 Aufträgen. 90.000 gehören zu einem großen Kunden, ein seltener Kunde hat zehn. Ein Index auf der Kundennummer hilft beim Finden; den Betrag, der summiert wird, muss SQL Server aber für jede gefundene Zeile in der Tabelle nachschlagen (Key Lookup).
Zehn Lookups oder neunzigtausend
Index
Kundennummer
Key Lookup
je Treffer einmal
Tabelle
mit Betrag
Seek + Key Lookup90.000einzelne Nachschlagevorgänge
Scan1 ×jede Seite der Tabelle einmal, egal wie viele Treffer
Arbeit wächst mit den Treffern.
Für zehn Treffer ist das ideal: zehn Mal suchen, zehn Mal nachschlagen, fertig. Für 90.000 Treffer heißt derselbe Weg: 90.000 einzelne Nachschlagevorgänge. Dann ist es günstiger, die Tabelle einmal ganz zu lesen. „Index Seek“ ist also kein Qualitätsurteil; es kommt auf die Menge an, die danach bearbeitet wird.
SQL Server kompiliert eine parametrisierte Prozedur mit dem Wert, der beim ersten Aufruf ankommt, und verwendet den Plan danach wieder. Das nennt man Parameter Sniffing, und es ist meistens nützlich: Der Optimizer plant für einen konkreten Bedarf statt für einen Durchschnitt. Heikel wird es, wenn ein Plan für zehn Zeilen später für 90.000 wiederverwendet wird (Microsoft zur Parameterempfindlichkeit).
Erwartung Wir kompilieren die Prozedur einmal für den seltenen und einmal für den großen Kunden und führen danach jeweils beide Werte aus. Vier Kombinationen. Welche liest am meisten?
Vorhersage: Welche Kombination liest am meisten?
Ihr Tipp: Wählen Sie die Zelle, die am meisten liest.
kompiliert fürseltenen Kunden@CustomerID = 42 · Lookup-Plan
kompiliert fürgroßen Kunden@CustomerID = 1 · Scan-Plan
ausgeführt fürseltenen Kunden10 Treffer
32logische Reads
10 Lookups2.790logische Reads
ein Scanausgeführt fürgroßen Kunden90.000 Treffer
270.158logische Reads
90.000 Lookups2.790logische Reads
ein ScanBeobachtung Der große Kunde mit einem Plan, der für den seltenen kompiliert wurde, braucht rund 270.000 logische Seitenzugriffe. Mit dem Plan für den großen Kunden sind es rund 2.800, fast hundertmal weniger, bei identischem Ergebnis. Umgekehrt ist für den seltenen Kunden gerade der Lookup-Plan günstig: 32 statt 2.790 Zugriffe. SQL Server 2022 und 2025 lieferten exakt dieselben Zahlen.
| Kompiliert für | Ausgeführt für | Logische Reads (Median, 5 warme Läufe) |
|---|---|---|
| seltenen Kunden | seltenen Kunden | 32 |
| seltenen Kunden | großen Kunden | 270.158 |
| großen Kunden | seltenen Kunden | 2.790 |
| großen Kunden | großen Kunden | 2.790 |
Logische Reads zählen Seitenzugriffe der Engine, auch auf Seiten, die schon im Speicher liegen. Sie sind keine Plattenzugriffe. Für den Vergleich derselben Abfrage auf demselben Datenbestand sind sie aber ein sehr stabiles Maß für Arbeit.
Was das Werkzeug zeigte sp_BlitzPlanCompare meldete die unterschiedlichen Compile-Werte (@CustomerID = 1 gegenüber 42) und schrieb einen Reproducer mit beiden Aufrufen dazu. Das ist genau die richtige Spur.
Folgerung Die Ursache belegt erst die Kreuzprobe: gleicher Laufzeitwert, nur der Compile-Wert geändert, Arbeit gemessen. Was nicht folgt: ein Rezept. OPTION (RECOMPILE), ein abdeckender Index oder ein anderer Weg muss sich an der echten Arbeitslast, an der Schreiblast und am Betriebsaufwand messen lassen.
Zwei Details für die Praxis. Der ausgegebene Reproducer ist eine Vorlage: Er stellt nicht den ursprünglichen Kompilierzustand her, ein erneuter Aufruf kann einfach den vorhandenen Plan wiederverwenden. Auf SQL Server 2025 schrieb er die Zahlenwerte außerdem als (1) und (42); so scheiterte der Aufruf mit Fehler 102, ohne Klammern lief er. Und: Wir haben bewusst Compatibility Level 140 genutzt. Ab SQL Server 2022 kann Parameter Sensitive Plan Optimization für unterschiedliche Wertebereiche absichtlich mehrere Pläne führen (Microsoft zu PSP); mehrere Pläne sind dort kein Fehler.
Fall 2 · 2KompilierungAktuell heißt nicht gleich verteilt
Der Optimizer kennt die Treffermenge nicht im Voraus, er schätzt sie mit Statistiken. Ein Statistikobjekt hat einen Kopf (wann aktualisiert, wie viele Zeilen, welcher Stichprobenumfang, wie viele Änderungen seitdem) und ein Histogramm, das die Verteilung der Werte mit höchstens 200 Stufen beschreibt (Microsoft zu Statistikkopf und Histogramm).
Erwartung Wenn wir auf einer Seite Daten löschen und die Statistik neu erstellen, sollte das Werkzeug das bemerken.
Änderung Wir haben auf einer Seite 5.000 Zeilen entfernt und danach mit FULLSCAN aktualisiert.
Beobachtung sp_BlitzPlanCompare zeigte die neue Zeilenzahl (95.000 statt 100.000) und die geänderten Statistikköpfe.
Folgerung und Grenze Das Werkzeug vergleicht die Köpfe, nicht die Histogramme. Das ist eine bekannte Grenze dieser frühen Fassung: Brent nennt sie im Kopf des Skripts selbst, der Histogrammvergleich ist für eine spätere Version zurückgestellt. Zwei Statistiken können gleich frisch sein, gleich viele Zeilen beschreiben und trotzdem eine völlig andere Verteilung zeigen. Und zwei unterschiedliche Zeitstempel sind für sich noch kein Problem. Die eigentliche Frage lautet: Passt die Schätzung für das betroffene Prädikat zur tatsächlichen Treffermenge? Der Kopfvergleich führt zu dieser Frage; beantworten muss man sie im Plan (geschätzte gegenüber tatsächlichen Zeilen) oder im Histogramm.
Illustration: gleiche Menge, andere Verteilung
schnell
- Zeilen
- 100.000
- Stichprobe
- 100.000
- Aktualisiert
- gleicher Zeitpunkt
gleichmäßig verteilt
langsam
- Zeilen
- 100.000
- Stichprobe
- 100.000
- Aktualisiert
- gleicher Zeitpunkt
stark schief: ein Kunde mit 90.000 Zeilengleichmäßig verteilt
Lab-Messung Was das Werkzeug tatsächlich gemeldet hat
- 100.000 → 95.000 Zeilenauf einer Seite 5.000 entfernt
- FULLSCANStatistik neu erstellt
- Kopf geändertZeilenzahl und Zeitstempel; sp_BlitzPlanCompare meldet es (Priorität 15)
Auf derselben Ebene liegt das Schätzmodell. Compatibility Level und Cardinality Estimator sind zwei verschiedene Dinge: Mit LEGACY_CARDINALITY_ESTIMATION = ON lief eine Datenbank auf Level 150 im Plan mit CE-Version 70, die Vergleichsseite mit 140. Das Werkzeug zeigte beide Ebenen. Aus der Engineversion allein lässt sich das aktive Schätzmodell also nicht ablesen (Microsoft zum Cardinality Estimator).
Fall 3 · 3PlanWo die Zeilenmengen auseinanderlaufen
Zwei gleich geformte Pläne können sehr unterschiedliche Mengen bearbeiten. Für die Diagnose ist interessant, an welcher Stelle sich der Zeilenfluss zuerst unterscheidet, denn die Schritte darüber arbeiten mit dieser Menge weiter.
Änderung Beide Seiten nutzen denselben parametrisierten Plan; die Bereichsgrenze ist einmal 3.000 und einmal 500.
Beobachtung sp_BlitzPlanCompare markierte den Clustered Index Seek (Knoten 3) mit 500 gegenüber 3.000 tatsächlichen Zeilen als erste auffällige Stelle, auf SQL Server 2022 und 2025 gleich. Im Bild sieht man: Auch der zweite Seek (Knoten 4) liefert 500 statt 3.000 Zeilen, denn derselbe Parameter begrenzt beide Zugriffe, und der Nested Loops darüber übernimmt die Menge. Das Werkzeug nennt die erste Abweichung in seiner Durchlaufreihenfolge, nicht jede.
Zeilenfluss in zwei gleichen Plänen
Local · @N = 500Instanz, auf der der Vergleich läuft
Remote · @N = 3000mitgebrachter Snapshot
Orange umrandet: Knoten 3, die erste vom Werkzeug markierte Mengenabweichung (500 gegenüber 3.000).
Folgerung Das ist eine echte Abkürzung: Statt zwei Pläne Knoten für Knoten zu vergleichen, schaut man auf die markierte Stelle. Sie sagt, wo die Mengen auseinanderlaufen, nicht warum. Anderer Parameter, anderer Datenbestand, anderer Filter? Das ist die nächste Frage. Und die erste Abweichung ist nicht automatisch der teuerste Schritt.
Damit der Vergleich trägt, müssen die Pläne zueinander passen. Bei gleicher Form vergleicht das Werkzeug die einander entsprechenden Knoten; bei unterschiedlicher Form nur den obersten. Echte Zeilenzahlen nutzt es nur, wenn beide Seiten Laufzeitwerte haben, sonst Schätzungen. Bei Prozeduren mit mehreren Statements lohnt deshalb ein Blick darauf, welche Pläne tatsächlich verglichen wurden.
Fall 4 · 4LaufbedingungenGleicher Plan, längere Wartezeit
Erwartung Wenn eine Abfrage plötzlich Sekunden statt Millisekunden braucht, liegt der Gedanke an einen schlechteren Plan nahe.
Änderung Eine schreibende Transaktion hält eine Sperre auf genau der Zeile, die unsere Abfrage lesen will.
Beobachtung Der Leser wartet, der Plan bleibt derselbe. sp_BlitzPlanCompare zeigte eine blockierte Sitzung und den aktuellen Wartetyp LCK_M_S. Die Meldung zu LiveState · BlockedSessions sagt dabei selbst: The slow server may be losing time to blocking, not the plan. Nach dem Zurückrollen des Schreibers war der Leser sofort fertig.
Ein Plan, zwei Läufe
Plan · in beiden Läufen derselbeClustered Index ScanStream Aggregate
Lauf A ohne Sperre
Lauf B ein Schreiber hält eine Sperre
LCK_M_S · wartet ≈ 2,4 sfertigZeit (schematisch)
Folgerung Hier hilft kein neuer Index. Die richtige nächste Frage gilt dem Blocker: Wer hält die Sperre, wie lange, und warum ist die Transaktion so lang? (Mehr dazu im Ratgeber-Weg Blocking und Timeouts.)
Ähnlich ist es bei anderen Ressourcen. Ein Memory Grant ist eine Speicherreservierung für Sortieren oder Hashing. Ist zu wenig Speicher frei, wartet die Abfrage oder lagert nach tempdb aus; ist der Grant viel zu groß, fehlt der Speicher anderen (Microsoft zu Memory Grants). Auch CPU-Zeit und Dauer gehören getrennt betrachtet: Bei parallelen Plänen kann die summierte CPU-Zeit größer sein als die Uhrzeit.
Priorität ist keine Diagnose
Die Ergebnisliste sortiert Unterschiede nach einer im Werkzeug hinterlegten Wichtigkeit. Das ist ein guter Einstieg, solange man jede Zeile mit zwei Fragen liest: Was sagt die Zeile? und Was beweist sie für meine Abfrage? Vier Gegenproben aus unserem Lab zeigen, warum die zweite Frage nötig ist.
Befundkarten zum Umdrehen
Vorderseite: die Ergebniszeile im Original, wie das Werkzeug sie ausgibt (englisch). Umdrehen:Rückseite: was sie beweist und wie die Gegenprobe ausging.
-
2KompilierungAusgabe des Werkzeugs (Original, englisch)
Priority 1 Category Index Setting Present
Object
PlanCompareLab.dbo.Orders.IX_Orders_Amount_UnusedLocalValue present RemoteValue NULL
Finding An index exists on one server but not the other. The optimizer on the side without it couldn't pick the same plan. Create the missing index there to compare apples to apples.
Unbenutzter Index
Was sie beweistDie Indexlisten der Tabelle unterscheiden sich. Nicht, dass die Abfrage den Index verwendet.
GegenprobeDie Abfrage nutzt den Index gar nicht; die Meldung kam trotzdem mit Priorität 1.
-
2KompilierungAusgabe des Werkzeugs (Original, englisch)
Priority 1 Category Index Setting Present
Object
PlanCompareLab.dbo.Orders.IX_Orders_CustomerLocalValue NULL RemoteValue present
Finding An index exists on one server but not the other. The optimizer on the side without it couldn't pick the same plan. Create the missing index there to compare apples to apples.
Priority 1 Category Index Setting Present
Object
PlanCompareLab.dbo.Orders.IX_Orders_Customer_RenamedLocalValue present RemoteValue NULL
Finding An index exists on one server but not the other. The optimizer on the side without it couldn't pick the same plan. Create the missing index there to compare apples to apples.
Nur umbenannt
Was sie beweistDie Indexnamen unterscheiden sich.
GegenprobeTechnisch derselbe Index unter neuem Namen. Verglichen werden Namen, nicht Definitionen.
-
1KontextAusgabe des Werkzeugs (Original, englisch)
Priority 1 Category Object Setting Present
Object
PlanCompareLab.dbo.OrdersLocalValue NULL RemoteValue present
Finding A table referenced by the plan is missing on one server. This will break the query entirely.
Priority 1 Category Object Setting Present
Object
PlanCompareOther.dbo.OrdersLocalValue present RemoteValue NULL
Finding A table referenced by the plan is missing on one server. This will break the query entirely.
Andere Datenbanknamen
Was sie beweistUnter diesem vollständigen Namen gibt es die Objekte auf einer Seite nicht.
GegenprobeDie Datenbank hieß auf der anderen Seite anders. Daraus entstehen scheinbare Lücken bei allen Objekten.
-
2KompilierungAusgabe des Werkzeugs (Original, englisch)
Priority 1 Category ForcedPlan Setting QueryStore
Object
PlanCompareLabLocalValue plan_id=35 hash=0x01B30C54B38BF8D7 RemoteValue NULL
Finding A forced plan exists on one side. Forced plans override the optimizer and almost always cause cross-server divergence.
Fremder Forced Plan
Was sie beweistIn dieser Datenbank ist irgendein Plan erzwungen.
GegenprobeErzwungen war der Plan einer unbeteiligten Prozedur. Für unsere Abfrage war nichts erzwungen.
- Unbenutzter Index, Priorität 1. Wir legten einen Index an, den die Abfrage gar nicht verwendet. Das Werkzeug meldete ihn mit höchster Priorität. Es betrachtet alle Indexe der beteiligten Tabellen.
- Nur umbenannt. Ein technisch gleicher Index unter neuem Namen ergab zwei Zeilen
Index · Present, eine für den alten und eine für den neuen Namen, jeweils nur auf einer Seite vorhanden. Verglichen werden Namen, nicht Definitionen. - Andere Datenbanknamen. Heißt die Datenbank auf beiden Seiten anders, entstehen scheinbare Lücken bei allen Objekten.
- Fremder Forced Plan. Ein im Query Store erzwungener Plan einer unbeteiligten Prozedur in derselben Datenbank erzeugte ebenfalls eine Meldung mit Priorität 1. Für unsere Abfrage war nichts erzwungen.
Daraus folgt keine Kritik am Werkzeug, sondern eine Lesehilfe. Die Index-Meldung schlägt vor, den Index auf der anderen Seite anzulegen, damit beide Seiten wirklich vergleichbar sind. Das ist ein Rat für den Vergleich, kein Befund über die Ursache. Bevor daraus in Produktion ein CREATE INDEX wird, prüfen wir Definition, Nutzung und die tatsächliche Arbeitslast. Und eine fehlende Zeile heißt nicht, dass beide Seiten gleich sind (siehe 25-%-Regel in der Vertiefung).
Vertiefung für den regelmäßigen Einsatz
Vertiefung
Für alle, die das Werkzeug regelmäßig einsetzen wollen. Für den Prüfweg unten sind diese Abschnitte nicht nötig. Zum Prüfweg springen ↓
Drei Uhren, drei Messgrenzen
Das Werkzeug zeigt Zeiten aus drei Quellen, und alle sind in Millisekunden. Sie messen trotzdem verschiedene Dinge:
- Letzter Actual Plan: eine einzelne, die zuletzt verfügbare Ausführung. Ab SQL Server 2019 über
LAST_QUERY_PLAN_STATSabrufbar, wenn die Option eingeschaltet ist. Ist sie aus, nutzt das Werkzeug den geschätzten Plan; fehlende Laufzeitwarnungen beweisen dann nichts (Microsoft zu sys.dm_exec_query_plan_stats). - Query Store: Summen über gespeicherte Zeitintervalle. Das Werkzeug addiert die Wartezeiten zum Plan über alle vorhandenen Intervalle. Ein Zeitraum mit vielen Ausführungen hat dann mehr Gesamtwartezeit, auch wenn jede einzelne schneller war. Fair wird der Vergleich erst mit gleichem Zeitfenster und Ausführungszahl; die Wartekategorien sind außerdem gröber als Wartetypen (Microsoft zu Query-Store-Waits).
- Sitzung: kumulierte Wartezeiten der Verbindung. Im Lab wartete eine Sitzung erst 1,2 Sekunden per
WAITFORund startete dann die Abfrage, die an einer Sperre hängen blieb. Das Werkzeug zeigte dabei die 1.200 ms des früherenWAITFOR. Sie gehören zur Geschichte der Sitzung, nicht zur Abfrage.
Drei Uhren
Letzter Actual Planeine Ausführung
letzter LaufQuery StoreSummen je Intervall
Sitzungkumuliert seit Anmeldung
WAITFOR 1.200 msAbfrage wartet auf SperreWartesumme der Sitzung: enthält den WAITFOREin Befund mit offener Ursache. Für einen gezielt blockierten Lauf von rund 2,4 Sekunden lieferten die Quellen auf unseren Builds Folgendes:
| Quelle | SQL Server 2022 CU27 (16.0.4295.3) | SQL Server 2025 CU9 (17.0.5005.3) |
|---|---|---|
| DMV-Dauer der letzten Ausführung | 2.424 ms | 2.421 ms |
| Explizit erfasster Actual Plan (Dauer / CPU) | 2.423 / 24 ms | 2.420 / 23 ms |
| Letzter Actual Plan (Dauer / CPU) | 1 / 1 ms | 24 / 2.420 ms |
| Ausgabe des Werkzeugs (Dauer / CPU) | 1 / 1 ms | 2.420 / 24 ms |
Auf SQL Server 2025 sind Dauer und CPU im letzten Actual Plan vertauscht. Brent hatte genau diesen Fehler im April beschrieben (Brents Fehlerbericht), und sein Werkzeug tauscht die Werte beim Lesen zurück; das Ergebnis passt. Auf unserem 2022-Build stehen dagegen 1 ms, wo 2,4 Sekunden gemessen wurden; das erklärt der Tausch nicht. Die Ursache haben wir nicht geklärt. Praktische Konsequenz: Zeiten aus dem letzten Actual Plan mit Build-Nummer notieren und gegen eine zweite Quelle prüfen.
Dieselbe Abfrage, zwei Cachekopien
Der Query Hash ist ein Fingerabdruck der Abfragelogik, der Plan Hash einer der Planform. Die Frage „dieselbe Abfrage?“ und die Frage „derselbe Arbeitsweg?“ haben also verschiedene Kennzeichen (Microsoft zu sys.dm_exec_query_stats). Beide reichen nicht immer, um einen Cacheeintrag eindeutig zu bestimmen: Mit unterschiedlicher ARITHABORT-Einstellung entstanden zwei Kopien derselben Prozedur, mit gleichem Query Hash und gleichem Plan Hash. Der Aufruf blieb mehrdeutig; einen Parameter für das Plan Handle hat die getestete Fassung nicht (Microsoft zu Cacheattributen). Das ist übrigens auch die Mechanik hinter dem Klassiker „in SSMS schnell, in der Anwendung langsam“ (Microsoft dazu).
Und: Das Werkzeug sucht den Ausgangsplan im Plan Cache. Nachdem wir den Cache unserer Datenbank geleert hatten, lag der Plan noch im Query Store, wurde aber nicht mehr gefunden. Query Store ist in der getesteten Fassung kein Ersatz für einen verdrängten Cacheplan. Auch das führt das Skript selbst als bekannte Grenze dieser Version.
Die 25-%-Regel
Bei bestimmten Zahlenwerten blendet das Werkzeug kleine Unterschiede aus. Gerechnet wird mit dem größeren der beiden Werte als Basis: 100 gegenüber 125 sind 25/125 = 20 % und werden unterdrückt; 100 gegenüber 150 sind 50/150 ≈ 33 % und werden gemeldet. Genau 25 % werden ebenfalls unterdrückt. Keine Meldung heißt also nicht „gleich“.
25-%-Rechner
Differenz 50 ÷ größerer Wert 150 ≈ 33 % · gemeldet
| A gegenüber B | Differenz ÷ größerer Wert | Ergebnis |
|---|---|---|
| 100 gegenüber 125 | 25/125 = 20 % | unterdrückt |
| 100 gegenüber 150 | 50/150 ≈ 33 % | gemeldet |
| 100 gegenüber 75 | 25/100 = 25 % | unterdrückt (genau 25 %) |
Vom Befund zur Wirkung: der Prüfweg
Am Ende steht ein einfacher Ablauf, der auch ohne das Werkzeug funktioniert:
- Symptom festhalten. Welche Ausführung ist langsam, mit welchem Parameter, wann, aus welcher Anwendung? Dabei Serverdauer und Zeit bis zur Anzeige im Client nicht verwechseln.
- Spuren sammeln. Hier spart sp_BlitzPlanCompare die meiste Zeit.
- Hypothese formulieren. Aus der Zeile
Parameter · CompiledValue(1 gegenüber 42) wird: Der für wenige Treffer kompilierte Plan macht beim großen Kunden zu viele Lookups. Aus „LCK_M_S“ wird: Der Leser wartet auf eine bestimmte Transaktion. - Gegenprobe. Eine begründete Größe ändern, Ergebnisgleichheit und Arbeit messen. Widerlegt? Zurück zur nächsten Spur.
- Wirkung und Kosten. Hilft die Änderung auch für andere Parameter, unter Schreiblast, im Betrieb?
Der Prüfweg, am Beispiel aus Fall 1
- 1SymptomDer Aufruf für den großen Kunden ist in Produktion langsam.
- 2Spurensp_BlitzPlanCompare meldet
Parameter · CompiledValue:@CustomerID1 gegenüber 42. - 3HypotheseDer für wenige Treffer kompilierte Plan macht beim großen Kunden zu viele Lookups.
- 4GegenprobeGleicher Laufzeitwert, nur der Compile-Wert geändert: rund 270.000 statt 2.800 Seitenzugriffe, gleiches Ergebnis.
- 5WirkungGegenmittel an echter Last, Schreiblast und Betriebsaufwand messen, auch für andere Parameter.
Gegenprobe widerlegt? Zurück zu den Spuren, zur nächsten.
Der Snapshot ist dabei Teil der Belege: mit Zeitpunkt aufbewahren und vor einer Weitergabe auf Inhalte prüfen.
Herkunft und Dank
sp_BlitzPlanCompare stammt von Brent Ozar und ist Teil des SQL Server First Responder Kit (MIT-Lizenz). Das Werkzeug macht eine mühsame Aufgabe greifbar: Die Untersuchung beginnt bei einer konkreten Abfrage und legt Parameter, Konfiguration, Tabellenmetadaten und aktuelle Zustände gezielt nebeneinander. Danke an Brent und die Community rund um das First Responder Kit.
Dieser Beitrag ist eine eigene Darstellung mit eigenen Beispielen und Messungen. Brent hat ihn weder geprüft noch freigegeben; Schlussfolgerungen und eventuelle Fehler sind unsere. Für das Werkzeug selbst sind Brents Vorstellung und die Dokumentation im Repository die richtigen Einstiege. Neuere Fassungen können Funktionen und Grenzen verändern.
Methode und Grenzen
Getestete Fassung: OptionalScripts/sp_BlitzPlanCompare.sql, Commit 53c380ed vom 21.10.2026, unverändert.
Aufbau und Messung im Detail
Aufbau: je zwei eigene Instanzen SQL Server 2022 CU27 (16.0.4295.3) und 2025 CU9 (17.0.5005.3) unter Linux in Containern, je 2 CPUs und 2 GB Max Server Memory; ergänzende Funktionsproben auf 2017 und 2019. Startkonfiguration Compatibility Level 140, MAXDOP 2, Cost Threshold 50, Query Store an, LAST_QUERY_PLAN_STATS ab 2019 bewusst eingeschaltet. Rein synthetische Daten: 100.000 Aufträge, 90.000 davon für einen Kunden, zehn je seltenem Kunden.
Messung: Parameterkreuzprobe mit gezielter Neukompilierung je Zelle, danach fünf warme Läufe; Reads in jeder Zelle stabil. CPU- und Dauerwerte sind wegen geteilter VM-Ressourcen nur illustrativ; die Readszahlen tragen die Aussage. 51 datenbasierte Prüfungen über alle Proben bestanden.
Nicht geprüft: Azure SQL Database, Managed Instance, Windows-Engine, eingeschränkte Rechte, ein eigenes Histogramm-Beispiel (das Werkzeug vergleicht in dieser Fassung nur Statistikköpfe), große Plan Caches, Overhead unter realer Last, PSP bei Compatibility Level 160/170, Spills und Memory Grants als eigener Positivfall, viele Sonderfälle mit mehreren Statements. Die Zahlen gehören zu unserem synthetischen Aufbau und sind keine Erwartungswerte für andere Datenbanken.
Lesen Sie die Liste richtig?
Jede Antwort wird sofort aufgelöst, mit Verweis auf die passende Stelle im Beitrag. Ohne Cookies.