Grundsätze der SQL-Abfrageoptimierung

SQL Server Indexierung Execution Plan Query-Struktur

Grundsätze der
SQL-Abfrageoptimierung

Ein strukturierter Leitfaden über die drei wichtigsten Bereiche: Indexierung & Statistiken, Abfragestruktur und Execution Plan Analyse – mit konkreten Beispielen für MS SQL Server.

Abschnitt 1

Indexierung & Statistiken

Indizes sind das wirkungsvollste Mittel zur Beschleunigung von SQL-Abfragen. Fehlt ein geeigneter Index, liest SQL Server bei jeder Abfrage alle Zeilen der Tabelle – ein sogenannter Table Scan. Bei großen Tabellen ist das der häufigste Grund für schlechte Laufzeiten.

Clustered Index

Bestimmt die physische Speicherreihenfolge der Tabelle. Jede Tabelle kann nur einen haben – typischerweise auf dem Primärschlüssel.

Non-Clustered Index

Separate Struktur mit Zeigern auf die Originaldaten. Mehrere pro Tabelle möglich. Ideal für häufig gefilterte Nicht-PK-Spalten.

Covering Index

Enthält alle von einer Abfrage benötigten Spalten direkt im Index. Verhindert den Rückgriff auf die Basistabelle (Key Lookup).

Funktionen auf indizierten Spalten vermeiden

Wird eine Funktion direkt auf eine indizierte Spalte angewendet, kann SQL Server den Index nicht nutzen. Der Optimizer muss dann jede Zeile einzeln auswerten – ein vollständiger Scan ist die Folge. Die Lösung: den Wert transformieren, nicht die Spalte.

Langsam – Index nicht nutzbar
SELECT * 
FROM Orders 
WHERE YEAR(OrderDate) = 2024
Schnell – Bereich auf indexierter Spalte
SELECT * 
FROM Orders 
WHERE OrderDate >= '2024-01-01' 
AND OrderDate < '2025-01-01'
Regel: Funktionen auf indizierten Spalten – YEAR(), UPPER(), CONVERT(), CAST() – machen den Index wirkungslos. Immer den Vergleichswert anpassen, nie die Spalte.

Index-Typen im Überblick

TypWann sinnvollBesonderheit
ClusteredBereichsabfragen, ORDER BY auf PKNur einer pro Tabelle
Non-ClusteredWHERE-Filter auf Nicht-PK-SpaltenHäufigste Index-Art
CoveringAbfragen ohne Key LookupINCLUDE-Klausel nutzen
FilteredTeilmengen (z.B. WHERE IsActive = 1)Selten, aber sehr effektiv
ColumnstoreAnalytische Abfragen, AggregationenAb SQL Server 2012

Covering Index mit INCLUDE

Ein Key Lookup entsteht, wenn SQL Server nach einem Index Seek noch zusätzliche Spalten aus der Basistabelle nachladen muss. Das lässt sich durch Aufnahme dieser Spalten in den Index mit INCLUDE verhindern.

T-SQL — Covering Index
-- Abfrage, die einen Key Lookup erzeugt: 
SELECT CustomerID, CompanyName, ContactEmail 
FROM Customers 
WHERE Region = 'Bayern'; 
 
-- Covering Index: Suchspalte als Schlüssel, restliche als INCLUDE 
CREATE NONCLUSTERED INDEX IX_Customers_Region 
ON Customers (Region) 
INCLUDE (CustomerID, CompanyName, ContactEmail); 
GO

Statistiken und Fragmentierung

SQL Server verwendet Statistiken, um die Anzahl der betroffenen Zeilen zu schätzen und darauf basierend den besten Execution Plan zu wählen. Veraltete Statistiken führen zu falschen Schätzungen – und damit zu schlechten Plänen.

T-SQL — Statistiken & Fragmentierung
-- Statistiken für eine Tabelle aktualisieren 
UPDATE STATISTICS Orders WITH FULLSCAN; 
 
-- Alle Statistiken der Datenbank aktualisieren 
EXEC sp_updatestats; 
 
-- Fragmentierung aller Indizes prüfen 
SELECT 
OBJECT_NAME(ips.object_id) AS Tabelle, 
i.[name] AS Indexname, 
ips.avg_fragmentation_in_percent AS Fragmentierung_Pct 
FROM sys.dm_db_index_physical_stats 
 (DB_ID(), NULL, NULL, NULL, 'LIMITED') ips 
INNER 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 
ORDER BY Fragmentierung_Pct DESC;
Faustregel Fragmentierung: Unter 10 % – keine Maßnahme nötig. 10–30 % → ALTER INDEX ... REORGANIZE. Über 30 % → ALTER INDEX ... REBUILD. Statistiken sollten nach jedem größeren Ladevorgang aktualisiert werden.

Abschnitt 2

Abfragestruktur

Die Formulierung einer Abfrage beeinflusst direkt, welchen Execution Plan der Optimizer wählt. Kleine syntaktische Unterschiede können dabei über Index-Nutzung oder vollständigen Table Scan entscheiden.

SELECT * vermeiden

Das Sternchen liest alle Spalten – auch breite Textfelder (NVARCHAR(MAX)) oder Binärspalten, die für die Abfrage irrelevant sind. Das erhöht I/O und Netzwerklast und verhindert die Nutzung von Covering Indexes.

Ineffizient – alle Spalten
SELECT * 
FROM Customers 
WHERE Region = 'Bayern'
Besser – nur benötigte Spalten
SELECT CustomerID, 
CompanyName, 
ContactEmail 
FROM Customers 
WHERE Region = 'Bayern'

CTE, Subquery oder temporäre Tabelle?

CTEs (Common Table Expressions) verbessern die Lesbarkeit erheblich. SQL Server führt sie jedoch meist inline aus – das bedeutet, das Ergebnis wird bei mehrfacher Referenzierung im selben Query unter Umständen mehrfach berechnet. Für große Zwischenergebnisse, die mehrfach verwendet werden, sind temporäre Tabellen oft die bessere Wahl, da sie eigene Statistiken und Indizes erhalten können.

T-SQL — CTE vs. temporäre Tabelle
-- CTE: gut lesbar, sinnvoll bei einmaliger Verwendung 
WITH TopCustomers AS ( 
SELECT CustomerID, SUM(Amount) AS Total 
FROM Orders 
GROUP BY CustomerID 
HAVING SUM(Amount) > 10000 
) 
SELECT c.CompanyName, tc.Total 
FROM TopCustomers tc 
JOIN Customers c ON tc.CustomerID = c.CustomerID; 
 
-- Temporäre Tabelle: besser bei Wiederverwendung oder großen Ergebnismengen 
SELECT CustomerID, SUM(Amount) AS Total 
INTO #TopCustomers 
FROM Orders 
GROUP BY CustomerID 
HAVING SUM(Amount) > 10000; 
 
-- Index auf der Temp-Tabelle verbessert nachfolgende JOINs 
CREATE INDEX IX_TC ON #TopCustomers (CustomerID); 
 
SELECT c.CompanyName, tc.Total 
FROM #TopCustomers tc 
JOIN Customers c ON tc.CustomerID = c.CustomerID; 
 
DROP TABLE #TopCustomers;
Wann Temp-Tabelle, wann CTE? CTE für Lesbarkeit bei einmaliger Verwendung, kleinen Ergebnismengen und rekursiven Abfragen. Temporäre Tabelle wenn das Ergebnis mehrfach genutzt wird, groß ist oder von einem eigenen Index profitiert.

Explizite JOINs verwenden

Die veraltete Syntax mit impliziten Joins (Komma-getrennte Tabellen in FROM mit Join-Bedingung in WHERE) ist schwerer zu lesen und fehleranfälliger – Cross Joins entstehen leicht versehentlich, wenn eine Bedingung vergessen wird.

Veraltet – impliziter Join
SELECT o.OrderID, c.CompanyName 
FROM Orders o, Customers c 
WHERE o.CustomerID = c.CustomerID 
AND o.Status = 'Open'
Korrekt – expliziter INNER JOIN
SELECT o.OrderID, c.CompanyName 
FROM Orders o 
INNER JOIN Customers c 
ON o.CustomerID = c.CustomerID 
WHERE o.Status = 'Open'

Häufige Stolperstellen

MusterProblemEmpfehlung
LIKE '%Wert%' Führendes Wildcard verhindert Index-Nutzung – immer Table Scan Fulltext-Suche prüfen
NOT IN mit Subquery Schlägt bei NULL-Werten lautlos fehl; kann Full Scan erzwingen NOT EXISTS bevorzugen
OR in WHERE Verhindert häufig einheitliche Index-Nutzung UNION ALL in Betracht ziehen
Implizite Typkonvertierung nvarchar vs. varchar – Konvertierung auf jeder Zeile Datentypen angleichen
Cursor / WHILE-Schleife Zeilenweise Verarbeitung statt mengenbasierter Logik Set-basierte Lösung suchen
SELECT DISTINCT Erzwingt Sortierung / Deduplizierung – oft ein JOIN-Problem JOIN-Logik prüfen

Abschnitt 3

Execution Plan Analyse

Der Execution Plan zeigt, wie SQL Server eine Abfrage intern ausführt: welche Indizes verwendet werden, wie Tabellen verknüpft werden und wo der größte Anteil der Gesamtkosten entsteht. Das Lesen von Plänen ist die effektivste Methode zur Diagnose von Leistungsproblemen.

Plan anzeigen lassen

T-SQL — Execution Plan aktivieren
-- Geschätzter Plan (kein Ausführen erforderlich) – SSMS: Strg+L 
SET SHOWPLAN_XML ON; 
GO 
SELECT OrderID, Amount FROM Orders WHERE CustomerID = 42; 
GO 
SET SHOWPLAN_XML OFF; 
 
-- Tatsächlicher Plan (nach Ausführung) – SSMS: Strg+M, dann F5 
-- Alternativ: I/O- und Zeit-Statistiken im Nachrichtenbereich 
SET STATISTICS IO ON; 
SET STATISTICS TIME ON; 
GO 
SELECT OrderID, Amount FROM Orders WHERE CustomerID = 42; 
GO 
SET STATISTICS IO OFF; 
SET STATISTICS TIME OFF;
Geschätzt vs. tatsächlich: Der tatsächliche Plan enthält die realen Zeilenzahlen nach der Ausführung und ist damit bei Parametersniffing-Problemen und falschen Schätzungen deutlich aussagekräftiger als der geschätzte Plan.

Verarbeitungsablauf im Optimizer

01
Parsing
Syntaxprüfung
02
Algebrizer
Objektauflösung
03
Optimizer
Kostenbasiert
04
Execution
Planausführung
05
Plan Cache
Wiederverwendung

Die wichtigsten Plan-Operatoren

Table Scan
Liest die gesamte Tabelle. Kein nutzbarer Index vorhanden.
⚠ Kritisch bei großen Tabellen
Index Seek
Navigiert direkt im B-Baum des Index. Geringer I/O – ideal.
Index Scan
Liest den gesamten Index. Besser als Table Scan, aber prüfenswert.
Key Lookup
Rückgriff auf Basistabelle für fehlende Spalten nach einem Index Seek.
⚠ Covering Index erwägen
Hash Match
Join via Hashtabelle im Arbeitsspeicher (TempDB). Entsteht bei fehlenden Join-Indizes.
⚠ Auf Spills achten
Nested Loops
Effizient für kleine äußere Mengen mit Index auf der inneren Seite.
Merge Join
Verknüpft zwei bereits sortierte Eingaben. Setzt indizierte Join-Spalten voraus.
Sort
Explizites Sortieren bei fehlendem Index auf ORDER BY-Spalten. Teuer.
⚠ TempDB-Spills möglich
Parallelism
Verteilung auf mehrere CPU-Kerne. Gut bei Analytik, oft kontraproduktiv bei OLTP.

Warnzeichen im Plan

Warnung / SymptomBedeutungMaßnahme
Gelbes Ausrufezeichen Fehlende Statistiken oder implizite Typkonvertierung im Plan Statistiken aktualisieren, Datentypen prüfen
Missing Index Hint Optimizer schlägt einen potenziell nützlichen Index vor Vorschlag analysieren, nicht blind anlegen
Estimated vs. Actual Rows stark abweichend Veraltete Statistiken oder Parametersniffing-Problem FULLSCAN, OPTION (RECOMPILE)
Spill to TempDB Arbeitsspeicherzuweisung zu gering für Sort- oder Hash-Operation Statistiken, Indexierung, Memory Grant Hints
Key Lookup / RID Lookup Non-Clustered Index enthält nicht alle benötigten Spalten INCLUDE-Spalten im Index ergänzen
Thick Arrows im Plan Sehr viele Zeilen werden zwischen Operatoren übertragen Filter früher anwenden, Indexierung prüfen

Teuerste Abfragen aus dem Plan Cache

Die DMVs sys.dm_exec_query_stats und sys.dm_exec_sql_text liefern die aktuell teuersten Abfragen direkt aus dem Plan Cache – ein idealer Startpunkt für die systematische Optimierung.

T-SQL — Top 10 teuerste Abfragen
SELECT TOP 10 
qs.total_worker_time / qs.execution_count AS avg_cpu_us, 
qs.total_logical_reads / qs.execution_count AS avg_logical_reads, 
qs.execution_count, 
SUBSTRING(qt.[text], (qs.statement_start_offset / 2) + 1, 
((CASE qs.statement_end_offset 
WHEN -1 THEN DATALENGTH(qt.[text]) 
ELSE qs.statement_end_offset 
END - qs.statement_start_offset) / 2) + 1) AS statement_text 
FROM sys.dm_exec_query_stats qs 
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt 
ORDER BY avg_cpu_us DESC;
Tipp: Die Spalte avg_logical_reads gibt an, wie viele Datenseiten durchschnittlich pro Ausführung gelesen werden. Hohe Werte hier sind ein zuverlässiger Indikator für fehlende oder unpassende Indizes.
MS SQL Server · Abfrageoptimierung · Leitfaden