Diplom-Biologe | Senior IT-Consultant
|
SH |
Sascha Hess xenosystems.de - IT-Consulting & Data Management |
www.xenosystems.de |
|
|
NOTFALL-KIT – SQL SERVER 2026 |
|
|
SQL Server |
|
|
Langsame Queries |
|
|
Systematische Diagnose und Behebung von SQL-Server-Performance-Problemen |
WAS SIE IN DIESEM KIT ERHALTEN:
|
|
1 |
10 Query-Fallen Die häufigsten Ursachen langsamer Abfragen — mit sofort kopierbaren Fixes |
|
|
2 |
Execution-Plan-Leitfaden Lesen, verstehen, handeln — der komplette Plan-Analyse-Workflow |
|
|
3 |
DMV-Diagnose-Toolkit 7 DMV-Skripte lokalisieren jedes Performance-Problem in Minuten |
|
|
4 |
Rewrite-Rezepte Konkrete Vorher-Nachher-Rewrites für die häufigsten langsamen Muster |
|
|
5 |
30-Tage-Tuning-Plan Strukturiertes Vorgehen vom ersten Symptom bis zur dauerhaften Lösung |
HAFTUNGSAUSSCHLUSS
Alle Skripte, Diagnose-Methoden und Empfehlungen wurden sorgfältig erarbeitet und in realen SQL-Server-Umgebungen erprobt. Da jede Datenbankumgebung individuell ist, übernimmt der Autor keinerlei Haftung für Datenverlust, Systemausfälle, Produktionsunterbrechungen oder sonstige Schäden. Führen Sie alle Änderungen zunächst in einer Testumgebung durch und erstellen Sie vor jedem Eingriff ein vollständiges Backup.
KEINE ERGEBNISGARANTIE
Genannte Performance-Verbesserungen (z. B. Faktor 10–100× schneller) sind Erfahrungswerte aus realen Projekten und keine verbindliche Zusicherung. Tatsächliche Ergebnisse hängen von Hardware, Datenvolumen, Abfragestruktur und Systemauslastung ab.
VERSIONSHINWEIS
Die Inhalte beziehen sich auf SQL Server 2022 (16.x) und SQL Server 2025 (17.x), Stand März 2026. Microsoft veröffentlicht regelmäßig kumulative Updates, die das Verhalten des Query Optimizers, des Execution-Plan-Formats und der DMVs ändern können.
URHEBERRECHT
Dieses Dokument ist für den persönlichen oder betriebsinternen Gebrauch des Käufers lizenziert. Weiterverkauf, Weitergabe an Dritte und öffentliche Veröffentlichung sind ohne schriftliche Genehmigung nicht gestattet.
KEINE VERBINDUNG ZU MICROSOFT
Dieses Kit ist ein unabhängiges Werk und steht in keinerlei Verbindung zur Microsoft Corporation. SQL Server, SSMS und Azure Data Studio sind eingetragene Marken der Microsoft Corporation.
Eine ausführliche Version dieses Haftungsausschlusses befindet sich am Ende dieses Dokuments.
01 Einleitung
Der systematische Weg von Symptom zu Ursache
02 Die 10 Query-Fallen
Häufigste Ursachen langsamer Abfragen — mit Sofort-Fixes
03 DMV-Diagnose-Toolkit
7 Skripte für die vollständige Erstdiagnose
04 Execution Plans lesen
Operatoren, Warnungen, Kosten — der komplette Leitfaden
05 Wait Statistics & Engpässe
Was der Server wirklich tut, während er wartet
06 Query Store nutzen
Flight Recorder, Regressions-Jagd und Plan Forcing
07 Rewrite-Rezepte
Vorher-Nachher für die 5 häufigsten langsamen Muster
08 Index-Chirurgie
Fehlende, ungenutzte und doppelte Indizes gezielt behandeln
09 Parameter Sniffing & Statistiken
Die unsichtbaren Plan-Killer — diagnostizieren und beheben
10 30-Tage-Tuning-Plan
Vom ersten Symptom zur dauerhaften Performance-Verbesserung
01
Eine Abfrage braucht 45 Sekunden. Der ERP-Benutzer beschwert sich. Der DBA öffnet SSMS, schaut sich die Abfrage an, fügt einen Index hinzu — und hofft. Manchmal hilft es. Oft hilft es nur kurz. Gelegentlich macht es andere Abfragen langsamer.
Symptom-basiertes Tuning ist Raten mit Werkzeug. Systematische Diagnose ist Handwerk.
SQL Server stellt mit seinen Dynamic Management Views (DMVs), dem Query Store und dem Execution Plan ein vollständiges Diagnosesystem zur Verfügung — das die meisten DBAs und Entwickler nur zu einem Bruchteil nutzen. Der Grund: Es gibt keinen klar strukturierten Diagnose-Workflow. Dieses Kit liefert genau das.
Der vollständige Diagnose-Workflow in fünf Schritten:
→ Schritt 1 — Lokalisieren: Welche Abfragen kosten am meisten? DMV-Toolkit (Kapitel 3) liefert die Antwort in unter 5 Minuten.
→ Schritt 2 — Klassifizieren: Welche Ursachenkategorie liegt vor? Fehlender Index? Schlechter Plan? Parameter Sniffing? Lock-Contention? (Kapitel 2 und 5)
→ Schritt 3 — Verstehen: Was genau macht der Query Optimizer? Execution-Plan-Analyse (Kapitel 4) zeigt jeden Kostentreiber.
→ Schritt 4 — Beheben: Rewrite, Index, Statistik-Update, Plan Forcing — das richtige Werkzeug für die richtige Ursache (Kapitel 6–9).
→ Schritt 5 — Validieren: War die Maßnahme wirksam? Wurde etwas anderes schlechter? Query Store dokumentiert Vorher/Nachher automatisch.
|
|
WAS SIE IN DIESEM BUCH ERWARTEN DÜRFEN ■ 10 Query-Fallen — Die häufigsten Ursachen langsamer Abfragen mit sofort kopierbaren Diagnose- und Fix-Skripten. ■ DMV-Diagnose-Toolkit — 7 DMV-Skripte, die jedes Performance-Problem in Minuten auf der Prioritätenliste verorten. ■ Execution-Plan-Leitfaden — Lesen, Interpretieren, Handeln — der komplette Workflow für Plan-Analyse. ■ Rewrite-Rezepte — Konkrete Vorher-Nachher-Rewrites für die häufigsten langsamen SQL-Muster. ■ 30-Tage-Tuning-Plan — Strukturiertes Vorgehen vom ersten Symptom bis zur dauerhaften Verbesserung. |
|
|
VORAUSSETZUNGEN Dieses Kit setzt SQL-Grundkenntnisse und Zugriff auf SSMS oder Azure Data Studio voraus. Für Kapitel 6 (Query Store) wird mindestens SQL Server 2016 benötigt, für SQL Server 2025-spezifische Features mindestens Version 17.x. Alle Skripte sind auf SQL Server 2022 und 2025 getestet. |
02
Die Mutter aller Performance-Probleme: SQL Server liest die gesamte Tabelle, weil kein passender Index existiert oder der vorhandene Index nicht genutzt werden kann. Bei 10 Millionen Zeilen entspricht das dem Durchblättern eines Telefonbuchs von vorne.
DIAGNOSE:
|
-- Missing-Index-DMV: Top-Kandidaten nach Nutzen-Score SELECT TOP 10 ROUND(migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans), 0) AS Nutzen_Score, mid.statement AS Tabelle, mid.equality_columns, mid.inequality_columns, mid.included_columns, migs.user_seeks, migs.last_user_seek FROM sys.dm_db_missing_index_group_stats migs JOIN sys.dm_db_missing_index_groups mig ON migs.group_handle = mig.index_group_handle JOIN sys.dm_db_missing_index_details mid ON mig.index_handle = mid.index_handle ORDER BY Nutzen_Score DESC; |
FIX:
|
-- Index anlegen (Equality zuerst, dann Inequality, dann INCLUDE): CREATE NONCLUSTERED INDEX IX_Bestellungen_KundeID_Datum ON dbo.Bestellungen (KundeID, Bestelldatum) INCLUDE (GesamtBetrag, Status) WITH (ONLINE = ON); -- Kein Table-Lock bei Enterprise Edition |
Der Index wird genutzt — aber er enthält nicht alle benötigten Spalten. SQL Server führt für jede gefundene Zeile einen zusätzlichen Lookup im Clustered Index durch. Bei 100.000 Treffern sind das 100.000 einzelne Lesevorgänge.
DIAGNOSE:
|
-- Key Lookups über DMV finden (Top-Kandidaten nach Lookup-Häufigkeit): SELECT TOP 10 OBJECT_NAME(s.object_id) AS Tabelle, i.name AS Index_Name, s.user_lookups, s.user_seeks, CAST(100.0 * s.user_lookups / NULLIF(s.user_seeks + s.user_lookups, 0) AS DECIMAL(5,1)) AS Lookup_Anteil_Pct, s.last_user_lookup FROM sys.dm_db_index_usage_stats s JOIN sys.indexes i ON s.object_id = i.object_id AND s.index_id = i.index_id WHERE s.database_id = DB_ID() AND s.user_lookups > 100 ORDER BY s.user_lookups DESC; |
FIX:
|
-- Fehlende Spalten als INCLUDE ergänzen (Covering Index): -- VORHER: erzeugt Key Lookup für GesamtBetrag und Status CREATE INDEX IX_Alt ON dbo.Bestellungen (KundeID);
-- NACHHER: kein Lookup mehr nötig CREATE INDEX IX_Bestellungen_KundeID ON dbo.Bestellungen (KundeID) INCLUDE (Bestelldatum, GesamtBetrag, Status); |
Eine WHERE-Klausel vergleicht VARCHAR-Spalte mit NVARCHAR-Parameter — oder INT-Spalte mit VARCHAR-Literal. SQL Server konvertiert implizit jeden Zeilenwert, kann den Index nicht nutzen und führt einen Table Scan durch. Der Execution Plan zeigt eine gelbe Warnung — die meisten lesen sie nicht.
DIAGNOSE:
|
-- Implicit Conversions aus dem Plan Cache lesen: SELECT TOP 20 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 WHERE CAST(qp.query_plan AS NVARCHAR(MAX)) LIKE '%PlanAffectingConvert%' ORDER BY qs.total_elapsed_time DESC; |
FIX:
|
-- VORHER (Implicit Conversion, kein Index-Seek möglich): WHERE KundenNummer = 12345 -- INT-Literal auf VARCHAR-Spalte WHERE Email = N'test@firma.de' -- NVARCHAR-Literal auf VARCHAR-Spalte
-- NACHHER (korrekter Literal-Typ): WHERE KundenNummer = '12345' -- VARCHAR-Literal auf VARCHAR-Spalte WHERE Email = 'test@firma.de' -- VARCHAR-Literal auf VARCHAR-Spalte -- Langfristig: Datentypen in Tabelle und Applikation angleichen. |
Eine Unterabfrage in der SELECT-Liste, die auf die äußere Abfrage verweist, wird für jede zurückgegebene Zeile einzeln ausgeführt. Bei 50.000 Zeilen im Ergebnis: 50.000 einzelne Unterabfrage-Ausführungen.
FIX:
|
-- VORHER: Unterabfrage läuft für JEDE Zeile in Orders: SELECT o.OrderID, o.CustomerID, (SELECT SUM(Amount) FROM OrderLines WHERE OrderID = o.OrderID) AS Total FROM Orders o;
-- NACHHER: JOIN mit vorberechneter Aggregation (einmalige Ausführung): WITH OrderTotals AS ( SELECT OrderID, SUM(Amount) AS Total FROM OrderLines GROUP BY OrderID ) SELECT o.OrderID, o.CustomerID, COALESCE(ot.Total, 0) AS Total FROM Orders o LEFT JOIN OrderTotals ot ON o.OrderID = ot.OrderID; |
Eine Bedingung ist dann "nicht-sargable", wenn sie verhindert, dass SQL Server einen Index-Seek durchführen kann — weil die indizierte Spalte in eine Funktion eingewickelt ist oder ein führendes Wildcard-Zeichen verwendet wird.
FIX:
|
-- VORHER: nicht-sargable (Index wird ignoriert): WHERE YEAR(Bestelldatum) = 2025 -- Funktion auf indizierter Spalte WHERE LEFT(Kundennummer, 3) = 'KD-' -- Funktion auf indizierter Spalte WHERE Beschreibung LIKE '%Fehler%' -- Führendes Wildcard
-- NACHHER: sargable (Index-Seek möglich): WHERE Bestelldatum >= '2025-01-01' AND Bestelldatum < '2026-01-01' -- Range-Seek auf Index WHERE Kundennummer LIKE 'KD-%' -- Trailing Wildcard: Seek möglich -- Für Volltextsuche: Full-Text Index anlegen |
SELECT DISTINCT und ORDER BY erzwingen teure Sort-Operationen im Execution Plan — mit Memory Grant und oft Spillover auf die TempDB-Festplatte. Ein DISTINCT, das nur Duplikate verdeckt, die durch fehlende JOIN-Bedingungen entstehen, ist ein Datenfehler mit kosmetischer Behandlung.
FIX:
|
-- VORHER: DISTINCT versteckt JOIN-Fehler und erzwingt Sort: SELECT DISTINCT k.KundeID, k.Name FROM Kunden k JOIN Bestellungen b ON k.KundeID = b.KundeID;
-- NACHHER: EXISTS — kein Kartesisches Produkt, kein Sort: SELECT k.KundeID, k.Name FROM Kunden k WHERE EXISTS ( SELECT 1 FROM Bestellungen b WHERE b.KundeID = k.KundeID ); -- ORDER BY nur dann, wenn die Reihenfolge wirklich benötigt wird. |
Stored Procedures werden bei jedem Aufruf neu kompiliert — wegen WITH RECOMPILE, Statistik-Änderungen über dem Schwellenwert oder Tabellenvariablen ohne Statistiken. Recompilierung kostet CPU bei jedem Aufruf.
FIX:
|
-- Tabellenvariable (keine Statistiken, erzwingt oft Recompile) ersetzen: -- VORHER: DECLARE @TempData TABLE (ID INT, Wert DECIMAL(10,2));
-- NACHHER: Temporäre Tabelle mit Index und echten Statistiken: CREATE TABLE #TempData (ID INT, Wert DECIMAL(10,2)); CREATE INDEX IX_Wert ON #TempData (Wert); INSERT #TempData SELECT ID, Wert FROM Quelltabelle; SELECT * FROM #TempData WHERE Wert > 1000; DROP TABLE #TempData; |
Eine Transaktion hält Sperren auf Zeilen oder Seiten, die eine andere Transaktion benötigt. SQL Server wartet — und der Nutzer wartet.
DIAGNOSE:
|
-- Aktuelle Blockierungssituation in Echtzeit: SELECT blocking.session_id AS Blocker_Session, blocked.session_id AS Blocked_Session, blocked.wait_time / 1000 AS Wartezeit_Sekunden, blocked.wait_type, SUBSTRING(bt.text, 1, 200) AS Blocker_Query, SUBSTRING(dt.text, 1, 200) AS Blocked_Query FROM sys.dm_exec_requests blocked JOIN sys.dm_exec_sessions blocking ON blocked.blocking_session_id = blocking.session_id CROSS APPLY sys.dm_exec_sql_text(blocking.sql_handle) bt CROSS APPLY sys.dm_exec_sql_text(blocked.sql_handle) dt WHERE blocked.blocking_session_id > 0 ORDER BY blocked.wait_time DESC; |
FIX:
|
-- RCSI aktivieren: Leser blockieren Schreiber nicht mehr (MVCC-Prinzip) ALTER DATABASE IhreDatenbank SET READ_COMMITTED_SNAPSHOT ON;
-- Batch-Update mit Commit-Zwischenpunkten (verhindert lange Lock-Haltung): WHILE EXISTS (SELECT 1 FROM dbo.Bestellungen WHERE Status = 'Neu') BEGIN BEGIN TRAN; UPDATE TOP (1000) dbo.Bestellungen SET Status = 'Verarbeitet' WHERE Status = 'Neu'; COMMIT; END |
Der Query Optimizer schätzt 1 Zeile — tatsächlich sind es 500.000. Falsche Schätzungen führen zu falschen Join-Strategien, falschen Memory Grants und katastrophalen Execution Plans.
DIAGNOSE:
|
-- Veraltete Statistiken identifizieren: SELECT OBJECT_NAME(s.object_id) AS Tabelle, s.name AS Statistik, sp.last_updated, sp.rows, sp.modification_counter FROM sys.stats s CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp WHERE OBJECT_SCHEMA_NAME(s.object_id) <> 'sys' AND sp.last_updated < DATEADD(DAY, -7, GETDATE()) AND sp.rows > 10000 ORDER BY sp.modification_counter DESC; |
FIX:
|
-- Statistiken mit maximaler Genauigkeit aktualisieren: UPDATE STATISTICS dbo.Bestellungen WITH FULLSCAN;
-- SQL Server 2025: Persistierte Stichprobengröße (neue Option) UPDATE STATISTICS dbo.Bestellungen WITH SAMPLE 30 PERCENT, PERSIST_SAMPLE_PERCENT = ON; |
Viele Abfragen nutzen TempDB für Sortierungen, Hash-Joins und temporäre Objekte. Eine falsch konfigurierte TempDB ist ein systemweiter Engpass, der jede Query betrifft.
DIAGNOSE:
|
-- TempDB-Dateikonfiguration prüfen: SELECT name, physical_name, size * 8 / 1024 AS Groesse_MB, growth FROM tempdb.sys.database_files ORDER BY file_id;
-- PAGELATCH-Waits prüfen (TempDB-Contention-Signal): SELECT wait_type, waiting_tasks_count, wait_time_ms / 1000.0 AS Wartezeit_Sek FROM sys.dm_os_wait_stats WHERE wait_type LIKE 'PAGELATCH%' ORDER BY wait_time_ms DESC; |
FIX:
|
-- Optimale Konfiguration: 1 Datei pro CPU-Kern, max. 8, alle gleich groß ALTER DATABASE tempdb ADD FILE (NAME='tempdev2', FILENAME='D:\TempDB\tempdev2.ndf', SIZE=4096MB, FILEGROWTH=512MB); -- Für tempdev3 bis tempdev8 wiederholen, dann SQL Server neu starten. |
03
Diese sieben Skripte bilden das komplette Erst-Diagnose-Set. Führen Sie sie in dieser Reihenfolge aus — von den teuersten Abfragen über Waits bis zu Indizes. Das Ergebnis ist eine vollständige Priorisierungsliste ohne External Tools, ohne Profiler, ohne Extended Events.
|
SELECT TOP 15 qs.total_elapsed_time / 1000000.0 AS Total_Sek, qs.execution_count AS Ausfuehrungen, qs.total_elapsed_time / 1000000.0 / qs.execution_count AS Avg_Sek, qs.total_worker_time / 1000000.0 AS CPU_Total_Sek, qs.total_logical_reads AS Logical_Reads, qs.total_logical_reads / qs.execution_count AS Avg_Reads, DB_NAME(st.dbid) AS Datenbank, 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 ORDER BY qs.total_elapsed_time DESC; |
|
SELECT TOP 15 qs.execution_count, qs.total_elapsed_time / 1000000.0 / qs.execution_count AS Avg_Sek, qs.total_logical_reads / qs.execution_count AS Avg_Reads, qs.total_spills / qs.execution_count AS Avg_Spills, qs.plan_generation_num AS Recompile_Zaehler, SUBSTRING(st.text, 1, 300) AS Query_Beginn FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st WHERE qs.execution_count > 100 ORDER BY Avg_Sek DESC; |
|
SELECT TOP 20 wait_type, waiting_tasks_count, wait_time_ms / 1000.0 AS Wartezeit_Sek, max_wait_time_ms / 1000.0 AS Max_Wartezeit_Sek, CAST(100.0 * wait_time_ms / SUM(wait_time_ms) OVER () AS DECIMAL(5,2)) AS Anteil_Pct FROM sys.dm_os_wait_stats WHERE wait_type NOT IN ( 'SLEEP_TASK','BROKER_TO_FLUSH','BROKER_TASK_STOP', 'CLR_AUTO_EVENT','DISPATCHER_QUEUE_SEMAPHORE', 'FT_IFTS_SCHEDULER_IDLE_WAIT','LAZYWRITER_SLEEP', 'LOGMGR_QUEUE','ONDEMAND_TASK_QUEUE', 'REQUEST_FOR_DEADLOCK_SEARCH','RESOURCE_QUEUE', 'SERVER_IDLE_CHECK','SLEEP_DBSTARTUP','SLEEP_DCOMSTARTUP', 'SLEEP_MASTERDBREADY','SLEEP_MASTERMDREADY', 'SLEEP_MASTERUPGRADED','SLEEP_MSDBSTARTUP', 'SLEEP_SYSTEMTASK','SLEEP_TEMPDBSTARTUP', 'SNI_HTTP_ACCEPT','SP_SERVER_DIAGNOSTICS_SLEEP', 'SQLTRACE_BUFFER_FLUSH','SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'WAITFOR','XE_DISPATCHER_WAIT','XE_TIMER_EVENT', 'BROKER_EVENTHANDLER','CHECKPOINT_QUEUE', 'DBMIRROR_EVENTS_QUEUE','SQLTRACE_WAIT_ENTRIES' ) ORDER BY wait_time_ms DESC; |
|
SELECT r.session_id, r.status, r.wait_type, r.wait_time / 1000 AS Wait_Sek, r.total_elapsed_time / 1000 AS Laufzeit_Sek, r.cpu_time / 1000 AS CPU_Sek, r.logical_reads, r.blocking_session_id, DB_NAME(r.database_id) AS Datenbank, SUBSTRING(st.text, 1, 500) AS Query_Text, r.percent_complete, r.estimated_completion_time / 1000 AS Noch_Sek FROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) st WHERE r.session_id <> @@SPID AND r.total_elapsed_time > 5000 ORDER BY r.total_elapsed_time DESC; |
|
SELECT OBJECT_NAME(i.object_id) AS Tabelle, i.name AS Index_Name, i.type_desc, s.user_seeks, s.user_scans, s.user_lookups, s.user_seeks + s.user_scans + s.user_lookups AS Gesamt_Lesezugriffe, s.user_updates AS Schreibzugriffe, s.last_user_seek FROM sys.indexes i LEFT JOIN sys.dm_db_index_usage_stats s ON i.object_id = s.object_id AND i.index_id = s.index_id AND s.database_id = DB_ID() WHERE i.type > 0 AND OBJECT_SCHEMA_NAME(i.object_id) <> 'sys' ORDER BY s.user_updates DESC, Gesamt_Lesezugriffe ASC; |
|
SELECT OBJECT_NAME(ips.object_id) AS Tabelle, i.name AS Index_Name, CAST(ips.avg_fragmentation_in_percent AS DECIMAL(5,1)) AS Fragmentierung_Pct, ips.page_count, CASE WHEN ips.avg_fragmentation_in_percent > 30 THEN 'REBUILD' WHEN ips.avg_fragmentation_in_percent > 10 THEN 'REORGANIZE' ELSE 'OK' END AS Empfehlung FROM sys.dm_db_index_physical_stats( DB_ID(), NULL, NULL, NULL, 'LIMITED') ips JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id WHERE ips.avg_fragmentation_in_percent > 10 AND ips.page_count > 1000 ORDER BY ips.avg_fragmentation_in_percent DESC; |
|
SELECT COUNT(*) AS Plaene_Gesamt, SUM(CASE WHEN usecounts = 1 THEN 1 ELSE 0 END) AS Einmal_Plaene, CAST(100.0 * SUM(CASE WHEN usecounts = 1 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0) AS DECIMAL(5,1)) AS Einmal_Plan_Pct, SUM(size_in_bytes) / 1048576 AS Cache_Gesamt_MB FROM sys.dm_exec_cached_plans WHERE objtype = 'Adhoc'; -- > 30 %: sp_configure 'optimize for ad hoc workloads', 1 einschalten |
04
|
Operator |
Was er tut |
Wann problematisch |
|
**Table Scan** |
Liest gesamte Tabelle |
Fast immer — fehlender Index |
|
**Clustered Index Scan** |
Liest gesamten Clustered Index |
Bei großen Tabellen mit Filterung |
|
**Index Seek** |
Navigiert direkt zu Zeilen |
Gut — gewünschter Zustand |
|
**Key Lookup** |
Holt Spalten aus Clustered Index nach |
Bei vielen Zeilen sehr teuer |
|
**Nested Loops** |
Join durch Iteration |
Gut für kleine innere Mengen |
|
**Hash Match** |
Join über Hash-Tabelle |
Memory-intensiv, Spill möglich |
|
**Merge Join** |
Join auf sortierten Daten |
Effizient, erfordert Sortierung |
|
**Sort** |
Sortiert Ergebnismenge |
Teuer, kann auf TempDB spillen |
|
**Spool (Lazy/Eager)** |
Zwischenpuffer für Teilbaum |
Zeigt fehlenden Index oder Cursor |
|
**Parallelism (Exchange)** |
Verteilt Arbeit auf CPU-Kerne |
CXPACKET-Waits, Skew-Problem |
Execution Plans werden von rechts nach links und von oben nach unten gelesen. Der dickste Pfeil zeigt den größten Datenstrom. Kosten-Prozente beziehen sich auf den Gesamtplan.
|
BEISPIEL: Wie man einen Plan liest ══════════════════════════════════════════════════════════════ [SELECT] ← [Hash Match] ← [Index Seek] ← Datenstrom ← [Table Scan] ← Datenstrom ══════════════════════════════════════════════════════════════ → Lesen: Table Scan und Index Seek → Hash Match → SELECT → Dicker Pfeil links vom Table Scan: zu viele Zeilen → Table Scan 78 % Kosten: Das ist der Engpass → Lösung: Index auf die Scan-Tabelle anlegen ══════════════════════════════════════════════════════════════ |
→ Implicit Conversion (roter Ausrufer): Datentyp-Mismatch verhindert Index-Nutzung — Falle 03.
→ No Join Predicate: Kartesisches Produkt — JOIN-Bedingung fehlt vollständig. Kritisch.
→ Spill to TempDB: Operator hatte nicht genug RAM, Daten auf TempDB ausgelagert. Ursache: falsche Cardinality-Schätzung.
→ Missing Index (grüner Hinweis): SQL Server empfiehlt einen Index. Als Diagnosehinweis nutzen, nicht blind umsetzen.
→ Residual Predicate: Filter wird nach dem Index-Seek nochmal auf Zeilenebene angewendet — Index deckt nicht alle Filter-Spalten ab.
|
INTERPRETATIONSHILFE: ────────────────────────────────────────────────────────────── Abweichung < 2×: Normal — gute Schätzung Abweichung 2–10×: Warnsignal — Statistiken prüfen Abweichung > 10×: Problem — veraltete Statistiken oder Parameter Sniffing (Kapitel 9) Abweichung > 100×: Krise — Plan komplett unbrauchbar, Join-Strategie und Memory Grant falsch ────────────────────────────────────────────────────────────── Estimated = 1, Actual = 500.000: Klassisches Sniffing-Muster Estimated = 500.000, Actual = 1: Statistik-Histogramm veraltet ────────────────────────────────────────────────────────────── |
05
Wait Statistics sind die ehrlichste Performance-Diagnose, die SQL Server liefert. Jeder Wait-Typ benennt direkt, worauf gewartet wird. Das Wait-Muster gibt die Diagnose-Richtung vor — bevor ein einziger Execution Plan geöffnet wird.
|
Wait-Typ |
Bedeutung |
Häufige Ursache |
Erste Maßnahme |
|
`PAGEIOLATCH_SH/EX` |
Wartet auf Daten-Page von Disk |
Fehlender Index, zu wenig RAM |
Index anlegen, RAM erhöhen |
|
`PAGELATCH_UP/EX` |
TempDB-Contention |
Zu wenige TempDB-Dateien |
8 TempDB-Dateien gleicher Größe |
|
`LCK_M_X/S/U` |
Lock-Contention |
Lange Transaktionen |
RCSI aktivieren, Transaktionen kürzen |
|
`CXPACKET/CXCONSUMER` |
Parallelismus-Ungleichgewicht |
Zu hoher MAXDOP |
MAXDOP auf 4–8, Cost Threshold 50 |
|
`SOS_SCHEDULER_YIELD` |
CPU-Sättigung |
Fehlende Indizes, zu viele Abfragen |
Indizes anlegen, Resource Governor |
|
`ASYNC_NETWORK_IO` |
App liest Ergebnisse zu langsam |
Applikation verarbeitet Rows zu langsam |
Connection Pooling, Ergebnismenge reduzieren |
|
`RESOURCE_SEMAPHORE` |
Wartet auf Memory Grant |
Falsche Cardinality-Schätzung |
Statistiken aktualisieren |
|
`WRITELOG` |
Wartet auf Log-Flush |
Langsames Log-Storage |
SSD für Log-Dateien |
|
`HADR_SYNC_COMMIT` |
Always-On Sync-Commit |
Sekundäre Replica zu langsam |
Netzwerk prüfen, async commit erwägen |
|
SELECT s.session_id, r.wait_type, r.wait_time / 1000.0 AS Wait_Sek, r.wait_resource, r.blocking_session_id, r.logical_reads, r.total_elapsed_time / 1000 AS Laufzeit_Sek FROM sys.dm_exec_sessions s JOIN sys.dm_exec_requests r ON s.session_id = r.session_id WHERE s.session_id = 57; -- Session-ID aus DMV 4 eintragen |
06
Der Query Store ist der mächtigste Performance-Assistent, den SQL Server seit Version 2016 mitbringt — und er ist in den meisten KMU-Umgebungen entweder deaktiviert oder wird nicht genutzt.
|
ALTER DATABASE IhreDatenbank SET QUERY_STORE = ON ( OPERATION_MODE = READ_WRITE, QUERY_CAPTURE_MODE = AUTO, MAX_STORAGE_SIZE_MB = 1000, INTERVAL_LENGTH_MINUTES = 15, SIZE_BASED_CLEANUP_MODE = AUTO, STALE_QUERY_THRESHOLD_DAYS = 30 ); |
|
-- ABFRAGE 1: Top-Ressourcenverbraucher (letzter Tag) SELECT TOP 15 q.query_id, qt.query_sql_text, CAST(AVG(rs.avg_duration) / 1000000.0 AS DECIMAL(10,3)) AS Avg_Sek, CAST(AVG(rs.avg_cpu_time) / 1000000.0 AS DECIMAL(10,3)) AS Avg_CPU_Sek, AVG(rs.avg_logical_io_reads) AS Avg_Reads, SUM(rs.count_executions) AS Ausfuehrungen, COUNT(DISTINCT rs.plan_id) AS Planzahl 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 WHERE rs.last_execution_time > DATEADD(DAY, -1, GETDATE()) GROUP BY q.query_id, qt.query_sql_text ORDER BY AVG(rs.avg_duration) DESC;
-- ABFRAGE 2: Regressions — Queries die plötzlich langsamer wurden SELECT TOP 10 q.query_id, SUBSTRING(qt.query_sql_text, 1, 200) AS Query_Beginn, CAST(rs_neu.avg_duration / 1000000.0 AS DECIMAL(10,3)) AS Avg_Neu_Sek, CAST(rs_alt.avg_duration / 1000000.0 AS DECIMAL(10,3)) AS Avg_Alt_Sek, CAST(rs_neu.avg_duration / NULLIF(rs_alt.avg_duration, 0) AS DECIMAL(10,2)) AS Faktor_Schlechter 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_neu ON q.query_id = p_neu.query_id JOIN sys.query_store_runtime_stats rs_neu ON p_neu.plan_id = rs_neu.plan_id AND rs_neu.last_execution_time > DATEADD(DAY, -1, GETDATE()) JOIN sys.query_store_plan p_alt ON q.query_id = p_alt.query_id JOIN sys.query_store_runtime_stats rs_alt ON p_alt.plan_id = rs_alt.plan_id AND rs_alt.last_execution_time BETWEEN DATEADD(DAY,-8,GETDATE()) AND DATEADD(DAY,-7,GETDATE()) WHERE rs_neu.avg_duration > rs_alt.avg_duration * 2 AND rs_alt.count_executions > 10 ORDER BY Faktor_Schlechter DESC;
-- ABFRAGE 3: Plan Forcing — erzwinge den guten alten Plan EXEC sys.sp_query_store_force_plan @query_id = 42, -- Query-ID aus Abfrage 1 oder 2 @plan_id = 7; -- Plan-ID des guten, alten Plans |
|
|
PLAN FORCING — WANN UND WIE Plan Forcing ist ein kurzfristiges Pflaster, kein dauerhafter Fix. Es stabilisiert eine Regression sofort und gibt Ihnen Zeit, die eigentliche Ursache zu beheben (Statistiken, Indexänderung, Kompatibilitätslevel). Dokumentieren Sie jeden erzwungenen Plan: Query-ID, Plan-ID, Datum, Grund und geplantes Review-Datum. |
07
|
-- VORHER: COUNT(*) liest alle Zeilen, um zu prüfen ob > 0: IF (SELECT COUNT(*) FROM dbo.Bestellungen WHERE KundeID = 42 AND Status = 'Offen') > 0 PRINT 'Hat offene Bestellungen';
-- NACHHER: EXISTS stoppt beim ersten Treffer: IF EXISTS (SELECT 1 FROM dbo.Bestellungen WHERE KundeID = 42 AND Status = 'Offen') PRINT 'Hat offene Bestellungen'; -- Performance-Gewinn: bis zu 100× bei großen Tabellen |
|
-- VORHER: Cursor iteriert zeilenweise (langsam): DECLARE @KundeID INT; DECLARE cur CURSOR FOR SELECT KundeID FROM dbo.Kunden WHERE Aktiv = 1; OPEN cur; FETCH NEXT FROM cur INTO @KundeID; WHILE @@FETCH_STATUS = 0 BEGIN UPDATE dbo.Bestellungen SET PriorKunde = 1 WHERE KundeID = @KundeID AND GesamtBetrag > 10000; FETCH NEXT FROM cur INTO @KundeID; END CLOSE cur; DEALLOCATE cur;
-- NACHHER: Mengenbasiertes UPDATE (einmalige Operation): UPDATE b SET b.PriorKunde = 1 FROM dbo.Bestellungen b JOIN dbo.Kunden k ON b.KundeID = k.KundeID WHERE k.Aktiv = 1 AND b.GesamtBetrag > 10000; -- Performance-Gewinn: typisch 10–1000× |
|
-- VORHER: Self-Join für laufende Summen (quadratische Komplexität): SELECT a.Datum, a.Umsatz, SUM(b.Umsatz) AS Umsatz_Kumuliert FROM dbo.TagesUmsatz a JOIN dbo.TagesUmsatz b ON b.Datum <= a.Datum GROUP BY a.Datum, a.Umsatz ORDER BY a.Datum;
-- NACHHER: Window Function (linearer Scan, ein Durchlauf): SELECT Datum, Umsatz, SUM(Umsatz) OVER ( ORDER BY Datum ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS Umsatz_Kumuliert FROM dbo.TagesUmsatz ORDER BY Datum; -- Von O(n²) auf O(n) — massiver Gewinn bei großen Tabellen |
|
-- VORHER: Verschachtelte Subqueries werden mehrfach ausgewertet: SELECT * FROM ( SELECT KundeID, SUM(Betrag) AS JU FROM dbo.Bestellungen WHERE YEAR(Datum) = 2025 GROUP BY KundeID ) x WHERE x.JU > (SELECT AVG(JU) FROM ( SELECT SUM(Betrag) AS JU FROM dbo.Bestellungen WHERE YEAR(Datum) = 2025 GROUP BY KundeID) avg_sub);
-- NACHHER: Temporäre Tabelle — einmalig berechnet: SELECT KundeID, SUM(Betrag) AS Jahresumsatz INTO #KU FROM dbo.Bestellungen WHERE Datum >= '2025-01-01' AND Datum < '2026-01-01' GROUP BY KundeID; CREATE INDEX IX_JU ON #KU (Jahresumsatz); SELECT * FROM #KU WHERE Jahresumsatz > (SELECT AVG(Jahresumsatz) FROM #KU); DROP TABLE #KU; |
|
-- VORHER: OFFSET muss alle vorherigen Seiten materialisieren: SELECT KundeID, Name, Umsatz FROM dbo.Kunden ORDER BY Umsatz DESC OFFSET 10000 ROWS FETCH NEXT 50 ROWS ONLY; -- SQL Server sortiert 10.050 Zeilen, gibt 50 zurück
-- NACHHER: Keyset Pagination über letzten bekannten Wert: -- (Letzter Wert der vorherigen Seite: Umsatz 5432, ID 7891) SELECT TOP 50 KundeID, Name, Umsatz FROM dbo.Kunden WHERE Umsatz < 5432.00 OR (Umsatz = 5432.00 AND KundeID > 7891) ORDER BY Umsatz DESC, KundeID ASC; -- Benötigt Index auf (Umsatz, KundeID) — skaliert auf beliebige Tiefe |
08
Jeder Index kostet bei jedem INSERT, UPDATE und DELETE Schreiblast. Das Ziel ist nicht "möglichst viele Indizes" — sondern "die richtigen Indizes".
|
-- Indizes mit identischen Leading Columns (Duplikat-Kandidaten): SELECT OBJECT_NAME(i1.object_id) AS Tabelle, i1.name AS Index_1, i2.name AS Index_2, COL_NAME(ic1.object_id, ic1.column_id) AS Erste_Spalte, COALESCE(s1.user_seeks + s1.user_scans + s1.user_lookups, 0) AS Nutzung_1, COALESCE(s2.user_seeks + s2.user_scans + s2.user_lookups, 0) AS Nutzung_2 FROM sys.indexes i1 JOIN sys.indexes i2 ON i1.object_id = i2.object_id AND i1.index_id < i2.index_id JOIN sys.index_columns ic1 ON i1.object_id = ic1.object_id AND i1.index_id = ic1.index_id AND ic1.key_ordinal = 1 JOIN sys.index_columns ic2 ON i2.object_id = ic2.object_id AND i2.index_id = ic2.index_id AND ic2.key_ordinal = 1 AND ic1.column_id = ic2.column_id -- gleiche erste Spalte LEFT JOIN sys.dm_db_index_usage_stats s1 ON i1.object_id = s1.object_id AND i1.index_id = s1.index_id AND s1.database_id = DB_ID() LEFT JOIN sys.dm_db_index_usage_stats s2 ON i2.object_id = s2.object_id AND i2.index_id = s2.index_id AND s2.database_id = DB_ID() WHERE OBJECT_SCHEMA_NAME(i1.object_id) <> 'sys' ORDER BY OBJECT_NAME(i1.object_id); |
→ Nutzen-Score > 100.000 (Missing-Index-DMV): Index anlegen — hohe Priorität.
→ Lookup-Anteil > 50 % (DMV 5): INCLUDE-Spalten des bestehenden Index erweitern — kein neuen Index anlegen.
→ 0 Lesezugriffe seit Restart (DMV 5): Vier Wochen beobachten, dann droppen wenn weiterhin 0.
→ Gleiche erste Spalte (Duplikat-Skript): Schwächeren Index droppen oder in den stärkeren integrieren.
→ Schreiblastige Tabellen (> 70 % Writes): Jeden neuen Index kritisch hinterfragen. Schreiblast × Index-Anzahl = Wartungskosten.
09
SQL Server kompiliert einen Execution Plan beim ersten Aufruf — basierend auf den Parameterwerten dieses ersten Aufrufs. Dieser Plan wird gecacht und für alle Folgeaufrufe wiederverwendet, auch wenn spätere Parameter völlig andere Datenmengen selektieren.
|
DAS SNIFFING-DILEMMA: ────────────────────────────────────────────────────────────── Erster Aufruf: GetOrders @KundeID = 1 → 5 Bestellungen → Plan: Index Seek (gecacht)
Zweiter Aufruf: GetOrders @KundeID = 99 → 500.000 Bestellungen → gecachter Seek-Plan → Optimal wäre Hash Join → Timeout ────────────────────────────────────────────────────────────── |
|
-- Query Store: mehrere Pläne für dieselbe Query = Sniffing bestätigt SELECT q.query_id, p.plan_id, CAST(rs.avg_duration / 1000000.0 AS DECIMAL(10,3)) AS Avg_Sek, rs.count_executions, rs.last_execution_time 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 WHERE qt.query_sql_text LIKE '%GetOrders%' ORDER BY q.query_id, p.plan_id; -- Mehrere plan_id für dieselbe query_id → Sniffing |
|
-- STRATEGIE 1: OPTIMIZE FOR UNKNOWN -- Plan für "durchschnittliche" Werte kompiliert, nicht für den ersten Parameter. CREATE OR ALTER PROCEDURE dbo.GetOrders @KundeID INT AS BEGIN SELECT * FROM dbo.Bestellungen WHERE KundeID = @KundeID OPTION (OPTIMIZE FOR (@KundeID UNKNOWN)); END;
-- STRATEGIE 2: OPTION (RECOMPILE) -- Plan wird bei jedem Aufruf neu erstellt — gut für seltene, kritische Queries. CREATE OR ALTER PROCEDURE dbo.GetOrders @KundeID INT AS BEGIN SELECT * FROM dbo.Bestellungen WHERE KundeID = @KundeID OPTION (RECOMPILE); END;
-- STRATEGIE 3: OPPO (SQL Server 2025, Compatibility Level 170) -- Automatische Erkennung und mehrere gecachte Pläne pro Query. ALTER DATABASE IhreDatenbank SET COMPATIBILITY_LEVEL = 170; -- OPPO ist dann automatisch aktiv — kein Code-Eingriff nötig.
-- STRATEGIE 4: Sofortlösung — Plan Cache leeren DBCC FREEPROCCACHE ( (SELECT TOP 1 plan_handle FROM sys.dm_exec_procedure_stats WHERE OBJECT_NAME(object_id) = 'GetOrders') ); |
10
Dieser Plan führt Sie in 30 Tagen durch einen vollständigen, strukturierten Query-Tuning-Zyklus — von der ersten Bestandsaufnahme bis zu automatisierten Überwachungsroutinen.
|
|
VOR DEM START Erstellen Sie ein vollständiges Backup aller Datenbanken. Aktivieren Sie den Query Store auf allen Produktivdatenbanken (Kapitel 6, Konfigurationsskript). Notieren Sie aktuelle Baseline-Werte: Top-5-Queries aus DMV 1, Wait-Statistics-Verteilung aus DMV 3, TempDB-Dateianzahl. Diese Baseline ist die Referenz für den Erfolgsnachweis nach 30 Tagen. |
■ TAG 1–2: VOLLSTÄNDIGE ERSTDIAGNOSE
■ Alle 7 DMV-Skripte aus Kapitel 3 ausführen — Ergebnisse in Excel dokumentieren
■ Query Store auf allen Produktivdatenbanken aktivieren
■ TempDB-Konfiguration prüfen: Anzahl Dateien, Größe, Storage-Typ (Falle 10)
■ Wait Statistics exportieren: Baseline für PAGEIOLATCH, PAGELATCH, LCK_M, CXPACKET
■ Top-10-Queries nach Total Elapsed Time identifizieren und priorisieren
■ TAG 3–4: URSACHEN KLASSIFIZIEREN
■ Die Top-10-Queries einzeln im Actual Execution Plan öffnen (Ctrl+M in SSMS)
■ Je Query notieren: Hauptoperatoren, Warnungen, Estimated vs. Actual Rows, Key Lookups
■ Kategorie zuweisen: Missing Index / Implicit Conversion / Sniffing / Blocking / Statistiken
■ Quick-Win-Liste erstellen: Welche Fixes sind einfach und in unter 1 Stunde umsetzbar?
■ TAG 5–7: QUICK WINS UMSETZEN
■ Alle Implicit Conversions (gelbe Warnungen im Plan) identifizieren — Datentypen angleichen
■ Top-3 Missing Indexes mit Nutzen-Score > 100.000 anlegen (mit ONLINE=ON)
■ Veraltete Statistiken aktualisieren: UPDATE STATISTICS ... WITH FULLSCAN für Top-Tabellen
■ TempDB auf min. 8 gleichgroße Dateien konfigurieren — Server-Neustart im Wartungsfenster
■ TAG 8–10: EXECUTION-PLAN-ANALYSE DER TOP-5
■ Jeden der Top-5-Queries Schritt für Schritt durch Kapitel 4 führen
■ Sort-Operatoren mit Spill: Statistiken aktualisieren oder Index mit Sortierungsreihenfolge anlegen
■ Hash-Match-Joins bei kleinen Ergebnismengen: Prüfen ob Nested Loops besser wäre
■ Spool-Operatoren: Ursache identifizieren — fehlender Index, Cursor, Subquery?
■ TAG 11–13: REWRITE-SESSION
■ Correlated Subqueries in SELECT-Liste in CTEs oder JOINs umschreiben (Rezept 1 und 4)
■ Cursor durch mengenbasierte Operationen ersetzen — einen Cursor pro Tag (Rezept 2)
■ Nicht-sargable WHERE-Bedingungen korrigieren (Falle 05 aus Kapitel 2)
■ Performance vor und nach jedem Rewrite messen und im Query Store dokumentieren
■ TAG 14: PARAMETER-SNIFFING-ANALYSE
■ Query Store: Abfragen mit mehr als einem Plan-Eintrag identifizieren (Kapitel 9)
■ Sniffing-Kandidaten mit OPTIMIZE FOR UNKNOWN testen
■ SQL Server 2025: Compatibility Level auf 170 setzen, OPPO aktivieren
■ Plan Forcing für kritische Regressions-Queries als Sofortmaßnahme einrichten
■ TAG 15–17: INDEX-INVENTUR
■ DMV 5 (Index-Nutzung) exportieren: Welche Indizes haben 0 Lesezugriffe seit letztem Restart?
■ Ungenutzte Indizes markieren — zwei Wochen beobachten bevor Droppen
■ Doppelte Indizes (Kapitel 8) identifizieren — schwächeren Index droppen
■ Missing-Index-DMV erneut auswerten: Welche Empfehlungen sind nach Quick Wins noch offen?
■ TAG 18–20: BLOCKING-ANALYSE UND RCSI
■ DMV 4 auf Blocking prüfen — Häufigkeit und Dauer in dieser Woche dokumentieren
■ READ_COMMITTED_SNAPSHOT auf Nicht-Produktivdatenbank testen (Falle 08 aus Kapitel 2)
■ Lange Transaktionen identifizieren: Batch-Updates ohne Commit-Zwischenpunkte?
■ Resource Governor erwägen: BI-Abfragen von ERP-Transaktionen trennen
■ TAG 21: STATISTIK-WARTUNGSJOB EINRICHTEN
■ SQL Agent Job: Täglicher Statistik-Update (01:00) mit sp_updatestats
■ Wöchentlicher FULLSCAN-Update für die 10 wichtigsten Tabellen (Samstag, 03:00)
■ Ola Hallengren Maintenance Solution installieren (ola.hallengren.com) für Index-Rebuild
■ Job-Fehlerbenachrichtigung konfigurieren: E-Mail bei fehlgeschlagenem Wartungsjob
■ TAG 22–25: QUERY-STORE-MONITORING AUFBAUEN
■ Regressions-Abfrage (Query Store Abfrage 2 aus Kapitel 6) als täglichen SQL Agent Job einrichten
■ Ergebnisse in Monitoring-Tabelle schreiben: dbo.QS_Regressions_Log
■ E-Mail-Alert einrichten: Wenn Query sich um Faktor > 3 verschlechtert → sofortige Benachrichtigung
■ Power BI Dashboard für Query-Store-Metriken aufbauen (optional): Avg-Duration-Trend je Query
■ TAG 26–28: WAIT-STATISTICS-MONITORING
■ Baseline-Wait-Statistics mit aktuellen Werten vergleichen — Verbesserung dokumentieren
■ Täglichen Wait-Statistics-Snapshot-Job einrichten: Werte in dbo.WaitStats_History speichern
■ Trendanalyse: Welche Wait-Typen wachsen? Welche wurden durch Tuning-Maßnahmen reduziert?
■ Extended Events Session für Deadlock-Monitoring aktivieren
■ TAG 29–30: ABSCHLUSS & DOKUMENTATION
■ Alle umgesetzten Maßnahmen dokumentieren: Was wurde geändert, warum, mit welchem Ergebnis?
■ Vorher-Nachher-Vergleich: Top-10-Queries Baseline vs. aktuell — Verbesserungsfaktor je Query
■ Offene Maßnahmen in Backlog priorisieren: Was kommt in den nächsten 90 Tagen?
■ Ergebnisse der Führungsebene präsentieren: Zahlen, Fakten, Messbarkeit ■
|
|
ERGEBNIS NACH 30 TAGEN Ihr Ergebnis nach 30 Tagen: Vollständige Diagnose aller Performance-Engpässe, behobene Quick Wins mit messbarer Verbesserung, ein aktives Query-Store-Monitoring das Regressions automatisch erkennt, ein Statistik- und Index-Wartungsjob der dauerhaft für gute Pläne sorgt — und ein dokumentiertes Vorher-Nachher das den ROI des Tuning-Projekts belegt. |
Die in diesem Dokument enthaltenen SQL-Skripte, Diagnose-Methoden und Empfehlungen wurden nach bestem Wissen und Gewissen auf Basis langjähriger praktischer Erfahrung in der Administration von SQL-Server-Umgebungen erstellt. Dennoch kann keine Gewähr für Vollständigkeit, Richtigkeit oder universelle Anwendbarkeit übernommen werden.
Der Autor übernimmt ausdrücklich keinerlei Haftung für unmittelbare oder mittelbare Schäden, einschließlich, aber nicht beschränkt auf: Datenverlust oder -korruption, Systemausfälle, Produktionsunterbrechungen sowie entgangenen Gewinn. Die Nutzung aller bereitgestellten Skripte und Empfehlungen erfolgt ausschließlich auf eigenes Risiko des Anwenders.
Alle in diesem Dokument beschriebenen Maßnahmen — insbesondere das Anlegen und Löschen von Indizes, das Ändern von Datenbankoptionen (RCSI, Compatibility Level, TempDB-Konfiguration) und das Forcieren von Execution Plans — sind vor dem Produktionseinsatz in einer Testumgebung zu erproben. Erstellen Sie vor jedem Eingriff in eine Produktionsdatenbank ein vollständiges Backup.
Alle SQL-Skripte wurden auf SQL Server 2022 (16.x) und SQL Server 2025 (17.x) entwickelt und getestet. Einige Funktionen (OPPO, Persist Sample Rate, Compatibility Level 170) sind ausschließlich auf SQL Server 2025 verfügbar. Auf älteren Versionen können diese Skripte Fehler erzeugen oder abweichende Ergebnisse liefern.
Die verwendeten DMVs erfordern mindestens die Berechtigung VIEW SERVER STATE. Die angezeigten Daten sind kumulativ seit dem letzten SQL-Server-Neustart — ein Neustart setzt alle DMV-Statistiken zurück.
Dieses Dokument und alle Inhalte sind urheberrechtlich geschützt. © 2026 Sascha Hess, xenosystems.de. Alle Rechte vorbehalten.
SQL Server, SSMS und Azure Data Studio sind eingetragene Marken der Microsoft Corporation. Ola Hallengren Maintenance Solution ist ein unabhängiges Open-Source-Projekt. Alle anderen genannten Produkt- und Unternehmensnamen sind Eigentum ihrer jeweiligen Inhaber.
Es gilt ausschließlich deutsches Recht. Gerichtsstand für alle Streitigkeiten ist, soweit gesetzlich zulässig, Weimar, Thüringen, Deutschland.
Sascha Hess ist Diplom-Biologe und IT-Professional mit über 20 Jahren Erfahrung in der Administration von SQL-Server-Umgebungen, ERP- und BI-Systemen. Er hat mehr als 300 SQL-Server-Instanzen administriert und betreut — von mittelständischen KMU bis zu Universitäten und Energieversorgern — und dabei hunderte langsame Queries systematisch diagnostiziert und optimiert.
Sein Ansatz verbindet naturwissenschaftliche Präzision mit hochgradiger IT-Spezialisierung. Schwerpunkte: SQL Server Performance-Tuning, ERP-Einführungen, Business Intelligence (Power BI, DeltaMaster), Data Governance und IT-Interim-Management.
Web: www.xenosystems.de | E-Mail: info@xenosystems.de | Standort: Weimar, Thüringen / Remote
|
Service |
Beschreibung |
|
Query-Tuning Session |
Strukturierte 1-Tages-Session: vollständige DMV-Diagnose, Execution-Plan-Analyse Ihrer Top-5-Queries, konkrete Rewrite- und Index-Empfehlungen mit Prioritätenliste und messbaren Ergebnissen. |
|
SQL Server DB Health Check |
Professioneller Audit Ihres SQL Servers — Managementreport mit Risikobewertung, Performance-Analyse, Indexstrategie und priorisiertem Maßnahmenplan. Scope: 3–5 Tage. |
|
SQL Server Performance-Projekt |
Umfassendes Performance-Tuning über 4–8 Wochen: DMV-Baseline, Rewrite-Sessions, Index-Strategie, Query-Store-Monitoring, Wait-Statistics-Dashboard und Dokumentation aller Maßnahmen. |
|
Power BI Einführung |
End-to-End: Datenmodell auf SQL Server, Semantic Layer, Dashboards, Row-Level Security, Gateway und Deployment Pipelines. Scope: 4–12 Wochen. |
|
Interim IT-Management |
Übernahme der IT-Steuerung auf Zeit — Budgetplanung, Dienstleister-Management, strategische IT-Ausrichtung. |
Vollständiges Dokument
Strategische Wissens-Roadmap · Checklisten · Praxisbeispiele
49,90 €
Sichere Zahlung über PayPal · Sofort-Download nach Zahlungseingang