Wissen

Praxis-Artikel und Buchkapitel zu SQL-Performance, Sicherheit und Hochverfügbarkeit – alle frei verfügbar.

Beratung

Festpreis-Analyse mit Bericht und Handlungsempfehlung – oder strategische Begleitung bei Architektur, Migration und Hochverfügbarkeit.

Fachbücher

Die fünfbändige Reihe „Ulis SQL-Bibliothek“ – Band 1 verfügbar. Leseprobe herunterladen!

Tools

UB.SimSQL: SQL-Server-Lastsimulator mit regelbasierten Konfigurationsempfehlungen. Lokal, ohne Cloud, ohne Abo.

Schulungen

Online-Workshops zu Performance, Sicherheit und Entwicklung – kompakt, hands-on, ohne MOC-Folienschlacht.

Indexe aus Entwicklersicht: – SQL Server Performance

von

Dieser Artikel ist ein Kapitel aus:
SQL Server Performance & Troubleshooting
Praxisleitfaden, ca. 600 Seiten

[ Hier bei Amazon bestellen ]
[ Mehr zum Buch ]

Indexe aus Entwicklersicht:

Was der DBA nicht für dich tun kann — und warum du das selbst verstehen musst

Du kennst das Gespräch: Der DBA kommt mit einem Screenshot aus dem Query Store, zeigt auf eine Abfrage die 40 Sekunden läuft, und sagt: "Kein Index." Du schaust auf deinen Code und denkst: "Das ist doch ein ganz normales SELECT." Ja. Und genau da liegt das Problem.

Indizes sind kein DBA-Thema. Sie sind dein Thema. Als Entwickler baust du die Abfragen, die Indizes brauchen. Du entscheidest welche Spalten ins WHERE kommen und welche ins SELECT. Du weißt welche Abfragen heiß sind — welche 10.000-mal pro Stunde laufen und welche nur beim monatlichen Report. Der DBA sieht Symptome. Du kennst den Kontext.

Kapitel 17 behandelt die DBA-Perspektive: Wartung, Fragmentierung, Rebuild vs. Reorganize. Kapitel 21 erklärt SARGability — ob deine WHERE-Klausel einen Index überhaupt nutzen kann. Dieses Kapitel zeigt dir, wie du den richtigen Index für eine konkrete Abfrage baust, warum Covering Indizes das Wichtigste sind was du heute lernen kannst, und wann du besser gar keinen Index anlegen solltest.

 

Praxisbeispiel: Trendforge Digital: 23 Indizes auf einer Tabelle

Bei Trendforge Digital (Fallstudie Kapitel 34) hatte die zentrale Bestelltabelle 23 Non-Clustered Indizes — darunter Einzelindizes auf fast jede Spalte. Das Ergebnis: Ein INSERT dauerte 340 ms. Nicht, weil die Tabelle groß war, sondern, weil SQL Server 23 B-Tree-Strukturen gleichzeitig aktualisieren musste. Jeder Entwickler hatte "seinen" Index hinzugefügt, wenn eine Abfrage langsam war. Niemand hatte geprüft ob ein ähnlicher Index schon existierte. Nach Konsolidierung auf 7 sorgfältig entworfene Indizes: 18 ms pro INSERT — Faktor 19 schneller.

 

Den richtigen Index für eine Abfrage bauen: Schritt für Schritt

Es gibt eine bewährte Dreischritt-Methode um für eine konkrete Abfrage den optimalen Index zu entwerfen. Sie klingt einfach — ist aber in der Praxis genau das, was die meisten Entwickler überspringen.

Schritt 1: WHERE-Klausel analysieren

Gleichheitsprädikate (Spalte = Wert) kommen zuerst in den Key des Index — sie sind die Weichen im B-Tree, die direkt zur richtigen Stelle springen. Range-Prädikate (Spalte >= Wert, BETWEEN, LIKE 'xyz%') kommen danach. Der Grund: Nachdem SQL Server auf den Gleichheitsbedingungen seek-t, kann er den Rest als kontinuierlichen Bereich durchlaufen. Ein Range-Prädikat als erste Key-Spalte macht den Index für Gleichheitsfilter auf nachfolgende Spalten unbrauchbar.

-- Abfrage die wir optimieren:
SELECT BestellNr, Betrag, LieferDatum
FROM   dbo.Bestellung
WHERE  Status    = N'Aktiv'          -- Gleichheitsprädikat: zuerst in Key
  AND  ErstelltAm >= '2024-01-01';   -- Range-Prädikat: danach in Key

 

-- Richtiger Index: Gleichheit zuerst, dann Range
CREATE NONCLUSTERED INDEX ix_bestellung_status_erstellt
ON dbo.Bestellung (Status, ErstelltAm)
-- SQL Server seekt auf Status='Aktiv', scannt dann Range auf ErstelltAm
-- → sehr effizient

 

-- Falscher Index: Range zuerst, dann Gleichheit
-- CREATE NONCLUSTERED INDEX ix_bestellung_erstellt_status
-- ON dbo.Bestellung (ErstelltAm, Status)
-- SQL Server muss den gesamten Datumsbereich scannen,
-- Status-Filter läuft erst danach → deutlich mehr IO

 

Hinweis: Selektivitäts-Faustregel

Ein Index lohnt sich, wenn die Abfrage weniger als 5% der Zeilen einer Tabelle selektiert. Bei höherer Selektivität entscheidet der Optimizer oft für einen Table Scan — zu Recht, denn viele einzelne Index-Seeks plus Key Lookups können teurer sein als ein sequentieller Full Scan. Diese Grenze ist keine feste Regel: Sie hängt von der Tabellengröße, der Zeilengröße und dem IO-Subsystem ab. Aber als erste Einschätzung funktioniert sie gut.

 

Schritt 2: ORDER BY und GROUP BY berücksichtigen

Wenn deine Abfrage nach den gleichen Spalten sortiert die auch im Index-Key stehen, entfällt der Sort-Operator im Ausführungsplan komplett. Das klingt nach einer kleinen Optimierung — ist es nicht. Ein Sort bei 1 Million Zeilen kostet Memory Grant (Kapitel 12), kann in TempDB spillen, wenn zu wenig Memory bewilligt wird, und blockiert den parallelen Plan-Fluss. Wenn du ORDER BY und WHERE-Spalten im Index kombinieren kannst, tu es.

-- Abfrage mit Sortierung:
SELECT KundenID, ErstelltAm, Betrag
FROM   dbo.Bestellung
WHERE  Status = N'Aktiv'
ORDER BY ErstelltAm DESC;

 

-- Index der Sort eliminiert:
CREATE NONCLUSTERED INDEX ix_bestellung_status_erstellt_desc
ON dbo.Bestellung (Status, ErstelltAm DESC)
-- ErstelltAm DESC passt zur ORDER BY-Richtung
-- → SQL Server liefert bereits sortiert aus dem Index
-- → kein Sort-Operator im Plan notwendig

Schritt 3: SELECT-Spalten prüfen — brauche ich INCLUDE?

Wenn dein SELECT Spalten enthält die nicht im Index-Key stehen, muss SQL Server für jede gefundene Zeile zur Basistabelle zurückspringen — der Key Lookup. Diesen Sprung siehst du in Kapitel 15 als teuren Operator im Ausführungsplan. Die Lösung: INCLUDE-Spalten. Sie werden im Blattknoten des Index mitgespeichert, ohne den B-Tree aufzublähen. So kann SQL Server alle SELECT-Spalten direkt aus dem Index lesen — kein Lookup, kein zusätzliches IO.

Covering Index: Das wichtigste Konzept für Entwickler

Ein Covering Index enthält alle Spalten die eine Abfrage benötigt — sowohl für den Filter (Key Columns) als auch für das Ergebnis (INCLUDE Columns). Wenn SQL Server einen Covering Index für eine Abfrage findet, muss er die Basistabelle überhaupt nicht mehr anfassen. Das ist kein marginaler Unterschied — das sind Größenordnungen.

-- Testaufbau: Tabelle mit 5 Millionen Zeilen

 

-- Abfrage ohne Covering Index:
SET STATISTICS IO ON;

 

SELECT BestellNr, Betrag, LieferDatum
FROM   dbo.Bestellung
WHERE  KundenID = 4711;

 

-- Ausgabe STATISTICS IO:
-- Table 'Bestellung'. Scan count 1,
-- logical reads 48274
-- → 48.274 Pages gelesen, weil Key Lookup jede Zeile einzeln holt

 

-- Jetzt Covering Index anlegen:
CREATE NONCLUSTERED INDEX ix_bestellung_kundeid_cover
ON dbo.Bestellung (KundenID)              -- Key: Suchspalte
INCLUDE (BestellNr, Betrag, LieferDatum); -- INCLUDE: alle SELECT-Spalten

 

-- Gleiche Abfrage nochmal:
SELECT BestellNr, Betrag, LieferDatum
FROM   dbo.Bestellung
WHERE  KundenID = 4711;

 

-- Ausgabe STATISTICS IO nach Index:
-- Table 'Bestellung'. Scan count 1,
-- logical reads 342
-- → Von 48.274 auf 342 Logical Reads: Faktor ~141 weniger IO

48.274 Logical Reads auf 342 — das ist kein Tuning, das ist eine Transformation. In der Praxis sieht man solche Faktoren regelmäßig, wenn ein Key Lookup durch INCLUDE eliminiert wird. Das ist auch der Grund, warum Kapitel 15 den Key Lookup als einen der kritischsten Operatoren im Ausführungsplan markiert: Er sieht harmlos aus (ein kleines Dreieck-Symbol), kostet aber bei hohen Zeilenzahlen massiv IO.

 

Definition: Key vs. INCLUDE: Was wohin?

Key Columns: Spalten in WHERE, JOIN, ORDER BY, GROUP BY — sie bestimmen die Sortierreihenfolge des B-Tree und ermöglichen gezielte Seeks.

INCLUDE Columns: Spalten nur in SELECT — sie werden im Blattknoten gespeichert, ohne den B-Tree zu beeinflussen. Kein Such-Overhead, aber Speicherbedarf.

Niemals eine Spalte sowohl in Key als auch INCLUDE aufführen — das ist redundant und erzeugt eine Warnung.

Spalten die nie in WHERE/JOIN/ORDER BY auftauchen, gehören immer ins INCLUDE — nicht in den Key.

 

Index-Reihenfolge: Warum (Status, Datum) nicht dasselbe ist wie (Datum, Status)

Bei Composite-Indizes — also Indizes über mehrere Spalten — ist die Reihenfolge der Key-Spalten entscheidend. Der B-Tree sortiert erst nach der ersten Spalte, dann innerhalb gleicher Werte nach der zweiten. Das hat eine wichtige Konsequenz: SQL Server kann nur dann direkt auf eine Spalte seekten, wenn alle vorangehenden Spalten durch Gleichheitsprädikate gebunden sind.

 

WHERE-Klausel

Index (Status, Datum)

Index (Datum, Status)

Status = 'Aktiv'

Index Seek

Index Scan

Datum >= '2024-01-01'

Index Scan (Range)

Index Seek auf Range

Status = 'Aktiv' AND Datum >= '2024-01-01'

Seek + Range optimal

Range Seek, dann Status-Filter

Datum BETWEEN … AND …

Scan nötig (Status fehlt)

Index Seek auf Range

Composite-Index: Nutzbarkeit abhängig von Spaltenreihenfolge

 

Die Regel ist einfach: Gleichheitsprädikate kommen vor Range-Prädikaten. Wenn du typischerweise nach Status = 'Aktiv' filterst und dann zusätzlich nach Datum eingrenzt, ist (Status, Datum) der richtige Index. SQL Server seekt direkt auf den Status-Wert und scannt dann den Datumsbereich — sehr effizient. Umgekehrt müsste SQL Server den gesamten Datumsbereich scannen und danach Status filtern — viel mehr Arbeit.

 

Hinweis: Erinnerst du dich an SARGability aus Kapitel 21?

Ein perfekt entworfener Composite-Index hilft nichts, wenn die WHERE-Klausel nicht SARGable ist. CAST(ErstelltAm AS DATE) = @Datum zerstört die Nutzbarkeit des Index auf ErstelltAm vollständig — egal wie gut der Index gebaut ist. Index-Design und SARGability sind zwei Seiten derselben Medaille. Immer beides prüfen.

 

Wann Indizes nicht helfen — und sogar schaden

Kleine Tabellen

Bei Tabellen mit weniger als 1.000 Zeilen ist ein Full Table Scan fast immer schneller als ein Index Seek + Key Lookup. Der Overhead für die Index-Traversierung lohnt sich nicht. SQL Server weiß das — und ignoriert deinen Index oft selbst, wenn er existiert. Konfigurationstabellen, Lookup-Tabellen, Referenzdaten: Kein Index nötig. Der Optimizer macht das richtig.

Niedrige Selektivität

Eine Spalte "Geschlecht" mit zwei Werten in 50/50-Verteilung ist für einen Index wertlos. Ein Index Seek findet 50% der Zeilen — dann muss SQL Server 50% der Basistabelle per Key Lookup laden. Das ist teurer als ein direkter Table Scan. Gleiches gilt für Status-Spalten mit wenigen Werten und ausgeglichener Verteilung: "Aktiv" bei 60%, "Inaktiv" bei 40% — kein Index-Nutzen.

Volatil aktualisierte Spalten

Jedes UPDATE auf eine indizierte Spalte bedeutet: SQL Server entfernt die Zeile aus ihrer alten Position im Index und fügt sie an der neuen ein. Bei einer Spalte "ZuletztGeändert" die bei jedem UPDATE mitgesetzt wird, passiert das bei jeder Zeilen-Änderung. Index-Overhead für eine Spalte die du selten filterst, aber ständig beschreibst — eine schlechte Investition.

Wenn die ganze Tabelle gebraucht wird

Report-Abfragen die 80% der Tabelle aggregieren, Analytics-Queries über den gesamten Datenbestand, Batch-Jobs die alle Zeilen verarbeiten: Hier hilft kein Index. Im Gegenteil — ein Columnstore Index (Kapitel 17) wäre die richtige Antwort für OLAP-Zugriffsmuster auf OLTP-Tabellen. Non-Clustered Indizes sind für selektive Zugriffe gemacht, nicht für Full-Table-Analytics.

Index und ORM: Eine komplizierte Beziehung

ORM-Frameworks wie Entity Framework erzeugen SQL — und dieses SQL ist oft nicht das SQL, das du schreiben würdest. Das hat direkte Konsequenzen für deine Index-Strategie. Ein manuell optimierter Covering Index hilft nichts, wenn der ORM eine Query generiert die ihn nicht nutzen kann.

SELECT * macht Covering Indizes unmöglich

Entity Framework lädt standardmäßig alle Spalten einer Entität — das entspricht SELECT * auf Datenbankebene. Ein Covering Index der genau die drei Spalten enthält die du brauchst, wird ignoriert — weil der ORM fünfzehn weitere Spalten will. Ergebnis: Jeder Index-Seek endet mit einem Key Lookup auf alle Spalten. Das lässt sich durch explizite Projektion lösen: .Select(x => new { x.BestellNr, x.Betrag, x.LieferDatum }) statt .FirstOrDefault(). Mehr dazu in Kapitel 30.

N+1 und Indizes: Auch tausend Index-Seeks sind zu viele

Das N+1-Problem (Kapitel 30) ist der häufigste ORM-Fehler: Eine Abfrage lädt 1.000 Bestellungen, dann führt der ORM für jede Bestellung eine separate Abfrage für den Kunden aus — macht 1.001 Datenbankaufrufe statt einem. Und jetzt kommt der Index ins Spiel: Auch mit perfektem Index auf KundenID sind 1.000 Einzelabfragen deutlich teurer als ein einziger JOIN. Ein guter Index rettet dich nicht vor schlechtem Abfrage-Design.

-- ORM generiert diesen Code bei N+1-Problem:
-- (vereinfacht — tatsächliches SQL variiert je nach Framework)

 

-- Abfrage 1: Alle Bestellungen laden
SELECT * FROM dbo.Bestellung WHERE Status = N'Aktiv';
-- → Ergebnis: 1.000 Zeilen

 

-- Abfragen 2 bis 1001: Für jede Bestellung den Kunden laden
SELECT * FROM dbo.Kunde WHERE KundenID = 1;
SELECT * FROM dbo.Kunde WHERE KundenID = 2;
-- ... 998 weitere Abfragen ...

 

-- Auch mit Index auf KundenID: 1.000 Roundtrips zum Server
-- Besser: Einen einzigen JOIN mit EF .Include(b => b.Kunde)
-- Oder noch besser: Explizite SQL-Abfrage mit JOIN schreiben

ORM-generierte CAST-Fallen und SARGability

Entity Framework und andere ORMs generieren manchmal Abfragen die SARGability-Killer einbauen ohne, dass du es merkst. Ein Datum-Vergleich im ORM der intern als CAST(ErstelltAm AS DATE) = @p landet, zerstört jeden Index auf ErstelltAm. Trendforge hatte genau dieses Muster: Das ORM-Framework erzeugte WHERE CAST(Datum AS DATE) = @p für jeden Datums-Filter — alle 14 Datum-Indizes auf der Haupttabelle wertlos. Wie das zu beheben ist erklärt Kapitel 21 im Detail.

Indizes testen: Vorher-Nachher mit SET STATISTICS IO

Bevor du einen Index anlegst: Messen. Nachdem du ihn angelegt hast: Nochmal messen. SET STATISTICS IO ist dein wichtigstes Werkzeug dafür — es zeigt dir wie viele Seiten SQL Server lesen musste, was direkt proportional zur Abfrage-Dauer ist.

-- Vollständige Diagnose-Sequenz für Index-Evaluierung

 

-- Schritt 1: Messung OHNE Index
SET STATISTICS IO ON;
SET STATISTICS TIME ON;

 

SELECT KundenID, ErstelltAm, Betrag, LieferDatum
FROM   dbo.Bestellung
WHERE  KundenID = 4711
  AND  ErstelltAm >= '2024-01-01';

 

-- Notiere: "logical reads" aus der Ausgabe
-- Notiere: "elapsed time" aus der Ausgabe
-- Öffne Actual Execution Plan (Strg+M in SSMS) und notiere Operator-Typen

 

-- Schritt 2: Index anlegen
CREATE NONCLUSTERED INDEX ix_bestellung_kunde_erstellt
ON dbo.Bestellung (KundenID, ErstelltAm)   -- Seek-Spalten: Gleichheit zuerst
INCLUDE (Betrag, LieferDatum);              -- Output-Spalten: kein Lookup nötig

 

-- Schritt 3: Gleiche Abfrage nochmal messen
SELECT KundenID, ErstelltAm, Betrag, LieferDatum
FROM   dbo.Bestellung
WHERE  KundenID = 4711
  AND  ErstelltAm >= '2024-01-01';

 

-- Vergleiche logical reads: sollte deutlich weniger sein
-- Execution Plan: Index Seek statt Table Scan?
-- Key Lookup weg? (weil INCLUDE alle SELECT-Spalten abdeckt)

 

-- Schritt 4: Existierende Missing Index-Hinweise für diese Tabelle prüfen
SELECT
    mid.statement                AS Tabelle,
    mid.equality_columns         AS EqualitySpalten,
    mid.inequality_columns       AS InequalitySpalten,
    mid.included_columns         AS IncludeSpalten,
    migs.avg_user_impact         AS ImpactProzent,
    migs.user_seeks + migs.user_scans AS Aufrufe
FROM sys.dm_db_missing_index_details      AS mid
JOIN sys.dm_db_missing_index_groups       AS mig
    ON mid.index_handle = mig.index_handle
JOIN sys.dm_db_missing_index_group_stats  AS migs
    ON mig.index_group_handle = migs.group_handle
WHERE mid.statement LIKE '%Bestellung%'   -- nur für unsere Tabelle
ORDER BY migs.avg_user_impact DESC;
-- Hoher Impact + hohe Aufrufe = Kandidat für neuen Index
-- Aber: Prüfen ob neuer Index den bestehenden ix_bestellung_kunde_erstellt
-- nicht bereits abdeckt — nicht blind einen zweiten anlegen

 

Tipp: Musterwerk GmbH: Index-Review als Routine

Bei Musterwerk GmbH (Fallstudie Kapitel 32) hat das Team eine einfache Regel eingeführt: Jede neue Stored Procedure wird mit SET STATISTICS IO ON getestet, bevor sie in Produktion geht. Wenn logical reads > 10.000 für eine selektive Abfrage, wird ein Covering Index geprüft. Das kostet beim Code-Review 10 Minuten — und verhindert Notfall-Aktionen um 3 Uhr nachts.

 

Diagnose: Index-Probleme erkennen und beheben

 

Hinweis: Symptome: Woran erkennst du Index-Probleme?

Key Lookup im Actual Execution Plan: Ein kleines Loop-Symbol das von einem Index Seek zur Basistabelle zurückspringt. Bei mehr als einigen hundert Zeilen ist das ein klares Signal für fehlende INCLUDE-Spalten.

Hohe Logical Reads für kleine Ergebnismengen: SET STATISTICS IO zeigt 50.000 reads für eine Abfrage die 20 Zeilen zurückgibt — Index fehlt oder ist nicht SARGable.

Gelber Hinweis-Balken in SSMS über dem Execution Plan: "Missing Index (Impact XX%)." SQL Server hat während der Ausführung einen besseren Index identifiziert.

Index Scan statt Index Seek: Der vorhandene Index wird genutzt, aber komplett durchlaufen statt gezielt angesprungen. Ursache: SARGability-Verletzung (Kapitel 21) oder falsche Spaltenreihenfolge im Composite Index.

Langsame INSERTs/UPDATEs: Schreiboperationen werden langsamer, obwohl die Datenmenge konstant bleibt. Verdacht: Zu viele Indizes auf der Tabelle.

 

 

Hinweis: So misst du das: SET STATISTICS IO + Execution Plan

SET STATISTICS IO ON; SET STATISTICS TIME ON; — vor der Abfrage einschalten

Im Ergebnis: "Table 'X'. Scan count N, logical reads M" — logical reads ist die Kernzahl.

Richtwerte: < 1.000 reads für selektive Abfragen = gut. > 10.000 = Index fehlt oder falsch. > 100.000 für kleine Ergebnismengen = dringend handeln.

Actual Execution Plan (Strg+M in SSMS): Operator-Typen prüfen. Index Seek = gezielter Zugriff (gut). Index Scan = vollständiger Index-Durchlauf (meist schlecht). Table Scan = kein passender Index (schlecht).

Pfeildicker im Plan = mehr Zeilen: Dickere Pfeile nach einem Filter-Operator deuten auf Kardinalitätsschätzungsfehler hin (Kapitel 16).

sys.dm_db_missing_index_details: Zeigt alle Missing-Index-Empfehlungen seit letztem Restart. Impact-Wert > 80% und hohe Aufrufzahlen = Kandidat für Index-Anlage.

 

 

Warnung: Typische Fehlinterpretationen bei Index-Problemen

"Der Optimizer weiß schon welchen Index er nutzen soll" — nur, wenn der Index existiert, SARGable ist und die Statistiken aktuell sind. Alle drei Bedingungen müssen erfüllt sein.

"Mehr Indizes schaden nicht" — doch. Jeder Index kostet bei jedem INSERT, UPDATE und DELETE. Auf schreibintensiven Tabellen kann das den Schreib-Durchsatz halbieren.

"Missing Index-Empfehlung einfach umsetzen" — SQL Server kennt nur diese eine Abfrage. Er weiß nicht ob ein ähnlicher Index schon existiert, wie selten die Abfrage wirklich läuft, oder wie schreibintensiv die Tabelle ist. Empfehlung als Hinweis verstehen, nicht als Anweisung.

"Index Scan ist fast so gut wie Index Seek" — bei kleinen Tabellen ja. Bei großen Tabellen kann ein Index Scan genauso teuer sein wie ein Table Scan — der B-Tree wird komplett durchlaufen.

"Full Text Index löst mein LIKE '%Suchbegriff%'-Problem" — LIKE mit führendem Wildcard ist auch mit Volltextindex nicht durch einen normalen Non-Clustered Index zu lösen. Das ist ein Architekturproblem, kein Index-Problem.

 

 

Tipp: Erste Gegenmaßnahmen bei Index-Problemen

1. Execution Plan analysieren: Key Lookup identifizieren → fehlende Spalten als INCLUDE ergänzen. Das ist in 60% der Fälle die schnellste Lösung.

2. sys.dm_db_missing_index_details abfragen → Top-3 nach Impact*Aufrufe prüfen. Existiert schon ein ähnlicher Index der leicht angepasst werden könnte?

3. SET STATISTICS IO ON und Abfrage messen — logical reads als Baseline notieren. Nach Index-Anlage vergleichen.

4. Bei Schreib-Problemen: sys.dm_db_index_usage_stats prüfen. Spalte user_seeks = 0 und user_updates > 1.000.000 = Index wird nie zum Suchen genutzt, kostet aber bei jedem Write. Kandidat zum Löschen.

5. Duplikate prüfen, bevor ein neuer Index angelegt wird: Kann der neue Index einen bestehenden ersetzen oder erweitern? Zwei Indizes auf (KundenID) und (KundenID, ErstelltAm) — der erste ist redundant, wenn alle Abfragen auf KundenID auch ErstelltAm filtern.

 

Zusammenfassung

Indizes sind das mächtigste Werkzeug das ein Entwickler für die Datenbankperformance hat — und gleichzeitig die häufigste Quelle selbstverschuldeter Probleme. Die wichtigsten Punkte aus diesem Kapitel:

  • Gleichheitsprädikate zuerst in den Index-Key, dann Range-Prädikate. Diese Reihenfolge entscheidet ob SQL Server seekten oder scannen muss.
  • Covering Index = Key Columns für WHERE, INCLUDE Columns für SELECT. Ein Covering Index macht Key Lookups überflüssig — der Unterschied kann Faktor 100+ bei den Logical Reads sein.
  • Missing Index-Hints sind Ausgangspunkte, keine Anweisungen. Immer prüfen: Schreiblast, bestehende ähnliche Indizes, tatsächliche Aufrufhäufigkeit.
  • ORM-generierte Abfragen sind oft Index-feindlich: SELECT * verhindert Covering Indizes, CAST-Ausdrücke zerstören SARGability, N+1 macht jeden guten Index sinnlos.
  • Weniger Indizes, sorgfältig entworfen, schlagen immer eine große ungepflegte Sammlung — wie Trendforge Digital mit 23 Indizes auf einer Tabelle eindrucksvoll bewiesen hat.
  • SET STATISTICS IO ON ist dein Messgerät. Logical Reads vor und nach einer Index-Anlage — alles andere ist Spekulation.
  •  

    Kapitel 23 verlässt die Index-Ebene und schaut auf die Abfragen selbst: Warum mengenbasiertes Denken fast immer gewinnt, warum Cursor eine toxische Beziehung mit SQL Server eingehen, und welche Anti-Patterns du aus deinem Code verbannen solltest — darunter einige die auf den ersten Blick völlig unschuldig aussehen.

     

    Abb. 1: Covering Index: Aufbau und Nutzen

     

    Kapitel 23