Xenosystems Logo
Sascha Hess

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

 


 

 

Rechtliche Hinweise und Haftungsausschluss

 

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.

 


 

 

Inhaltsverzeichnis

 

 

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

Einleitung

 

Der systematische Weg von Symptom zur Ursache

 

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 10 Query-Fallen

 

Häufigste Ursachen langsamer Abfragen — mit Sofort-Fixes

 

01 Table Scan statt Index Seek

 

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

 

 

02 Key Lookup nach Index Seek

 

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);

 

 

03 Implicit Conversion — der unsichtbare Typ-Mismatch

 

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.

 

 

04 Correlated Subquery in der SELECT-Liste

 

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;

 

 

05 Nicht-sargable WHERE-Bedingungen

 

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

 

 

06 Übermäßiges DISTINCT und unnötige Sortierungen

 

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.

 

 

07 Übermäßige Recompilierung

 

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;

 

 

08 Blocking und Lock-Contention

 

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

 

 

09 Schlechte Cardinality-Schätzungen

 

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;

 

 

10 Fehlende oder falsch konfigurierte TempDB

 

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

DMV-Diagnose-Toolkit

 

7 Skripte für die vollständige Erstdiagnose in unter 10 Minuten

 

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.

 

DMV 1 — Die teuersten Abfragen nach Gesamtlaufzeit

 

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;

 

 

DMV 2 — Häufige Abfragen mit hohen Durchschnittskosten

 

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;

 

 

DMV 3 — Wait Statistics: Was wartet der Server?

 

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;

 

 

DMV 4 — Aktuell laufende langsame Abfragen

 

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;

 

 

DMV 5 — Index-Nutzung: gesucht, genutzt, ignoriert

 

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;

 

 

DMV 6 — Fragmentierungsgrad aller Indizes

 

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;

 

 

DMV 7 — Plan Cache: Anteil Einmal-Pläne (Ad-hoc-Bloat)

 

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

Execution Plans lesen

 

Operatoren, Warnungen und Kosten — der komplette Analyse-Leitfaden

 

Die wichtigsten Execution-Plan-Operatoren

 

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

 

Die Lese-Reihenfolge im Plan

 

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

══════════════════════════════════════════════════════════════

 

 

Die 5 wichtigsten Plan-Warnungen

 

  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.

 

Estimated vs. Actual Rows — die wichtigste Einzelkennzahl

 

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 & Engpässe

 

Was der Server wirklich tut, während Ihre Abfrage wartet

 

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-Diagnose-Tabelle

 

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

 

Echtzeit-Wait-Diagnose für eine einzelne Session

 

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

Query Store nutzen

 

Flight Recorder, Regressions-Jagd und Plan Forcing

 

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.

 

Query Store aktivieren und konfigurieren

 

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

);

 

 

Die drei wichtigsten Query-Store-Abfragen

 

-- 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

Rewrite-Rezepte

 

Vorher-Nachher für die 5 häufigsten langsamen SQL-Muster

 

Rezept 1 — EXISTS statt COUNT(*) für Existenzprüfungen

 

-- 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

 

 

Rezept 2 — Mengenoperationen statt Cursor

 

-- 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×

 

 

Rezept 3 — Window Functions statt Self-Join

 

-- 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

 

 

Rezept 4 — Temporäre Tabellen für mehrfach genutzte Zwischenergebnisse

 

-- 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;

 

 

Rezept 5 — Pagination mit Keyset statt OFFSET

 

-- 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

Index-Chirurgie

 

Fehlende, ungenutzte und doppelte Indizes gezielt behandeln

 

Jeder Index kostet bei jedem INSERT, UPDATE und DELETE Schreiblast. Das Ziel ist nicht "möglichst viele Indizes" — sondern "die richtigen Indizes".

 

Doppelte und redundante Indizes finden

 

-- 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);

 

 

Index-Chirurgie-Regeln auf einen Blick

 

  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

Parameter Sniffing & Statistiken

 

Die unsichtbaren Plan-Killer — diagnostizieren und dauerhaft beheben

 

Was ist Parameter Sniffing?

 

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

──────────────────────────────────────────────────────────────

 

 

Parameter Sniffing diagnostizieren

 

-- 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

 

 

Die vier Lösungsstrategien

 

-- 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

30-Tage-Tuning-Plan

 

Vom ersten Symptom zur dauerhaften Performance-Verbesserung

 

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.

 

WOCHE 1: DIAGNOSE & BASELINE

 

■ 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

 

WOCHE 2: TIEFENANALYSE & REWRITES

 

■ 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

 

WOCHE 3: INDEX-OPTIMIERUNG & GOVERNANCE

 

■ 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

 

WOCHE 4: MONITORING & DAUERBETRIEB

 

■ 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.

 


 

 

Ausführlicher Haftungsausschluss und Lizenzbestimmungen

 

1. Allgemeiner Haftungsausschluss

 

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.

 

2. Pflicht zum Backup und Testen

 

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.

 

3. Versionsspezifische Hinweise

 

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.

 

4. Dynamic Management Views

 

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.

 

5. Urheberrecht und Nutzungsrechte

 

Dieses Dokument und alle Inhalte sind urheberrechtlich geschützt. © 2026 Sascha Hess, xenosystems.de. Alle Rechte vorbehalten.

 

6. Markenrechte

 

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.

 

7. Anwendbares Recht und Gerichtsstand

 

Es gilt ausschließlich deutsches Recht. Gerichtsstand für alle Streitigkeiten ist, soweit gesetzlich zulässig, Weimar, Thüringen, Deutschland.

 


 

 

Über den Autor

 

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

Langsame Queries analysieren 2026

Strategische Wissens-Roadmap · Checklisten · Praxisbeispiele

49,90 €

Sichere Zahlung über PayPal · Sofort-Download nach Zahlungseingang