Consulting, Beratung
Tutorial: SQL Server-Indizes für EntwicklerEinführung: Dieser Fachartikel richtet sich an Entwickler mit Grundkenntnissen in Microsoft SQL Server und bietet eine umfassende Einführung in das Thema Indizes. Wir beleuchten, was Indizes sind und warum sie für die Performance einer Datenbank entscheidend sind. Anschließend werden die verschiedenen Index-Typen in SQL Server (z. B. gruppierte, nicht gruppierte, Columnstore-, gefilterte, XML- und räumliche Indizes etc.) mit praktischen Beispielen erläutert. Sie erfahren, wie Indizes per T-SQL erstellt werden und wie man Abfragepläne analysiert, um die Indexnutzung zu bewerten. Außerdem geben wir Best Practices für eine effektive Indexstrategie – etwa zum Vermeiden von Über-Indizierung, zur Index-Wartung und Fragmentierungsbekämpfung. Wir zeigen, wie Sie Indizes dynamisch an veränderte Datenmuster anpassen können, um stets optimale Abfrageleistung zu erzielen. Abschließend betrachten wir Besonderheiten und Neuerungen rund um Indizes ab SQL Server 2019 (z. B. Resumable Index Operations, OPTIMIZE_FOR_SEQUENTIAL_KEY und andere Verbesserungen). Zahlreiche praxisnahe, kommentierte T-SQL-Beispiele veranschaulichen die Konzepte, und in einer FAQ-Sektion beantworten wir 20 häufig gestellte Fragen verständlich.
1. Grundbegriffe: Was sind Indizes und warum sind sie wichtig?
Ein Index in SQL Server ist eine spezielle Datenstruktur, die das schnelle Auffinden von Datensätzen in einer Tabelle ermöglicht – ähnlich dem Index am Ende eines Buches, der Sie zu den Seiten mit einem bestimmten Stichwort führt. Ohne Index müsste SQL Server bei einer Abfrage (z. B. „Finde alle Kunden mit dem Nachnamen Müller“) jede Zeile der Tabelle durchsuchen (Full Table Scan), was bei großen Tabellen sehr zeitaufwändig ist. Ein Index speichert dagegen die Werte eines oder mehrerer ausgewählter Spalten in sortierter Form und verweist zu jeder Wert-Ausprägung auf die entsprechenden Datenseiten oder Zeilen der Tabelle. Durch diese vorsortierte Struktur kann die Datenbank-Engine gesuchte Werte wesentlich schneller lokalisieren, ähnlich wie man in einem Telefonbuch direkt zum Nachnamen springt anstatt jedes Blatt durchzugehen.
Wie Indizes funktionieren: Die meisten Indizes in SQL Server basieren intern auf einer B-Baum-Struktur (genauer: B+Tree). Das bedeutet, die Indexdaten sind hierarchisch organisiert: Es gibt eine Wurzel-Seite (root page), darunter evtl. mehrere Ebenen von Zwischenknoten, und am Ende die Blatt-Ebene, die Zeiger auf die eigentlichen Tabellendaten enthält (bzw. bei bestimmten Index-Typen auch direkt die Daten). Durch diese Baumstruktur kann die Datenbank eine gesuchte Schlüsselinformation in wenigen Schritten finden – jeder Schritt verengt die Suche, ähnlich wie man in einem alphabetisch sortierten Lexikon mit einem Finger immer weiter eingrenzt, bis man beim Zielwort landet. Somit reduziert ein Index drastisch die Anzahl der Datenseiten, die gelesen werden müssen, um ein Ergebnis zu finden.
Warum sind Indizes wichtig? Indizes sind einer der wichtigsten Hebel zur Performance-Optimierung von Datenbankabfragen. Sie ermöglichen z. B.:
- Schnelleres Filtern und Suchen: Abfragen mit WHERE-Bedingungen, JOINS oder Range-Queries (BETWEEN) können dank Index oft als Index Seek ausgeführt werden (gezielte Suche auf dem Index), anstatt als langsamer Table Scan.
- Schnelleres Sortieren und Gruppieren: Wenn eine ORDER BY- oder GROUP BY-Klausel auf indizierten Spalten basiert, kann SQL Server die Sortierung/Gruppierung effizienter durchführen, da die Daten bereits (teilweise) vorsortiert vorliegen.
- Verbesserte Join-Performance: Passende Indizes auf Join-Schlüsseln (z. B. Fremdschlüssel-Spalten) ermöglichen es, die verbundenen Zeilen schnell zu finden.
Allerdings kommen Indizes nicht umsonst: Nachteile und Trade-offs sind mit zu berücksichtigen. Jeder Index verbraucht zusätzlichen Speicherplatz auf der Festplatte und im Arbeitsspeicher (Cache), da er eine separate Kopie von Schlüsseldaten hält. Vor allem aber können Indizes Schreiboperationen verlangsamen – bei jedem INSERT, DELETE oder UPDATE einer indizierten Tabelle muss SQL Server auch alle entsprechenden Indizes aktualisieren. Daher ist eine ausgewogene Indexstrategie wichtig: Man möchte genügend Indizes haben, um Leseabfragen zu beschleunigen, aber nicht so viele, dass Schreibvorgänge übermäßig gebremst werden oder Indexpflege zum Alptraum wird. Im Laufe der folgenden Abschnitte gehen wir darauf ein, welche Arten von Indizes es gibt und wie man sie optimal einsetzt.
Bevor wir ins Detail gehen: Eine Tabelle ohne Clustered Index wird als Heap bezeichnet. Heaps haben keine bestimmte Sortierung der Datenzeilen. Das kann in manchen Fällen ausreichend sein, ist aber für viele Abfragen ineffizient. Meist definiert man mindestens einen Index (typischerweise einen gruppierten Index) pro Tabelle, um die physische Ordnung der Daten festzulegen. Im nächsten Abschnitt schauen wir uns die verschiedenen Index-Typen im SQL Server genauer an.
2. Index-Typen in SQL Server (Clustered, Nonclustered, Columnstore, Filtered, XML, Spatial, etc.)
SQL Server bietet eine Reihe von Index-Typen, um unterschiedlichen Anwendungsfällen gerecht zu werden. Im Folgenden stellen wir die wichtigsten Indexarten vor – von den klassischen B-Baum-Indizes (gruppiert/nicht gruppiert) bis zu spezielleren Formen wie Columnstore-, XML- oder räumlichen Indizes. Zu jedem Typ gibt es praktische Beispiele in T-SQL.
Clustered Index (Gruppierter Index)
Ein gruppierter Index bestimmt die physische Anordnung der Datenzeilen in der Tabelle nach den Werten des Indexschlüssels. Man kann sich einen gruppierten Index als die „Hauptsortierung“ der Tabelle vorstellen. Es kann pro Tabelle nur einen gruppierten Index geben, da die Zeilen nicht auf zwei verschiedene Arten gleichzeitig sortiert gespeichert werden können. Wenn eine Tabelle einen gruppierten Index besitzt, spricht man auch von einer geordneten Tabelle (clustered table); hat sie keinen, ist es ein Heap.
Merkmale eines Clustered Index: – Die Daten der Tabelle selbst liegen in der Blattebene des Index. Das bedeutet, die Index-Blätter enthalten nicht nur Schlüsselwerte, sondern alle Spalten der Tabelle (oder zeigen direkt auf komplette Datenseiten). Daher entfällt bei einem Clustered Index das separate Nachschlagen der Daten – ein Clustered Index Seek liefert direkt die gewünschten Zeilen. – Standardmäßig erstellt SQL Server beim Anlegen eines Primary Key automatisch einen gruppierten Index auf der Primärschlüssel-Spalte (sofern nicht anders angegeben). Primärschlüssel sind eindeutig und eignen sich meist gut als Clustered-Index-Schlüssel. – Typischerweise wählt man als Clustered-Index-Spalte einen Wert, der einzigartig, nicht NULL, möglichst monoton steigend (z. B. eine Identity-ID oder Zeitstempel) und oft für Bereichsabfragen genutzt ist. Monoton steigende Werte (wie Identitäten oder Zeitstempel) haben den Vorteil, dass neue Einfügungen immer am Ende erfolgen und nicht mitten in der Tabelle einsortiert werden müssen, was weniger Seitenaufteilungen und Fragmentierung verursacht.
Beispiel: Angenommen wir haben eine Tabelle Orders mit Bestellungen. Wir wollen, dass die Daten physisch nach OrderID sortiert gespeichert sind, da das unsere Primärschlüssel-Spalte ist. Man erstellt den Clustered Index entweder implizit über den Primary Key oder explizit per CREATE INDEX:
— Tabelle mit Primärschlüssel, der als gruppierter Index dient
CREATE TABLE dbo.Orders (
OrderID INT PRIMARY KEY, — Erstellt implizit einen gruppierten Index auf OrderID
CustomerID INT,
OrderDate DATE,
TotalAmount DECIMAL(10,2)
);
Im obigen Beispiel wird mit dem Primary Key auf OrderID automatisch ein gruppierter Index (PK_Orders_OrderID) angelegt. Alternativ kann man auch manuell einen Clustered Index auf einer oder mehreren Spalten erstellen. Zum Beispiel könnten wir entscheiden, die Tabelle physisch nach dem Datum zu sortieren, falls viele Abfragen nach OrderDate filtern:
— Einen gruppierten Index nachträglich auf OrderDate erstellen (falls noch keiner existiert)
CREATE CLUSTERED INDEX IX_Orders_OrderDate
ON dbo.Orders(OrderDate);
Hinweis: Pro Tabelle darf es nur einen gruppierten Index geben. Legt man – wie oben – einen neuen Clustered Index an und es existierte bereits einer (z. B. vom PK), würde der alte automatisch in einen nicht gruppierten Index umgewandelt oder man müsste ihn vorher löschen. Der gruppierte Index macht die Tabelle faktisch zur sortierten Reihenfolge nach dem Schlüssel; alle nicht gruppierten Indizes (siehe unten) referenzieren intern den Gruppierwert oder einen Zeilenzeiger, um an die eigentliche Zeile zu gelangen.
Nonclustered Index (Nicht gruppierter Index)
Ein nicht gruppierter Index ist ein separater Index, der neben der eigentlichen Tabelle existiert. Er enthält die indexierten Schlüsselspalten und Zeiger (Pointer) auf die dazugehörigen Datenzeilen in der Tabelle. Anders als der gruppierte Index beeinflusst ein nicht gruppierter Index nicht die physische Sortierung der Tabelle; man kann daher mehrere nicht gruppierte Indizes pro Tabelle erstellen (SQL Server erlaubt technisch bis zu 999 nicht gruppierte Indizes pro Tabelle ab aktuellen Versionen, aber praktikabel sind deutlich weniger – siehe Best Practices). Jeder nicht gruppierte Index hat seine eigene B-Baum-Struktur.
Merkmale eines Nonclustered Index: – Die Blattebene eines nicht gruppierten Index enthält Einträge bestehend aus dem Indexschlüssel und einem Zeilen-Locator. Dieser Locator verweist entweder auf den Primärschlüssel/Clustered-Index-Schlüssel der Tabelle (wenn die Tabelle einen Clustered Index hat) oder direkt auf den Datenzeiger der Heap-Tabelle (wenn es keinen Clustered Index gibt). So kann SQL Server von einem Indexeintrag zur vollständigen Datenzeile gelangen. – Ein nicht gruppierter Index eignet sich hervorragend, um Abfragen zu beschleunigen, die nach bestimmten Spalten filtern oder diese in JOINs/Aggregationen verwenden, sofern der Index die betreffenden Spalten enthält. Wenn z. B. viele Abfragen eine Kundentabelle nach LastName durchsuchen, ist ein Index auf LastName sinnvoll. – Man kann beliebig viele (bis zum technischen Limit) nicht gruppierte Indizes anlegen, sollte aber aus Performance-Gründen abwägen, welche wirklich nötig sind (jede zusätzliche Indexstruktur kostet Speicher und wirkt sich auf Schreiboperationen aus).
Beispiel: Wir erstellen auf der Orders-Tabelle einen nicht gruppierten Index auf CustomerID, damit Abfragen nach CustomerID schneller ausgeführt werden können:
— Nicht gruppierter Index auf CustomerID, um Abfragen nach Kunde zu beschleunigen
CREATE NONCLUSTERED INDEX IX_Orders_Customer
ON dbo.Orders(CustomerID);
Mit diesem Index können Abfragen wie SELECT * FROM Orders WHERE CustomerID = 12345 deutlich schneller sein, da SQL Server einen Index Seek auf IX_Orders_Customer durchführen kann, um alle OrderIDs (bzw. Zeiger) des Kunden 12345 zu finden, anstatt die gesamte Orders-Tabelle zu durchsuchen.
Ein potenzieller Nachteil tritt auf, wenn die Abfrage weitere Spalten aus der Tabelle benötigt, die nicht im Index enthalten sind. Im obigen Beispiel enthält IX_Orders_Customer zunächst nur die CustomerID als Schlüssel; wenn die Abfrage zusätzlich z. B. OrderDate und TotalAmount selektiert, muss SQL Server für jede gefundene OrderID noch die restlichen Spalten aus der eigentlichen Tabelle nachschlagen. Dies geschieht über einen Key Lookup (bei Clustered Table) bzw. RID Lookup (bei Heap). Diese Lookups sind zusätzliche I/O-Operationen und können die Performance schmälern, insbesondere wenn viele Zeilen betroffen sind.
Tipp – überdeckende Indizes: Um solche Lookups zu vermeiden, kann ein nicht gruppierter Index mit zusätzlichen Spalten erweitert werden, die nicht Teil des Such-Schlüssels sind, aber häufig von Abfragen benötigt werden. Diese nennt man included columns (enthaltene Spalten). Ein Index, der alle in einer Abfrage benötigten Spalten abdeckt (sei es als Schlüssel oder als Included Columns), wird als Covering Index (überdeckender Index) bezeichnet, da die Abfrage komplett vom Index „bedient“ werden kann, ohne auf die Basistabelle zuzugreifen.
Beispiel mit Included Columns: Angenommen, häufige Abfragen filtern nach CustomerID und möchten OrderDate und TotalAmount wissen (z. B. “Alle Bestellungen eines bestimmten Kunden mit Datum und Summe”). Wir können den obigen Index verbessern:
— Nicht gruppierter Index mit enthaltenen Spalten, um die Abfrage vollständig abzudecken
CREATE NONCLUSTERED INDEX IX_Orders_Customer_Include
ON dbo.Orders(CustomerID)
INCLUDE (OrderDate, TotalAmount);
Hier ist CustomerID weiterhin der Indexschlüssel (nach dem gesucht/sortiert wird), aber OrderDate und TotalAmount werden als Nicht-Schlüssel-Spalten im Index mit abgelegt. Dadurch enthält der Index alle drei Spalten. Eine Abfrage SELECT OrderDate, TotalAmount FROM Orders WHERE CustomerID = 12345 kann nun allein mit diesem Index beantwortet werden (Index Seek auf CustomerID und direkt die Werte OrderDate/TotalAmount aus dem Index lesen), ohne zurück zur Tabelle springen zu müssen. Der Index überdeckt diese Abfrage vollständig.
Zusammenfassend: Nicht gruppierte Indizes sind flexible Werkzeuge zur Beschleunigung von Abfragen. Man sollte sie gezielt dort einsetzen, wo sie einen häufigen Nutzen bringen, und mit Included Columns dafür sorgen, dass wichtige Abfragen möglichst keine teuren Lookups benötigen. Im nächsten Abschnitt beleuchten wir weitere spezielle Indexformen.
Columnstore Index (Spaltenstore-Index)
Ein Columnstore-Index ist eine besondere Indexform, die Daten spaltenorientiert statt zeilenorientiert speichert. Klassische Indizes (und auch die eigentlichen Tabellen) liegen physisch in Rowstore-Form vor, d. h. jede Datenzeile wird zusammenhängend gespeichert. Ein Columnstore hingegen legt die Werte jeder Spalte getrennt ab (alle Werte einer Spalte zusammen, stark komprimiert). Dieses Prinzip eignet sich hervorragend für analytische Workloads und Data Warehousing, wo häufig Aggregationen oder Scans über viele Zeilen, aber wenige Spalten gefahren werden.
Wichtige Punkte zu Columnstore-Indizes: – Spaltenorientierte Speicherung & Komprimierung: Durch die spaltenweise Speicherung wiederholen sich Werte oft, was eine sehr hohe Kompressionsrate ermöglicht (häufig x7 oder mehr gegenüber unkomprimierten Daten). Dadurch werden viel weniger Daten von Disk gelesen. Zudem kann die CPU moderne Vectorized Execution (Batch Mode) nutzen, um Abfragen auf Columnstores schneller zu verarbeiten. – Einsatzgebiet: Optimiert für OLAP-/Analytical-Queries, d.h. große Tabellen (Millionen von Zeilen), bei denen Abfragen oftmals Summen, Durchschnitte, etc. über viele Zeilen berechnen, aber typischerweise nicht einzelne Zeilen abfragen. Für OLTP-Workloads (häufig Einfügen/Ändern einzelner Zeilen und punktuelles Lesen) sind Columnstores weniger geeignet, da Einzelsuchen aufwändig sind. – Clustered vs. Nonclustered Columnstore: Man kann einen Columnstore-Index als clustered Index erstellen. In diesem Fall ersetzt er das rowstore-Format der Tabelle komplett – die Tabelle wird zum Columnstore (alle Spalten werden im Index gespeichert). Alternativ kann man einen Columnstore-Index zusätzlich zu einem bestehenden rowstore behalten (als nicht gruppierter Columnstore-Index). Letzteres wird manchmal in Hybridszenarien genutzt: z. B. eine große Fact-Tabelle in einer OLTP-Datenbank bekommt neben ihren normalen Indizes einen Nonclustered Columnstore-Index, um analytische Auswertungen zu beschleunigen, während weiterhin Transaktionen auf dem Rowstore möglich sind.
Beispiel: Angenommen, SalesData ist eine sehr große Tabelle (Faktentabelle) mit Verkaufsdatensätzen, und wir führen darauf vorwiegend Auswertungen (Summen, Durchschnitte über Monate etc.) durch. Hier könnte ein Columnstore-Index ideal sein:
— Erstellen eines gruppierten Columnstore-Index auf einer großen Tabelle
CREATE CLUSTERED COLUMNSTORE INDEX CCI_SalesData
ON dbo.SalesData;
Durch dieses Statement wird die Tabelle SalesData in einen Columnstore umgewandelt (bestehende rowstore-Indizes werden entfernt, da nun der Columnstore die Hauptspeicherform ist). Abfragen, die z. B. SUM(Revenue) über viele Zeilen bilden, laufen damit um ein Vielfaches schneller.
Wollen wir hingegen in einer OLTP-Tabelle ergänzend einen Columnstore-Index für Berichte anlegen, ohne die Primärstruktur zu ändern, geht das so:
— Nonclustered Columnstore-Index auf einer OLTP-Tabelle ergänzen
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_Orders_Analysis
ON dbo.Orders(CustomerID, OrderDate, TotalAmount);
Hier bleibt Orders eine normale (rowstore) Tabelle, und wir fügen einen Columnstore-Index hinzu, der drei Spalten umfasst. Dieser Index kann für analytische Abfragen (z. B. durchschnittliches Bestellvolumen je Kunde) genutzt werden. Hinweis: Nonclustered Columnstore-Indizes müssen nicht alle Spalten enthalten; man wählt typischerweise diejenigen, die für Analysen relevant sind. Die enthaltenen Spalten werden dann spaltenweise gespeichert und komprimiert.
In SQL Server 2019 wurden Columnstore-Indizes weiter verbessert (u.a. effizientere Wartung und schnellere Bulkloads). Sie sind heute ein zentrales Feature für Performance-Tuning im Data Warehouse-Bereich. Wichtig ist zu verstehen, dass Columnstore-Indizes vor allem für Massenoperationen extrem schnell sind, aber Zugriffe auf einzelne Zeilen oder sehr selektive Abfragen weniger profitieren, da dabei oft trotzdem viele Zeilen gescannt werden müssen. Man kann Columnstore und klassische Indizes auch kombinieren – es kommt ganz auf das Abfrageprofil an.
Gefilterter Index (Filtered Index)
Ein gefilterter Index ist ein spezieller nicht gruppierter Index, der nur einen Teil der Daten einer Tabelle abbildet. Man definiert beim Erstellen des Index ein Filter-Prädikat (WHERE-Bedingung), sodass nur Zeilen, welche die Bedingung erfüllen, im Index aufgenommen werden. Gefilterte Indizes ermöglichen es, sehr spezifische Indizes für einen Teilbereich der Daten zu erstellen – dies spart Speicher und Wartungsaufwand und kann die Leistung verbessern, wenn Abfragen genau auf diesen Teilbereich abzielen.
Wann sind gefilterte Indizes nützlich? Zum Beispiel, wenn eine Spalte viele gleichartige Werte hat, aber nur ein kleiner Anteil der Werte häufig abgefragt wird. Oder bei Status-Feldern (z. B. “IsActive”), wo oft nur aktive Datensätze relevant sind. Ein gefilterter Index kann dann genau die relevanten Einträge indexieren und ignoriert den Rest. Auch für regelmäßig nur auf aktuelle Daten zugreifende Abfragen (z. B. letzte 3 Monate) könnte man einen Filter setzen, um nur die aktuellen Zeiträume zu indizieren.
Beispiel: Betrachten wir eine Customers-Tabelle mit einer Spalte IsActive (Boolean bzw. BIT), die angibt, ob ein Kunde aktiv ist. Nehmen wir an, in der Tabelle sind 90% der Kunden aktiv (IsActive = 1) und 10% inaktiv. Wenn wir häufig nur aktive Kunden abfragen, lohnt es sich kaum, einen Index über alle Kunden zu legen, da die Selektion sehr unselektiv ist. Aber wir könnten einen gefilterten Index nur für die inaktiven Kunden bauen, falls wir beispielsweise gelegentlich nur nach inaktiven Kunden suchen:
— Gefilterter Index: indiziert nur inaktive Kunden (IsActive = 0)
CREATE NONCLUSTERED INDEX IX_Customers_Email_Inactive
ON dbo.Customers(Email)
WHERE IsActive = 0;
Dieser Index IX_Customers_Email_Inactive erfasst nur Zeilen, wo IsActive = 0. Er könnte etwa eine Abfrage SELECT * FROM Customers WHERE IsActive = 0 AND Email LIKE ‚%example.com‘ beschleunigen, da er alle inaktiven Kunden schon vorsortiert nach Email enthält. Für aktive Kunden bräuchte man diesen Index nicht, weil vielleicht entsprechende Abfragen selten sind oder ohnehin viele Treffer liefern würden.
Ein anderes Beispiel: Man hat eine Orders-Tabelle mit einer Spalte OrderStatus und möchte Indexe nur für offene Bestellungen pflegen, da erledigte Bestellungen selten gesucht werden. Dann könnte man einen Filter WHERE OrderStatus = ‚OPEN‘ im Index definieren.
Gefilterte Indizes können wesentlich kleiner sein als ein voller Index und damit schneller zu durchlaufen. Zudem müssen sie nur bei Änderungen der gefilterten Zeilen aktualisiert werden, was die Update-Last reduziert. Wichtig ist allerdings: Der Query Optimizer nutzt den gefilterten Index nur, wenn die Abfragebedingung mit dem Index-Filter auswertbar ist. Das heißt, in der Abfrage muss im WHERE typischerweise eine entsprechende Klausel enthalten sein (z. B. IsActive = 0 im obigen Beispiel), sonst erkennt der Optimizer nicht, dass er den Index verwenden kann. Gefilterte Indizes sind seit SQL Server 2008 verfügbar und ein mächtiges Werkzeug, um Indizes gezielt auf relevante Teilmengen einzusetzen.
XML-Index
SQL Server bietet die Möglichkeit, auf XML-Spalten spezielle Indizes anzulegen, um Abfragen auf XML-Daten (XQuery, Pfadabfragen, Wertsuche in XML) deutlich zu beschleunigen. Ein XML-Index zerlegt („shredded“) das XML-Dokument intern in eine tabellarische Struktur von Pfaden, Tags und Werten, sodass Suchen innerhalb des XMLs schneller durchgeführt werden können.
Es gibt zwei Arten von XML-Indizes: – Primärer XML-Index: Dieser indiziert alle Elemente, Attribute, Werte und Pfade innerhalb der XML-Dokumente einer Spalte. Es ist die Grundlage für alle weiteren XML-Indizes. Pro XML-Spalte kann es maximal einen Primary XML Index geben. – Sekundäre XML-Indizes: Auf Basis des primären Index kann man optionale sekundäre Indizes für spezielle Zugriffsmuster erstellen: – Pfad-Index (PATH): optimiert Abfragen, die nach bestimmten XML-Pfaden suchen (z. B. Existenz eines bestimmten Tags). – Wert-Index (VALUE): optimiert Abfragen, die nach bestimmten Werten innerhalb des XML suchen. – Eigenschaften-Index (PROPERTY): optimiert Abfragen, die via .value() Methoden auf einzelne Werte zugreifen.
Die sekundären Indizes nutzen den primären als Grundlage und beschleunigen gezielt die entsprechenden Operationen.
Beispiel: Nehmen wir eine Tabelle Products, die ein XML-Feld AttributesXml enthält, in dem strukturierte Produkteigenschaften gespeichert sind. Wir führen häufig Abfragen durch, die bestimmte Attribute im XML filtern. Ein XML-Index kann hier helfen:
— Primärer XML-Index auf der XML-Spalte
CREATE PRIMARY XML INDEX PXML_Products_Attributes
ON dbo.Products(AttributesXml);
— Sekundärer XML-Index (Wert-Index), um Suchen nach bestimmten Werten zu beschleunigen
CREATE XML INDEX XML_Products_Attributes_Value
ON dbo.Products(AttributesXml)
USING XML INDEX PXML_Products_Attributes
FOR VALUE;
Zunächst wird ein Primary XML Index PXML_Products_Attributes erstellt. Dieser speichert alle Pfade und Werte aus AttributesXml. Darauf aufbauend definieren wir einen sekundären Index vom Typ VALUE. Dieser würde beispielsweise Abfragen wie „Finde alle Produkte, bei denen im XML das <Color>-Element den Wert ‚Rot‘ hat“ erheblich beschleunigen.
Hinweis: XML-Indizes können sehr speicherintensiv sein – der primäre XML-Index kann bis zur Größe der Originaldaten (oder größer) anwachsen, da sämtliche Tags und Werte gespeichert werden. Man sollte XML-Indizes daher nur anlegen, wenn man sie wirklich benötigt, und eventuell gezielt nur sekundäre Indizes wählen, die einen Flaschenhals beheben. Zudem sind XML-Indizes nur auf Spalten des Typs xml erlaubt, nicht auf textuelle JSON- oder XML-Darstellungen in VARCHAR (für JSON siehe unten Neuerungen).
Räumlicher Index (Spatial Index)
Für Datentypen geometry und geography (für zweidimensionale bzw. geographische räumliche Daten) stellt SQL Server räumliche Indizes bereit. Ein räumlicher Index beschleunigt Abfragen auf geometrischen Objekten, etwa „Finde alle Punkte innerhalb eines bestimmten Polygons“ oder „Liefere das nächstgelegene Objekt zu einem gegebenen Punkt“. Ohne Index müsste die Datenbank jedes Objekt geometrisch prüfen, was bei vielen Objekten langsam ist.
Wie funktioniert ein Spatial Index? Intern verwendet SQL Server eine mehrstufige Gitteraufteilung des Raumes (grids). Der Raum wird in Zellen eingeteilt (auf bis zu 4 Ebenen, je nach Konfiguration). Jedes geometrische Objekt (Punkt, Fläche, Linie etc.) wird anhand dieser Grid-Zellen abstrahiert (es bekommt einen sogenannten „Geohash“ bzw. Zellencode). Der räumliche Index speichert diese Codes, sodass SQL Server bei einer Abfrage zunächst grob überlappend nach passenden Zellen suchen kann, anstatt jedes Objekt vollständig zu überprüfen. Dieser zweistufige Ansatz (erst grob via Index, dann genaue Prüfung der Kandidaten) verringert die Menge an teuren geometrischen Berechnungen drastisch.
Beispiel: Angenommen wir haben eine Tabelle Locations mit einer Spalte Coordinates vom Typ GEOGRAPHY, in der z. B. Längen-/Breitengrade von Filialen gespeichert sind. Wir möchten schnell die Filialen in einem bestimmten Umkreis finden können. Wir legen einen spatial index an:
— Räumlicher Index auf Geokoordinaten, mit automatischer Gittereinteilung
CREATE SPATIAL INDEX SX_Locations_Coords
ON dbo.Locations(Coordinates)
USING GEOGRAPHY_AUTO_GRID
WITH (CELLS_PER_OBJECT = 16);
Hier erstellen wir SX_Locations_Coords auf der Coordinates-Spalte. USING GEOGRAPHY_AUTO_GRID lässt SQL Server automatisch eine Gittereinteilung wählen (seit SQL 2012 verfügbar; vorher musste man die Feinheitsstufen manuell angeben). CELLS_PER_OBJECT = 16 ist eine Option, die beeinflusst, wie detailliert große Objekte erfasst werden (hier z. B. bis zu 16 Gitterzellen pro Objekt maximal).
Mit diesem Index können Abfragen wie SELECT * FROM Locations WHERE Coordinates.STDistance(@somePoint) < 5000 (alle Punkte im 5km-Radius) erheblich beschleunigt ausgeführt werden. Der Index hilft dabei, schnell eine Untermenge möglicher Kandidaten zu finden, auf die dann die exakte Distanzberechnung angewendet wird.
Räumliche Indizes sind ein Muss, wenn man komplexere Geo-Datenabfragen performant durchführen will. Beachten sollte man: Das Einrichten kann etwas Tuning erfordern (z. B. die Wahl der richtigen Grid-Einstellungen bei besonderen Datenverteilungen), aber die AUTO_GRID-Option übernimmt viel. Räumliche Indizes gibt es seit SQL Server 2008, mit Verbesserungen in 2012, und sie unterstützen sowohl 2D-Koordinaten (geometry) als auch Geographie mit Erdkrümmung (geography).
Volltextindex (Full-Text Index)
Neben den oben genannten strukturierten Indexen gibt es noch den Volltextindex – dieser ist speziell für die Suche in unstrukturierten Texten (z. B. langen Beschreibungen, Dokumenten) konzipiert. Ein Volltextindex wird nicht mit CREATE INDEX erstellt, sondern über die Full-Text Engine von SQL Server verwaltet. Er indexiert Wörter und Phrasen aus Textfeldern, um schnelle Suchabfragen à la „Finde alle Datensätze, die das Wort ‚Datenbank‘ enthalten“ zu ermöglichen.
Eigenschaften eines Volltextindex: – Er ist tokenbasiert: Der Text wird in Wörter zerlegt (Tokenizing, sprachabhängig – es können auch Stoppwörter ignoriert werden, Wortstämme berücksichtigt etc.). Diese Wörter werden in einer speziellen Index-Struktur gespeichert, die im Hintergrund von SQL Server verwaltet wird. – Man kann dann mit T-SQL-Funktionen wie CONTAINS oder FREETEXT Abfragen stellen, die im Volltextindex nach Vorkommen von Wörtern, Wortkombinationen oder linguistisch ähnlichen Formen suchen. Das ist viel leistungsfähiger als ein einfacher LIKE-Filter auf einen Text (vor allem für Substring-Suchen oder suchmaschinenartige Abfragen). – Volltextindizes eignen sich, um z. B. Produktsbeschreibungen, Artikeltexte, Kommentare etc. durchsuchbar zu machen.
Beispiel: Angenommen, wir haben eine Tabelle Articles mit einem Textfeld Content für Artikelinhalte. Um diese auf Schlagwörter zu durchsuchen, legen wir zunächst einen Fulltext-Katalog (falls noch nicht vorhanden) und dann einen Volltextindex an:
— Volltext-Katalog erstellen (einmalig, falls nicht vorhanden)
CREATE FULLTEXT CATALOG FTCatalog AS DEFAULT;
— Volltextindex auf der Articles-Tabelle erstellen
CREATE FULLTEXT INDEX ON dbo.Articles(Content LANGUAGE ‚German‘)
KEY INDEX PK_Articles — eindeutiger Index, meist der PK, als Referenz
ON FTCatalog
WITH (STOPLIST = SYSTEM);
Wir geben hier an, dass die Sprache des Inhalts Deutsch ist (damit die Engine korrekte Wortzerlegung und Stopplisten verwendet). Der Volltextindex wird auf der Spalte Content erstellt; KEY INDEX PK_Articles gibt an, welcher eindeutige Index genutzt wird, um die Beziehung zwischen Volltextindex und Tabelle herzustellen (in der Regel der Primärschlüssel). Mit STOPLIST = SYSTEM nutzen wir die standardmäßig hinterlegte Stoppliste (häufige Wörter wie „der“, „die“, „und“ werden ignoriert).
Nach Erstellung kann man Abfragen wie SELECT * FROM Articles WHERE CONTAINS(Content, ‚Datenbank‘) verwenden, und der Volltextindex liefert sehr schnell alle Artikel, in denen das Wort „Datenbank“ vorkommt. Ohne Volltextindex müsste man mit LIKE ‚%Datenbank%‘ arbeiten, was bei großen Texten extrem langsam ist.
Hinweis: Volltextindizes werden asynchron aktualisiert (Indexierung der Texte erfolgt im Hintergrund). Sie eignen sich nicht für millisekundengenaue Echtzeit-Suchen nach gerade eingestellten Texten, aber für die meisten Anwendungen ist die geringe Verzögerung unproblematisch. Die Volltextsuche ist ein großes Thema für sich; an dieser Stelle sei nur festgehalten, dass sie eine eigenständige Indexart darstellt, um unstrukturierte Textdaten effizient durchsuchbar zu machen.
Weitere Indexarten und Besonderheiten
Die obigen Index-Typen decken die gängigsten Anwendungsfälle ab. Ergänzend sei erwähnt: – Eindeutige Indizes (Unique Index): Jede Indexart (gruppiert oder nicht gruppiert) kann als UNIQUE deklariert werden. Ein eindeutiger Index stellt sicher, dass keine zwei Zeilen denselben Indexschlüsselwert haben (Dublettenfreiheit). Primärschlüssel legen automatisch einen eindeutigen Index an. Man kann aber auch auf anderen Spalten eindeutige Indizes erzwingen, um z. B. die E-Mail-Adresse in einer Benutzer-Tabelle eindeutig zu machen. Unique Indexe dienen sowohl der Datenintegrität als auch der Performance (der Optimizer weiß, dass max. eine Zeile zu einem Wert existieren kann). – Indizierung berechneter Spalten: Ist eine berechnete Spalte (Computed Column) deterministisch und ggf. persistent, kann man auf ihr ebenfalls einen Index anlegen. So kann man z. B. auf einer berechneten Jahr = YEAR(OrderDate)-Spalte einen Index schaffen, um Abfragen pro Jahr zu beschleunigen, ohne in jeder Query YEAR() berechnen zu müssen. Vor Indexerstellung muss die berechnete Spalte entweder in der Definition als PERSISTED markiert sein oder die Funktion deterministisch und ohne Kontextabhängigkeit sein. – Partitionierte Indizes: In Enterprise Editionen (und seit neueren Versionen auch Standard für gewisse Fälle) können große Tabellen partitioniert werden. Indizes auf partitionierten Tabellen sind entsprechend ebenfalls partitioniert. Dadurch lässt sich die Daten- und Indexverwaltung für sehr große Datenmengen verbessern (z. B. Partition-Weise Wartung). Partitionierung beeinflusst den logischen Indexaufbau nicht fundamental, er verteilt aber physisch die Daten auf Segmente. – Memory-Optimized Indexes (In-Memory OLTP): Für mit In-Memory optimierte Tabellen (Hekaton) gelten eigene Indexstrukturen (Hash-Indizes und speicheroptimierte Nicht-Clustered Indizes). Diese unterscheiden sich architektonisch, da sie komplett im RAM gehalten und latch-frei implementiert sind. Sie wurden hier nicht im Detail behandelt, da sie nur für Memory-Optimized Tables relevant sind – für Entwickler mit Grundkenntnissen in klassischen SQL-Server-Tabellen spielen diese seltener eine Rolle.
Mit diesem Überblick über Index-Typen sollten Sie nun einordnen können, welcher Index für welchen Zweck geeignet ist. Als nächstes schauen wir uns an, wie man Indizes mittels T-SQL-Syntax konkret erstellt und verwaltet.
3. Syntax und Erstellung von Indizes mit T-SQL
Nachdem wir wissen, welche Indexarten es gibt, geht es nun um die praktische Anwendung: Wie erstellt man Indizes per Transact-SQL? In diesem Abschnitt betrachten wir die Syntax von CREATE INDEX und weiteren relevanten Befehlen sowie typische Optionen. Zudem zeigen wir Beispiele für das Anlegen verschiedener Index-Typen mit T-SQL.
CREATE INDEX – Grundlegende Syntax
Der Befehl zum Anlegen eines Indexes lautet allgemein:
CREATE [ UNIQUE ] [ CLUSTERED | NONCLUSTERED ] INDEX IndexName
ON Schema.Tabelle (Spalte1 [ASC|DESC] [,…] )
[ INCLUDE (Nichtschlüssel-Spalte1 [,…]) ]
[ WHERE <Filterbedingung> ]
[ WITH (Option1 = Wert1 [,…]) ]
[ ON <Dateigruppe/Partition> ];
- Mit UNIQUE wird (falls gewünscht) die Eindeutigkeit erzwungen.
- CLUSTERED oder NONCLUSTERED legt fest, ob es ein gruppierter oder nicht gruppierter Index wird. (Für Columnstore-Indizes wird ähnlich CLUSTERED COLUMNSTORE INDEX bzw. NONCLUSTERED COLUMNSTORE INDEX verwendet.)
- In Klammern folgen die Spalten, die im Indexschlüssel enthalten sein sollen. Man kann eine oder mehrere Spalten angeben, durch Komma getrennt. Die Reihenfolge der Spalten ist hier die Sortierreihenfolge im Index (was für die Nutzbarkeit bei Abfragen entscheidend ist). Optional kann man für jede Spalte die Sortierrichtung angeben (ASC aufsteigend, DESC absteigend); standardmäßig ist ASC.
- Die INCLUDE-Klausel erlaubt das Angeben von zusätzlichen Spalten, die nicht Teil des Suchschlüssels sind, aber im Index mit gespeichert werden (siehe vorheriger Abschnitt zum Covering Index). Diese Spalten werden nicht sortiert und nicht für Suchen benutzt, aber sie stehen im Index zur Verfügung, um Abfragen zu bedienen.
- Die WHERE-Klausel definiert einen Filter für gefilterte Indizes. Lässt man sie weg, umfasst der Index alle Zeilen.
- Unter WITH (…) können verschiedene Indexoptionen gesetzt werden (dazu gleich mehr).
- ON <Dateigruppe> erlaubt die Platzierung des Index auf einer bestimmten Filegroup oder Partition Scheme. Standardmäßig, wenn nicht angegeben, wird der Index auf derselben Filegroup wie die Tabelle erstellt oder dem angegebenen Partitionierungs-Schema folgend partitioniert.
Wichtige Indexoptionen (WITH-Klausel)
Bei WITH (…) kann man u.a. folgende Optionen angeben: – FILLFACTOR = x: Gibt an, wie voll die Index-Seiten bei Erstellung (oder Rebuild) befüllt werden sollen, in Prozent. Standard 0 (bzw. 100) bedeutet „voll packen“. Ein niedrigerer Fillfactor (z. B. 80) lässt ca. 20% Luft auf jeder Seite, um nachträgliche Einfügungen in der Sortier-Reihenfolge ohne sofortige Seitenaufteilung zu ermöglichen. Das mindert zwar die initiale Dichte und braucht etwas mehr Platz, kann aber bei erwarteten vielen Zwischen-Inserts Performance verbessern. – PAD_INDEX = ON/OFF: Steuert, ob der Fillfactor auch für Intermediate Pages gelten soll (meist lässt man OFF, sodass nur Leaf-Level betroffen ist). – SORT_IN_TEMPDB = ON/OFF: Falls ON, nutzt SQL Server bei der Indexerstellung die TempDB als Auslagerungsort für Sortieroperationen statt die Benutzerdatenbank. Das kann während des Indexaufbaus zu schnellerem Aufbau und weniger Logging in der Haupt-DB führen, aber belastet TempDB. – ONLINE = ON/OFF: Besonders in Enterprise Edition (ab SQL 2019 teils auch Standard für Nicht-Clustered Indizes) kann man Indizes online erstellen oder neuaufbauen. ON bedeutet, dass während der Indexerstellung Schreib- und Leseoperationen auf die Tabelle weiterhin möglich sind (nur kurze Abschlüsse am Anfang/Ende). OFF (Standard in älteren Versionen oder niedrigeren Editionen) bedeutet, die Tabelle wird exklusiv gesperrt, bis der Index fertig ist. Online-Indexoperationen minimieren Downtime in produktiven Umgebungen. – MAXDOP = n: Begrenzt die Parallelität der Indexerstellung (Standard 0 heißt, der Server entscheidet gemäß max. Parallelitäts-Setting). Man kann hier z. B. MAXDOP = 1 setzen, um die Indexerstellung nur single-threaded laufen zu lassen (hilfreich, wenn man die CPU nicht zu stark belasten will), oder einen Wert, um explizit zu steuern. – DATA_COMPRESSION = ROW/PAGE: Man kann angeben, ob der Index mit Zeilen- oder Seitendatenkompression gespeichert werden soll (spart Platz, auf Kosten etwas höherer CPU bei Zugriffen). Diese Option ist in neueren SQL Server-Versionen oft frei verfügbar, früher Enterprise-Feature.
Neue Optionen ab SQL Server 2019+: – OPTIMIZE_FOR_SEQUENTIAL_KEY = ON/OFF: (Ab SQL 2019) Hilft bei Indexen mit einem ständig anwachsenden Schlüssel (z. B. Identity) Last-Page-Insert-Contention zu verringern. Wenn viele gleichzeitige Inserts immer auf die letzte Seite gehen, kann das Page-Latch-Contention erzeugen. Diese Option aktiviert interne Optimierungen, um diesen Engpass zu entschärfen (z. B. indem die Einfügungen kurz minimal verzögert verteilt werden). Standard ist OFF; man kann es auf ON setzen, wenn man hohe Insert-Concurrency auf einem monoton steigenden Index beobachtet. – RESUMABLE = ON: (SQL 2017 für Rebuild, ab 2019 auch für Create) Erlaubt, dass die Indexoperation pausier- und fortsetzbar ist. Das ist sehr nützlich für sehr große Indizes: Man kann den Build z. B. nach X Sekunden unterbrechen und später weitermachen, oder auch abbrechen und bereits getane Arbeit bleibt als Snapshot stehen. In der Praxis kombiniert man es mit der Angabe einer MAX_DURATION, aber dazu später in Neuerungen mehr.
Beispiel Syntax mit Optionen: Wir erstellen erneut einen Index, diesmal mit einigen Optionen:
— Beispiel: Index mit mehreren Spalten, Include und Optionen
CREATE NONCLUSTERED INDEX IX_Sales_CustDate
ON dbo.Sales(CustomerID ASC, OrderDate DESC)
INCLUDE (TotalAmount)
WITH (FILLFACTOR = 80, STATISTICS_NORECOMPUTE = OFF, ONLINE = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF);
Erläuterung:
Dieser Index IX_Sales_CustDate auf der Tabelle Sales hat einen zusammengesetzten Schlüssel aus CustomerID (aufsteigend sortiert) und OrderDate (absteigend sortiert – vorteilhaft, wenn man oft die neuesten Bestellungen zuerst sehen will). Als Include-Spalte haben wir TotalAmount hinzugefügt, um entsprechende Abfragen zu überdecken. In den Optionen setzen wir einen Fillfactor von 80 (d.h. der Index lässt 20% Platz pro Seite frei, was hier hypothetisch sinnvoll sein könnte, falls zwischen vorhandenen Bestelldatumswerten neue eingefügt werden). STATISTICS_NORECOMPUTE = OFF bedeutet, die Indexstatistiken werden nach Bedarf automatisch aktualisiert (das ist Standard). ONLINE = ON würde die Erstellung online durchführen (erfordert entsprechende Edition/Version). OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF belässt die Standardeinstellung; in diesem Fall, da unser Index nicht rein sequential (Kunde+Datum) ist oder wir hier wenig Insert-Contention erwarten, belassen wir es aus – es dient hier nur der Vollständigkeit.
Indexverwaltung: Ändern und Löschen
- Ändern (Rebuild/Reorganize): Indizes können mit dem Befehl ALTER INDEX verwaltet werden. Typische Aktionen sind ALTER INDEX … REBUILD (komplett neu aufbauen, defragmentiert vollständig, wendet Fillfactor neu an) oder ALTER INDEX … REORGANIZE (Seiten defragmentieren durch Neuordnen, ohne kompletten Neubau, online). Diese Befehle werden in Abschnitt 5 (Index-Wartung) noch ausführlicher behandelt.
- Löschen: Ein Index wird mit DROP INDEX IndexName ON Tabelle gelöscht. Vorsicht: Beim Löschen eines gruppierten Indexes (falls es einen gibt) gehen die Daten der Tabelle nicht verloren – die Tabelle bleibt als Heap bestehen, aber die physische Ordnung ist danach unsortiert. Nicht gruppierte Indizes, die vorher auf den Clustered Key verwiesen, referenzieren danach die physikalischen Zeilen (RID) oder werden ungültig (SQL Server dropt sie beim Löschen des Clustered Index in der Regel mit, sofern sie nicht mittels DROP INDEX … WITH (MOVE TO) auf den Heap migriert werden). Das Löschen eines nicht gruppierten Indexes entfernt einfach die entsprechende Indexstruktur; die zugrundeliegenden Daten bleiben unverändert.
Beispiel – Index löschen:
— Löschen des zuvor erstellten Indexes IX_Sales_CustDate
DROP INDEX IX_Sales_CustDate ON dbo.Sales;
Damit wäre der Index entfernt. Man würde so etwas tun, wenn man feststellt, dass ein Index kaum genutzt wird, aber Ressourcen verbraucht (siehe Abschnitt 6).
Zusammengefasst stellt T-SQL mit CREATE/ALTER/DROP INDEX alle nötigen Mittel bereit, um Indizes zu erstellen, zu modifizieren (z. B. neu aufzubauen, Option zu ändern) oder zu löschen. Viele dieser Operationen können auch komfortabel über das SSMS-GUI gemacht werden (Rechtsklick auf Index etc.), aber für Skripting und Automatisierung ist T-SQL unerlässlich. In der nächsten Sektion wenden wir uns der Ausführungsplan-Analyse zu: Wie erkennt man, ob und wie ein Index von einer Abfrage genutzt wird?
4. Analyse von Abfrageplänen zur Bewertung der Indexnutzung
Ein Index nützt nur etwas, wenn der Abfrageoptimierer ihn auch verwendet. Daher ist es für Entwickler wichtig zu verstehen, wie man überprüft, ob eine bestimmte Abfrage von vorhandenen Indizes Gebrauch macht, und ob die Indizes effizient eingesetzt werden. Die zentrale Informationsquelle hierfür ist der Ausführungsplan (Execution Plan) einer Abfrage.
Ausführungsplan einsehen
In SQL Server Management Studio (SSMS) kann man sich den Abfrageplan anzeigen lassen. Es gibt zwei Varianten: – Geschätzter Ausführungsplan: SSMS zeigt nach Drücken von Strg+L oder Klick auf das entsprechende Symbol einen Plan, ohne die Abfrage auszuführen. Das ist nützlich, um schnell zu sehen, wie der Optimierer eine Abfrage planen würde. – Tatsächlicher Ausführungsplan: Wenn man eine Abfrage ausführt und dabei die Option „Actual Execution Plan“ aktiviert (Strg+M in SSMS vor Ausführung, oder im Menü), bekommt man nach Ausführung den realen Plan inklusive tatsächlicher Metriken (Anzahl gelesener Zeilen etc.).
Der Ausführungsplan wird grafisch dargestellt – jeder Knoten repräsentiert einen Schritt (Operator) der Ausführung, z. B. einen Index Seek, Index Scan, Table Scan, Nested Loop Join etc. Durch Hover oder Rechtsklick -> Eigenschaften erhält man Details wie verwendeter Indexname, geschätzte/aktuelle Zeilen, Kostenanteile usw.
Index Seek vs. Index Scan vs. Table Scan
Im Kontext der Indexnutzung sind besonders folgende Plan-Operatoren interessant: – Index Seek: Der Optimierer konnte gezielt auf einem Index nach dem gewünschten Wert/Range suchen. Das ist in der Regel das, was wir sehen möchten – es bedeutet eine selektive, effiziente Nutzung eines Index. Im Plan steht dann z. B. Index Seek (NonClustered) on IX_Orders_Customer mit einer Seek-Prädikatsbedingung. – Index Scan: Hier wird zwar ein Index genutzt, aber nicht selektiv gesucht, sondern das Index wird mehr oder minder komplett durchlaufen. Das passiert, wenn die Abfrage zwar einen Index abdecken könnte, aber z. B. sehr viele Zeilen sowieso lesen muss (niedrige Selektivität), oder die Bedingung nicht zum sortierten Schlüssel passt (dann muss ggf. der ganze Index gescannt werden). Ein Index Scan ist in etwa vergleichbar mit einem Table Scan, nur dass er die Daten in Index-Reihenfolge liest (z. B. ein Scan auf einem nicht gruppierten Index kann auftreten, wenn die Abfrage alle Daten aus einer kleinen Tabelle will – dann nimmt der Optimizer eventuell einen vorhandenen Index und scannt ihn vollständig). – Table Scan: Das tritt auf, wenn die Tabelle keinerlei passenden Index hat (bei Heaps heißt es im Plan auch Table Scan, bei einer Clustered-Index-Tabelle wird ein Clustered Index Scan angezeigt). Hier werden wirklich alle Seiten der Tabelle gelesen. Ein Table/Clustered Index Scan kann bei kleinen Tabellen (< einige 100 Zeilen) okay sein, da die Kosten gering sind – aber bei großen Tabellen will man Table Scans in der Regel vermeiden, wenn Selektivität vorhanden wäre.
Plan-Beispiel interpretieren:
Stellen wir uns eine Abfrage vor:
SELECT OrderDate, TotalAmount
FROM dbo.Orders
WHERE CustomerID = 12345;
Angenommen, wir haben den Index IX_Orders_Customer_Include(CustomerID INCLUDE OrderDate, TotalAmount) aus dem vorherigen Abschnitt angelegt. Der Optimizer sollte erkennen, dass dieser Index die Abfrage perfekt abdeckt. Im Ausführungsplan würden wir erwarten: – Einen Index Seek auf IX_Orders_Customer_Include mit dem Seek-Prädikat CustomerID = 12345. – Da alle benötigten Spalten (OrderDate, TotalAmount) im Index vorhanden sind (entweder als Schlüssel oder Include), muss kein weiterer Lookup stattfinden.
Der Plan zeigt möglicherweise noch einen SELECT-Operator (Root) als Ergebnis und darunter den Index Seek als einzigen echten Datenzugriff. Das wäre optimal.
Wenn wir denselben Query ohne einen solchen Index ausführen (z. B. wir hätten nur einen clustered Index auf OrderID), sähe der Plan vermutlich so aus: – Clustered Index Scan oder Index Scan auf dem Clustered Index (da keine passende Suchstruktur auf CustomerID existiert, muss die DB alle Orders durchsuchen). – Der geschätzte Kostenanteil wäre viel höher. Eventuell weist SQL Server im Plantext sogar auf einen fehlenden Index hin (manchmal erscheint in grüner Schrift ein Vorschlag “Missing Index (Impact 90%): CREATE INDEX… ON Orders(CustomerID)…”).
Ein anderes Beispiel: Ein Plan mit Index Seek + Key Lookup. Das würde passieren, wenn wir einen Index haben, der die WHERE-Bedingung abdeckt, aber nicht alle ausgewählten Spalten enthält. Etwa:
SELECT OrderDate, TotalAmount
FROM Orders
WHERE CustomerID = 12345;
und wir hätten nur IX_Orders_Customer (ohne Include). Dann könnte der Plan so aussehen: – Index Seek auf IX_Orders_Customer (um die passenden OrderIDs zu finden). – Für jede gefundene OrderID: Key Lookup in den Clustered Index (oder RID Lookup im Heap), um OrderDate und TotalAmount zu holen. – Diese Kombination ist meist mit einem Nested Loop umgesetzt (für jede Zeile aus Seek ein Lookup).
Key Lookups erkennt man im Plan und kann daran sehen, welche Spalten nachgefordert werden (Properties zeigen die Output List etc.). Wenn solche Lookups sehr häufig vorkommen (viele Loop-Iterationen), kann das ein Hinweis sein, dass ein Include im Index hilfreich wäre, um den Lookup zu vermeiden.
Statistiken und weitere Hilfsmittel
Neben dem grafischen Plan kann man auch textuelle Hilfen nutzen: – SET STATISTICS IO ON; vor einer Abfrage zeigt nach Ausführung die Anzahl gelesener Seiten (logical reads) pro Objekt an. Ein Vergleich mit/ohne Index offenbart die Differenz. Beispiel: Ohne Index vielleicht „Table ‚Orders‘. Scan count 1, logical reads 5000“, mit Index „Index ‚IX_Orders_Customer‘. Scan count 1, logical reads 50; Table ‚Orders‘. Logical reads 100 (for lookups)“ etc. Diese Zahlen helfen, die I/O-Ersparnis zu quantifizieren. – Der Missing Index Report: Im tatsächlichen Ausführungsplan (XML oder GUI) markiert SQL Server manchmal fehlende Indizes, die die Query beschleunigen könnten. Diese sind ein guter Anhaltspunkt, aber sie werden automatisch generiert und nicht immer perfekt (dazu in Best Practices mehr). – Live Query Statistics: In neueren SSMS-Versionen kann man während der Ausführung langer Queries den Fortschritt/Plan live beobachten – für Indexanalyse weniger relevant, aber interessant bei Tuning langer Vorgänge.
Zusammenfassend: Um zu bewerten, ob Indizes gut greifen, sollten Entwickler regelmäßig einen Blick auf die Ausführungspläne der wichtigen Abfragen werfen. Sie können dort sehen: – Wird ein Index verwendet? Welcher? (Operator: Seek/Scan + Indexname) – Wie hoch ist der geschätzte vs. tatsächliche Aufwand? – Gibt es Table Scans, die auf fehlende Indizes hinweisen? – Gibt es Key Lookups, die durch Include-Spalten vermieden werden könnten? – Wurden vielleicht falsche Indexes gewählt? (Manchmal nutzt der Optimizer einen Index, der nicht optimal ist, z. B. weil Statistiken veraltet sind – dann sieht man hohe Diskrepanz in geschätzten vs. tatsächlichen Zeilen.)
Anhand dieser Erkenntnisse kann man dann gezielt weitere Indizes entwerfen oder vorhandene anpassen.
Im nächsten Abschnitt fassen wir allgemeine Best Practices zusammen, die bei der Indexstrategie beachtet werden sollten, um typische Fehler zu vermeiden und eine optimale Performance zu gewährleisten.
5. Best Practices zur Indexstrategie
Die richtige Indexstrategie erfordert einen Ausgleich verschiedener Anforderungen. Nachfolgend einige bewährte Best Practices und Richtlinien, die Entwicklern helfen, effektive Indizes zu entwerfen und zu pflegen:
- So wenige Indizes wie nötig, so viele wie sinnvoll: Jeder Index verbessert bestimmte Leseabfragen, verursacht aber Kosten bei Schreiboperationen (INSERT/UPDATE/DELETE) und beim Speichern. Überlegen Sie daher für jede Tabelle, welche Abfragen wirklich häufig und performancekritisch sind. Übermäßige Indizierung („für jede denkbare Abfrage einen Index“) führt oft zu mehr Nachteilen als Vorteilen. Konzentrieren Sie sich auf die kritischen Zugriffsmuster. Als Faustregel: OLTP-Tabellen kommen oft mit einer Handvoll gut gewählter Indizes aus. Dutzende Indizes auf einer Tabelle sind meist ein Warnsignal, dass optimiert werden kann (bzw. konsolidiert – siehe nächster Punkt).
- Redundante oder überschneidende Indizes vermeiden: Prüfen Sie geplante Indizes auf Überschneidungen. Zwei Indizes, die sich stark ähneln (z. B. Index1 on (A, B) und Index2 on (A)), können oft konsolidiert werden. Im genannten Beispiel deckt Index1 Abfragen auf A ebenso ab wie Index2 (vorausgesetzt, B ist ein kleiner zusätzlicher Overhead). Man könnte Index2 also weglassen. Jeder unnötige Index frisst Ressourcen und verlängert Wartungszeiten. Nutzen Sie Deckung (Include-Spalten) oder zusammengesetzte Schlüssel, um mit einem Index mehrere Abfragen abzudecken, anstatt viele Ein-Spalten-Indizes anzulegen.
- Wahl des Clustered Index mit Bedacht: Der Clustered Index bestimmt die physische Ordnung. Wählen Sie eine Spalte (oder Spaltenkombination), die stabil ist (sich selten ändert), möglichst einzigartig und klein (schmaler Schlüssel) – denn dieser Schlüssel steckt auch in jeder nicht gruppierten Indexzeile als Zeiger. Gut eignen sich surrogate Keys wie Identitäten, oder natürlich-monotone Felder wie Zeitstempel für Zeitreihen. Vermeiden sollte man als Clustered Key Spalten mit häufigen Updates (führt zu Zeilenverschiebungen) oder große Datentypen (z. B. VARCHAR(500) als Clustered Key wäre schlecht, da das jeden Index aufbläht). Immer einen Clustered Index? In den meisten Fällen ja – Heaps haben einige Nachteile (z. B. Forwarded Records bei Update, ineffiziente Suchoperationen außer via RID-Lookup). Es gibt Spezialfälle, wo Heaps sinnvoll sind (etwa bei sehr breiten Staging-Tabellen, in die nur eingefügt und dann via Bulk gelesen wird), aber in OLTP-Systemen gilt meist: Jede wichtige Tabelle sollte einen sinnvollen Clustered Index haben.
- Index-Selektivität und Schlüsselreihenfolge beachten: Indizes bringen am meisten, wenn das über sie gesuchte Kriterium hoch selektiv ist, d. h. der Index reduziert die Zeilenmenge stark. Ein Index auf eine Spalte, die nur 2–3 unterschiedliche Werte hat (z. B. Geschlecht, ja/nein Flags), bringt meist wenig – hier wird oft trotzdem ein großer Teil der Tabelle gelesen, so ein Index lohnt selten (Ausnahme: wenn diese Low-Cardinality-Spalte gefiltert mit anderen kombiniert wird). Wenn Sie mehrspaltige Indizes anlegen, bedenken Sie die Spaltenreihenfolge: Der Query Optimizer kann einen Index nur nutzen, solange die führenden Spalten im WHERE oder als Joinbedingung eingeschränkt sind. Beispiel: Ein Index auf (A, B, C) kann eine Abfrage auf A, A+B oder A+B+C effizient bedienen, nicht aber eine Abfrage, die nur nach B = X filtert (ohne A) – in letzterem Fall greift er die Reihenfolge nicht passend und der Index wird ggf. nicht genutzt. Daher sollte die erste Spalte diejenige sein, die in möglichst vielen wichtigen Abfragen im Filter vorkommt, und idealerweise auch die höchste Selektivität aufweist.
- Composite vs. separate Indizes: Oft stellt sich die Frage, ob man zwei einzelne Indizes oder einen zusammengesetzten Index anlegt. Das hängt vom Abfrageverhalten ab. Haben Sie viele Abfragen, die beide Spalten zusammen filtern (z. B. WHERE A=? AND B=?), dann ist ein zusammengesetzter Index (A,B) ideal. Werden Spalten immer separat gesucht, helfen separate Indizes. Häufig sind Kombinationen aber sinnvoll: z. B. (Kunde, Datum) für Abfragen, die alle Bestellungen eines Kunden in einem Datumsbereich suchen. Hier wäre ein Index auf (Kunde, Datum) viel besser als separate Indizes, denn der separate Index auf Datum nützt nichts, wenn man nach Kunde filtert, und umgekehrt (der Optimizer kann pro Tabelle meist nur einen Index gezielt seeken pro Abfrage-Pfad, außer in seltenen Fällen mit Index Intersection, was aber kaum vorkommt). Also: Indizes möglichst nach dem tatsächlichen Mehrspalten-Zugriffsmuster designen.
- Covering Indexe gezielt einsetzen: Wie oben erläutert, können Include-Spalten in Indizes Abfragen vollständig abdecken und teure Key-Lookups vermeiden. Identifizieren Sie häufige Abfragen, bei denen ein Index zwar genutzt wird, aber dann noch ein Lookup erfolgt (das sieht man im Ausführungsplan: Index Seek + Key Lookup). Hier kann es sinnvoll sein, die abgefragten Spalten (die im SELECT oder JOIN gebraucht, aber nicht im Filter stehen) als Included Columns dem Index hinzuzufügen. Beispiel: Ein Index auf CustomerID und Abfragen selektieren oft CustomerName zusätzlich – dann könnte man CustomerName im Index mit aufnehmen, um den Lookup zu sparen. Achtung: Machen Sie das nur für wirklich oft gebrauchte Szenarien; jedes Include macht den Index breiter (mehr Speicher, I/O). Besonders große Spalten (z. B. VARCHAR(MAX) oder BLOBs) sollte man nie in Indizes aufnehmen.
- Indexwartung: Fragmentierung beobachten und beheben: Durch Einfügen, Aktualisieren und Löschen von Daten fragmentieren Indizes im Laufe der Zeit – d. h. die physische Reihenfolge passt nicht mehr zur logischen, Seiten sind nur teilweise gefüllt oder ausser Reihenfolge auf der Disk. Starke Fragmentierung kann zu Performanceverlust (mehr I/O) führen. Verwenden Sie Tools oder SQL-Befehle wie sys.dm_db_index_physical_stats, um Fragmentierungsgrade zu ermitteln. Allgemeine Empfehlung: Bei Fragmentierung > 30% den Index REBUILD (komplett neu aufbauen, defragmentiert völlig, wendet Fillfactor an), bei Fragmentierung ab ~5–30% ggf. REORGANIZE (Seiten defragmentieren, defragmentiert nur etwas, dafür online und inkrementell). Rebuild erneuert auch die Statistik, Reorganize nicht. Planen Sie Index-Wartung z. B. als regelmäßigen Wartungsjob außerhalb der Hauptbetriebszeiten, oder nutzen Sie automatisierte Lösungen, um Indizes fit zu halten. (Manche neuere Ansätze plädieren dafür, Indexwartung nicht blind nach Schwellenwerten zu machen, sondern nur bei spürbarem Bedarf – aber das sprengt hier den Rahmen.)
- Statistiken aktuell halten: Zu Indizes gehören automatische Statistiken über die Werteverteilung. Diese sind entscheidend für den Optimizer, um zu entscheiden, ob ein Index genutzt werden sollte. Standardmäßig aktualisiert SQL Server Statistiken bei genügend Änderungen automatisch, aber es kann Fälle geben, wo sie veraltet sind. In solchen Fällen (erkennbar z. B. an völlig falschen Schätzungen im Ausführungsplan) hilft es, die Statistiken oder den Index zu aktualisieren (UPDATE STATISTICS oder Rebuild Index). Insbesondere nach Massenimports oder -löschungen kann ein manuelles Aktualisieren ratsam sein, bevor kritische Abfragen laufen.
- Indexnutzung überwachen: Nutzen Sie DMVs (Dynamic Management Views) wie sys.dm_db_index_usage_stats, um zu prüfen, wie oft welcher Index tatsächlich verwendet wurde (Seeks, Scans) seit dem letzten Serverstart. Dadurch können Sie ungeeignete Indizes identifizieren, die selten oder nie gebraucht werden, aber Updates verursachen (Spalte user_updates viel höher als seeks/scans). Solche Indizes sind Kandidaten zum Entfernen, um Schreibperformance zu verbessern. (Achtung: Berücksichtigen Sie den Zeitraum und ob evtl. seltene Reporting-Queries sie nutzen, bevor Sie sie droppen.)
- Index auf Fremdschlüssel-Spalten: Ein oft empfohlener Best Practice ist, auf Fremdschlüssel-Feldern (also Spalten, die auf einen Primary Key in einer anderen Tabelle verweisen) einen Index zu haben. Grund: JOIN-Operationen zwischen der Parent- und Child-Tabelle profitieren stark davon – meist hat die Parent einen PK (geclustert, indiziert), und wenn die Child auf dem FK keinen Index hat, muss beim Join die Child-Tabelle komplett gescannt werden. Mit Index kann man die passenden Child-Rows schnell finden. Ebenso sind Lösch-/Update-Überprüfungen (ON DELETE CASCADE etc.) effizienter. Daher: wo sinnvoll, Fremdschlüssel indexieren (besonders bei 1:n Beziehungen mit vielen n).
- Nicht jeden möglichen Index sofort erstellen – messen und iterieren: Es ist verlockend, aufgrund theoretischer Überlegungen viele Indizes gleich anzulegen. Besser ist oft ein iterativer Ansatz: Zunächst grundlegende Indizes (PK, wichtige eindeutige Keys, offensichtlich benötigte Abfrageindizes), dann das System unter realer Last beobachten. Wo zeigen sich Bottlenecks? Welche Abfragen sind langsam? Dann gezielt optimieren: Fehlt ein Index? Könnte ein Index angepasst werden? So stellen Sie sicher, dass Indizes ein konkretes Problem lösen und nicht “auf Verdacht” erstellt wurden. Nutzen Sie dabei auch die vom SQL Server gelieferten Missing-Index-Vorschläge (siehe nächster Abschnitt) mit Augenmaß.
- Spezialfälle erkennen: Manche Abfragen profitieren nicht von Indizes, egal wie viele man hat (z. B. wenn sehr große Teile einer Tabelle gelesen werden müssen, ist ein Indexseek möglicherweise nicht schneller als ein Scan). Hier sollte man Indizes nicht erzwingen, sondern eventuell andere Optimierungen erwägen (Partitionierung, Query Rewrite etc.). Auch Funktionen im WHERE können Indizes entwerten – z. B. WHERE FUNCTION(Spalte) = Wert führt oft dazu, dass ein Index auf Spalte nicht genutzt wird, es sei denn, man indexiert einen berechneten Ausdruck oder umschreibt die Query.
Diese Best Practices sollen als Leitplanken dienen. Natürlich gibt es immer wieder Fälle, die eine abweichende Strategie erfordern, aber im Allgemeinen helfen obige Punkte, eine solide Indexlandschaft zu gestalten. Im nächsten Teil geht es darum, wie man dynamisch auf veränderte Bedingungen reagiert – sprich, wenn sich Datenvolumen oder Abfrageprofile ändern, wie passt man die Indizes entsprechend an.
6. Dynamisches Anpassen von Indizes bei sich ändernden Datenmustern
Datenbanken sind lebendig: Daten wachsen, ändern ihre Verteilung, neue Abfragen kommen hinzu, alte fallen weg. Eine gute Indexstrategie ist daher nicht statisch, sondern dynamisch und wird im Laufe der Zeit überprüft und angepasst. In diesem Abschnitt besprechen wir, wie man Veränderungen erkennt und welche Maßnahmen ergriffen werden können, um Indizes an neue Gegebenheiten anzupassen.
Monitoring und Identifikation von Änderungsbedarf
- Überwachung der Abfrageleistung: Verwenden Sie Monitoring-Tools oder die SQL Server-eigenen Funktionen (wie Query Store ab SQL 2016 oder Profiler/Extended Events), um langsamere Abfragen aufzuspüren. Wenn eine zuvor schnelle Abfrage plötzlich langsam wird, kann das auf geänderte Daten hindeuten (z. B. stark gewachsene Tabelle -> eventuell fehlt nun ein Index, der vorher nicht nötig war, oder ein vorhandener Index ist durch geänderte Datenverteilung weniger effektiv).
- Missing Indexes DMV: SQL Server hält in sys.dm_db_missing_index_* Views Informationen über empfohlene Indizes, die während Abfrageoptimierungen als nützlich erkannt wurden. sys.dm_db_missing_index_details & Co zeigen beispielsweise welche Spalten in Filtern/JOINs fehlten und wie hoch der (geschätzte) Nutzen eines solchen Index wäre. Diese sollten periodisch ausgewertet werden. Man kann z. B. die Top-10 fehlenden Indizes nach kumuliertem Impact herausfinden. Wichtig: Diese Empfehlungen sind Vorschläge, die nicht blind eins-zu-eins umgesetzt werden sollten – oft schlagen sie sehr breite Indizes vor (inkl. vieler Spalten), oder mehrere ähnliche Indizes. Hier ist menschliches Augenmaß gefragt, die richtigen Schlüsse zu ziehen (z. B. Vorschläge konsolidieren, prüfen ob die Workload wirklich davon profitiert, etc.).
- Index-Nutzungsstatistiken: Wie in den Best Practices erwähnt, liefert sys.dm_db_index_usage_stats wertvolle Infos, wie oft Indizes benutzt werden. Schauen Sie speziell auf Indizes mit vielen user_updates aber nahezu keinen user_seeks/scans/lookups. Das sind Indizes, die ständig gepflegt werden, aber kaum jemand fragt sie ab – also klare Kandidaten, um sie zu entfernen. Durch Drop solcher Indexe entlastet man das System bei Schreiboperationen. Natürlich sollte man sicher sein, dass nicht ausgerechnet ein seltener aber wichtiger Report diese Indizes benötigt – daher idealerweise mit dem Team/DBA rücksprechen oder testweise mal deaktivieren und schauen ob jemand “schreit”.
Anpassungsmaßnahmen
- Hinzufügen neuer Indizes: Entdeckt man neue Abfragepatterns (z. B. die Anwendung baut eine neue Funktion ein, die anders filtert), sollte man prüfen, ob ein zusätzlicher Index nötig ist. Gerade wenn das Datenvolumen mittlerweile groß ist, kann ein Index, der früher unnötig war, plötzlich Sinn ergeben. Beispiel: Anfangs war eine Tabelle klein, ein Table Scan dauerte 50 ms – ein Index war nicht nötig. Jetzt hat die Tabelle 1 Mio Zeilen, der Scan dauert 5 Sekunden – hier lohnt nun evtl. ein Index. Also: Workload-Änderungen oder Datenwachstum sollten getrackt werden.
- Entfernen oder Zusammenführen von Indizes: Wenn sich herausstellt, dass bestimmte Abfragen nicht mehr laufen oder anders formuliert werden, können Indizes obsolet werden. Ebenso kann man, basierend auf Missing-Index-Hinweisen, feststellen, dass ein existierender Index nicht optimal gestaltet ist – statt einen neuen parallel anzulegen, könnte man einen vorhandenen ändern (Recreate mit anderer Spaltenkombination). Das erfordert ein wenig Planung: Man kann einen Index via DROP INDEX entfernen und neu CREATE INDEX mit passender Definition (direkt ändern geht nicht, außer via CREATE INDEX … WITH (DROP_EXISTING = ON) – das ist eine Abkürzung, um einen Index in einem Schritt neu zu bauen mit geändertem Schema, sofern der Name gleich bleibt).
- Index-Reorganisation nach Datenänderungen: Falls Datenmuster sich ändern, kann auch der Fillfactor oder die Seitenverteilung suboptimal werden. Beispiel: Man hat einen Fillfactor 90 gesetzt, weil man dachte, Inserts sind überwiegend am Ende, aber nun werden doch oft Zwischenwerte eingefügt -> viele Page Splits -> Fragmentierung. Hier könnte man überlegen, den Fillfactor zu senken und per Rebuild anzuwenden, um zukünftige Einfügungen aufzufangen.
- Automatische Optimierungstools: SQL Server (insbesondere Azure SQL) bietet Features wie Automatic Tuning, das kann z. B. fehlende Indizes nicht nur vorschlagen, sondern in Azure auch automatisch erstellen oder wenig genutzte Indizes entfernen, sofern aktiviert. In On-Premises SQL Server gibt es ab 2017/2019 ebenfalls das Feature „AUTOMATIC_TUNING“ mit Optionen wie CREATE_INDEX und DROP_INDEX. Diese sind jedoch mit Vorsicht zu genießen und standardmäßig off (auf Azure werden sie gerne genutzt). Für Produktivsysteme ist es meist besser, als Entwickler/DBA die Kontrolle zu behalten, aber man kann sich von solchen Tools Vorschläge einholen.
- Query Store und Plan-Evolution: Ab SQL 2016 hilft der Query Store, historische Abfragepläne und Laufzeitstatistiken aufzubewahren. Darüber kann man sehen, ob z. B. nach einer bestimmten Datenzunahme der Plan umgesprungen ist (z. B. von Index Seek zu Scan) und kann gezielt dagegensteuern (vielleicht mit einem Index oder einem plan hint oder einer Query-Optimierung). Query Store kann auch Empfehlungen (Force Last Good Plan etc.) geben, was indirekt auch mit Indizes zu tun hat – etwa wenn ein neuer Plan schlechter ist, weil ein Index nicht genutzt wird, etc.
Beispiel – Unbenutzte Indizes finden: Das folgende T-SQL-Skript listet Indizes, die seit dem letzten Server-Start nie genutzt wurden, aber geschrieben wurden (also vermutlich unnötig):
SELECT
OBJECT_NAME(i.object_id) AS Tabelle,
i.name AS IndexName,
ISNULL(us.user_seeks, 0) AS Seeks,
ISNULL(us.user_scans, 0) AS Scans,
ISNULL(us.user_lookups, 0) AS Lookups,
ISNULL(us.user_updates, 0) AS Writes
FROM sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats us
ON i.object_id = us.object_id AND i.index_id = us.index_id
WHERE i.object_id > 100 AND i.index_id > 0 — systemobjekte und Heaps ausschließen
AND COALESCE(us.user_seeks, us.user_scans, us.user_lookups, 0) = 0
AND ISNULL(us.user_updates, 0) > 0
ORDER BY us.user_updates DESC;
Dieses Statement sucht alle Indizes, für die kein einziger Seek/Scan/Lookup registriert ist (COALESCE(…)=0 bedeutet alle Null oder Null, d.h. nichts genutzt), aber die user_updates (Anzahl Änderungen) > 0 sind. Sortiert nach den meisten Writes. Diese Indizes belasten also das System (viele Writes), bringen aber keinen Lesenvorteil (0 Seeks). So etwas kann man meist löschen oder zumindest genau prüfen, ob wirklich nicht benötigt.
Beispiel – Fehlende Indizes Vorschläge auswerten:
SELECT
migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) AS Verbesserungscore,
mid.statement AS [Table],
ISNULL(mid.equality_columns, “) + ISNULL(‚ AND ‚ + mid.inequality_columns, “) AS IndexSchluessel,
mid.included_columns AS IncludeSpalten,
migs.user_seeks, migs.user_scans
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
WHERE mid.database_id = DB_ID()
ORDER BY Verbesserungscore DESC
Dies ist ein komplexerer Query, der versucht, die fehlenden Indexvorschläge nach einem Score (geschätzter Einfluss * Nutzungsanzahl) zu ordnen. Er gibt die Tabelle, vorgeschlagene Key-Spalten (equality_columns sind die exakten Matches aus WHERE/JOIN, inequality_columns sind Range-Predicates wie >, <, BETWEEN) und vorgeschlagene Include-Spalten aus. Die Interpretation solcher Ergebnisse sollte mit Vorsicht erfolgen. Oft sieht man hier z. B.: Table = Orders, Key = CustomerID, Include = OrderDate, TotalAmount, etc., weil genau unsere Beispielabfrage gefehlt hat. Als Entwickler kann man dann entscheiden, ob man genau diesen Index anlegt oder anpasst.
Flexibel bleiben: Ein zentraler Rat bei dynamischem Anpassen: Testen Sie Änderungen (idealerweise in Staging-Systemen oder in ruhigeren Zeiten auf Prod mit A/B-Vergleichen). Jeder neue Index kann unerwartete Nebenwirkungen auf den Optimizer haben (manchmal wählt er dann einen anderen Plan für eine Query, der evtl. sogar schlechter ist – das kommt selten vor, aber kann). Und jeder entfernte Index – ebenso vorsichtig – kann plötzlich eine Query verlangsamen, die man übersehen hat.
Daher: – Machen Sie eine Änderung nach der anderen und beobachten Sie das Ergebnis. – Halten Sie die Dokumentation up-to-date, welche Indizes existieren und warum (das hilft später, zu verstehen, ob ein alter Index noch gebraucht wird oder einst zur Lösungs eines Problems geschaffen wurde). – Überprüfen Sie periodisch (z. B. alle paar Monate oder Release-Zyklen) die Indexnutzung und Performance. Was vor einem Jahr optimal war, muss es heute nicht mehr sein, wenn Daten und Anwendung sich entwickelt haben.
Mit diesen dynamischen Anpassungen schließen wir den proaktiven Teil der Indexverwaltung. Zuletzt möchten wir noch auf spezifische Neuerungen ab SQL Server 2019 eingehen, die für Indizes relevant sind – damit Sie wissen, welche zusätzlichen Möglichkeiten moderne SQL Server-Versionen bieten.
7. Besonderheiten und Neuerungen ab SQL Server 2019
SQL Server 2019 (und die Folgeversion 2022) haben einige neue Features und Verbesserungen rund um Indizes eingeführt. Hier fassen wir die wichtigsten Punkte zusammen, die über das bisher Bekannte hinausgehen:
- Resumable Index Operations: Bereits in SQL Server 2017 wurde das Konzept eingeführt, Index-Neuaufbau pausieren und fortsetzen zu können (ALTER INDEX … REBUILD WITH (RESUMABLE = ON)). Ab SQL Server 2019 gilt das auch für das erstmalige Erstellen von Indizes (CREATE INDEX … WITH (RESUMABLE = ON)). Diese Funktion ist äußerst praktisch für sehr große Indizes: Man kann z. B. einen Indexbau nach 30 Minuten abbrechen (PAUSE), um in der Arbeitszeit keine Ressourcen zu blockieren, und in der Nacht fortsetzen. Oder bei einem Failover/Neustart geht nicht mehr die gesamte bereits geleistete Arbeit verloren, sondern der Indexbuild kann weiterlaufen. Um Resumable effektiv zu nutzen, sollte man auch eine MaxDOP und evtl. MAX_DURATION = x minutes angeben, um z. B. nach einem Wartungsfenster automatisch zu pausieren. Dieses Feature erhöht die Flexibilität im Index-Management beträchtlich.
- Optimierung für sequentielle Schlüssel: Weiter oben erwähnt, ermöglicht die Option OPTIMIZE_FOR_SEQUENTIAL_KEY = ON bei Indexen mit hohem Insert-Durchsatz auf einem monotonic steigenden Wert (z. B. Identity oder Zeitstempel) eine bessere Skalierung bei konkurrierenden Einfügungen. SQL Server 2019 hat diese Option neu eingeführt, um das sogenannte „Last Page Insert Latch Bottleneck“ zu entschärfen, der auftritt, wenn zu viele Prozesse gleichzeitig auf die letzte Index-Seite schreiben wollen. Mit dieser Option steuert die Engine intern den Zugriff so, dass sich Wartezeiten verringern. Für stark insert-lastige Tabellen mit solchem Muster lohnt es sich, diese Option zu aktivieren.
- Verbesserte Columnstore-Index-Verwaltung: In SQL Server 2019 wurden einige Verbesserungen rund um Columnstore eingeführt. Dazu gehören effizientere Indexwartung (Reorg/Rebuild) von Columnstores, geringerer Memory-Footprint für Metadaten, ein Low-Memory Load Pfad (damit große Bulkloads auch bei begrenztem RAM besser klarkommen) und generell Performance-Tweaks beim Schreiben in Batches. Außerdem kann man ab SQL 2019 Clustered Columnstore Indexe online erstellen und neuaufbauen (vorher war das eine offline-Operation) – wichtig für große Warehouse-Tabellen, die 24/7 verfügbar sein müssen.
- Indexe für neue Datentypen – JSON: JSON-Unterstützung kam zwar schon in SQL Server 2016 (als JSON-Funktionen auf NVARCHAR-Daten), aber richtig spannend wird es mit SQL Server 2022, der Preview auf 2025: Hier wird ein dedizierter JSON-Datentyp und damit auch ein JSON-Index eingeführt. Ähnlich dem XML-Index ermöglicht ein JSON-Index das schnelle Suchen innerhalb von JSON-Dokumenten, die in einer Spalte gespeichert sind. Die Syntax (CREATE JSON INDEX … ON Table(JsonColumn)) und Funktionsweise ähneln dem XML-Index – intern werden JSON-Pfade und Werte indexiert, sodass Abfragen mit JSON_VALUE, JSON_QUERY oder JSON_PATH_EXISTS erheblich beschleunigt werden. Dieses Feature ist zwar in SQL Server 2019 noch nicht vorhanden, aber Entwickler sollten wissen, dass in neueren Versionen auch für semistrukturierte JSON-Daten Indexierungsmöglichkeiten kommen.
- Intelligent Query Processing & Indizes: SQL Server 2019 führte das Paket Intelligent Query Processing ein. Einige Aspekte davon wirken sich indirekt auf Indizes aus. Beispielsweise Batch Mode on Rowstore – analytische Abfragen können im Batchmode ausgeführt werden auch ohne Columnstore Index, was manche Query-Patterns beschleunigt. Das betrifft zwar nicht die Indexstrukturen direkt, verbessert aber die Nutzung vorhandener Indizes in gewissen Szenarien (v.a. bei großen Aggregationen, da sie nun effizienter CPU nutzen können). Auch Table Variable Deferred Compilation (2019) kann dazu führen, dass Indizes auf Tabellenvariablen (die ja ab 2019 mit tatsächlicher Zeilenanzahl statt geschätztem 1 Planen) besser ausgenutzt oder gewählt werden.
- Mehr Replikate bei AOAG und Indizes: Bei Always On Availability Groups hat SQL 2019 die Unterstützung für mehr sekundäre Replikate (bis zu 5) hinzugefügt. Dies ist indirekt interessant für Indexstrategien, weil sekundäre Replikate oft Read-Only Workloads übernehmen (Read-scale). In solchen Architekturen kann man gezielt Indizes anlegen, die nur für Berichte genutzt werden, und die Last dieser Abfragen auf Sekundärserver verlagern. Das ist kein Feature im Index selbst, aber ein architektonischer Punkt: Indexe auf Read-Only Replikaten belasten den Primärserver nicht mit Schreibkosten, da diese Replikate nur lesen. Allerdings muss man mit Wartung dann schauen – Rebuild auf Primär repliziert sich etc.
- Verbesserte Datenträgerunterstützung (Memory, SSD) und Auswirkungen: SQL Server 2019 unterstützt Persistenten Speicher (PMEM) – zwar kein Index-Feature an sich, aber Operationen wie Sortieren beim Indexbuild können von schnelleren Storage profitieren. Zudem kommt 2019 mit Accelerated Database Recovery (ADR), was Transaktionslog-Handhabung verbessert – Index-Builds, die abgebrochen werden, sind hier schneller rollbackbar durch ADRs Versioning. Das ist eher ein DBA-Detail, aber erwähnenswert.
Kurzum, ab SQL Server 2019 hat man mehr Möglichkeiten, Indizes effizient zu erstellen und zu nutzen. Die Online- und Resumable-Funktionen machen das Leben leichter bei großen Tabellen, und Performance-Features wie Optimize_for_sequential_key verbessern Durchsatz in speziellen Fällen. In SQL Server 2022 wird der Bereich semistrukturierte Daten mit JSON-Indizes adressiert – analog zu XML-Indizes. Entwickler sollten diese Neuerungen im Hinterkopf behalten, wenn sie auf entsprechende Versionen setzen.
Abschließend folgt noch eine umfangreiche FAQ-Sektion mit praxisnahen Fragen und Antworten rund um Indizes, die viele Entwickler beschäftigen. Dies rundet unser Index-Kompendium ab.
FAQ – Häufig gestellte Fragen zu Indizes in SQL Server
-
Frage: Was ist der Unterschied zwischen einem gruppierten und einem nicht gruppierten Index?
Antwort: Ein gruppierter Index (Clustered Index) bestimmt die physische Sortierung der Daten in der Tabelle selbst – die Tabelle wird nach dem Indexschlüssel geordnet gespeichert. Es gibt pro Tabelle höchstens einen solchen Index. Ein nicht gruppierter Index (Nonclustered Index) ist hingegen eine separate Struktur, die Zeiger auf die Datenzeilen enthält, ohne die physische Ordnung der Tabelle zu ändern. Von diesen kann eine Tabelle viele haben. Bildlich: Der gruppierte Index ist wie das Inhaltsverzeichnis eines Buches, das die Kapitel in bestimmter Reihenfolge anordnet, während nicht gruppierte Indizes wie zusätzliche Register sind, die den Weg zu bestimmten Informationen zeigen, aber das Buch selbst bleibt unverändert sortiert. Im Abfrageplan sieht man beim gruppierten Index oft Clustered Index Seek/Scan (direkter Tabellenzugriff sortiert), bei nicht gruppierten Index Seek/Scan plus evtl. Key Lookup. -
Frage: Wie viele Indizes sollte eine Tabelle maximal haben?
Antwort: Es gibt kein fixes „Soll“, aber so wenige wie möglich, so viele wie nötig. Theoretisch darf eine Tabelle bis zu 999 Nonclustered und 1 Clustered Index haben (also 1000 insgesamt). Praktisch kommen die meisten Tabellen mit deutlich weniger aus (oft 5–10 Indexe oder weniger). Jeder Index bedeutet zusätzlichen Pflegeaufwand bei Schreibzugriffen und verbraucht Speicher. Entscheidend ist, dass die vorhandenen Indizes die wichtigsten Abfragen beschleunigen. Sobald Sie deutlich zweistellige Anzahlen an Indizes in Betracht ziehen, ist es Zeit zu prüfen, ob welche redundant sind oder zusammengelegt werden können. Besser, einen durchdachten Index, der mehrere Abfragen abdeckt, als mehrere einzelne für jede Kleinigkeit. -
Frage: Verlangsamen Indizes das Einfügen und Aktualisieren von Daten?
Antwort: Ja, jedes Schreiben wird durch Indizes tendenziell langsamer, da zusätzlich zur eigentlichen Dateneinfügung/-änderung auch alle betroffenen Indexstrukturen aktualisiert werden müssen. Je mehr Indizes eine Tabelle hat, desto mehr Arbeit pro INSERT/UPDATE/DELETE. Besonders Updates, die Indexschlüssel verändern, und Inserts können spürbar gebremst sein, wenn viele Indizes angepasst werden müssen. Im Extremfall kann ein massiver Bulk-Insert auf einer Tabelle mit 10 Indizes zehnmal langsamer sein als auf einer unindizierten Tabelle. Das heißt nicht, dass man keine Indizes verwenden sollte – die Lesevorteile überwiegen meist – aber man sollte unnötige Indizes vermeiden und bei sehr schreibintensiven Tabellen genau abwägen, welche Indizes wirklich gebraucht werden. -
Frage: Was ist Fragmentierung bei Indizes und warum ist sie problematisch?
Antwort: Fragmentierung bedeutet, dass die physische Reihenfolge der Indexseiten nicht mehr der logischen Sortierung entspricht bzw. dass viele Lücken auf den Seiten sind. Das passiert z. B. durch löschen oder unsortiertes Einfügen von Daten (Seiten werden aufgespalten, Daten verteilt sich). Hohe Fragmentierung führt dazu, dass beim sequentiellen Lesen des Index (z. B. Range-Scans) mehr Sprünge nötig sind und eventuell mehr Seiten gelesen werden als nötig (weil sie teils leer sind). Kurz: Fragmentierung kann die Leseperformance verschlechtern, da mehr I/O anfällt. Außerdem passt weniger relevante Daten auf einen Datenbankseiten-Cache (durch Lücken), was ineffizient ist. -
Frage: Wie kann man fragmentierte Indizes bereinigen oder optimieren?
Antwort: Man behebt Fragmentierung durch Neuorganisation oder Neuaufbau des Indexes: - ALTER INDEX … REORGANIZE: defragmentiert den Index in kleinen Schritten online, ordnet die Seiten physisch um, füllt Lücken auf, aber ändert nichts am Fillfactor. Gut bei mittlerer Fragmentierung (5–30%).
- ALTER INDEX … REBUILD: baut den Index komplett neu auf (kann auch online erfolgen, je nach Edition/Einstellung). Das gibt einen dichten, sortierten Index wie neu. Hier kann man einen Fillfactor neu setzen. Empfehlenswert bei hoher Fragmentierung (>30%). Nachteil: braucht mehr Ressourcen während des Vorgangs. Beide Ansätze reduzieren Fragmentierung und verbessern wieder die Performance von Scans/Seeks. Diese Operationen sollte man planmäßig durchführen, z. B. wöchentlich oder monatlich je nach Bedarf, möglichst in wartungsarmen Zeiten.
-
Frage: Wie kann ich prüfen, ob ein Index von einer Abfrage verwendet wird?
Antwort: Am einfachsten, indem man sich den Ausführungsplan der Abfrage anschaut. In SSMS kann man „Actual Execution Plan“ einschalten und die Query ausführen. Im grafischen Plan sieht man dann Operatoren wie Index Seek oder Index Scan, die den Namen des verwendeten Indexes angeben. Wenn dort z. B. „Index Seek (NonClustered) auf IX_MyTable_MyColumn“ steht, dann wurde dieser Index benutzt. Alternativ kann man auch SET STATISTICS IO ON nutzen: Wenn ein bestimmter Index genutzt wird, erscheint dessen Name in der Ausgabe mit den Anzahl der gelesenen Seiten. Ein weiterer Weg ist das Auslesen von sys.dm_exec_query_plan bzw. Query Store (dort sieht man für vergangene Ausführungen auch, welche Indexe Teil des Plans waren). Für eine sofortige Kontrolle ist der Execution Plan jedoch das Mittel der Wahl. -
Frage: Was passiert, wenn ich einen Index lösche? Gehen dabei Daten verloren?
Antwort: Beim Löschen eines Indexes (DROP INDEX) werden keine Tabellen-Daten gelöscht, sondern nur die Indexstruktur. Bei einem nicht gruppierten Index ist das unkritisch: der Index war nur eine Hilfsstruktur, die Daten bleiben unverändert in der Tabelle. Abfragen, die vorher diesen Index nutzten, müssen dann auf andere Weise auf die Daten zugreifen (ggf. langsamer). Beim Löschen eines gruppierten Indexes wird die Tabelle anschließend zu einem Heap. Die Datenzeilen sind danach immer noch alle da, aber es gibt keine definierte Sortierung mehr. SQL Server baut beim Drop des Clustered Index im Hintergrund die Heap-Struktur auf, damit die Daten weiterhin zugreifbar sind. Wichtig: Falls es nicht gruppierte Indizes gab, die auf dem Clustered Index basierten (also dessen Schlüssel zur Lokalisierung nutzten), werden diese beim Drop des CI entweder ebenfalls entfernt oder in interne Heap-Zeiger geändert. In jedem Fall: die Nutzdaten der Tabelle bleiben erhalten, aber die Performance bestimmter Abfragen kann sich ändern, da der gelöschte Index nicht mehr zur Verfügung steht. -
Frage: Was ist ein überdeckender Index (Covering Index)?
Antwort: Ein überdeckender Index ist ein Index, der alle Spalten enthält, die eine bestimmte Abfrage benötigt – entweder als Teil des Index-Schlüssels oder als Included Columns. Man sagt, der Index „deckt“ die Abfrage ab, weil die Datenbank die Abfrage rein aus dem Index beantworten kann, ohne auf die eigentliche Tabelle (oder den Clustered Index) zurückzugreifen. Beispiel: Eine Abfrage selektiert Spalten A, B mit Bedingung auf A. Ein Index auf (A) inklusive Spalte B würde diese Abfrage überdecken. Der Vorteil: Kein Key Lookup notwendig, was Zeit spart. Covering Indexe sind sehr effizient für häufige, gleichbleibende Abfragen, da sie die gesamte benötigte Information an einem Ort bündeln. -
Frage: Sollte ich jede Spalte indexieren?
Antwort: Nein, das wäre kontraproduktiv. Man sollte nicht jede Spalte einer Tabelle indizieren, nur um „für alle Fälle“ vorbereitet zu sein. Indizieren Sie vorrangig Spalten, die in WHERE-Klauseln, JOIN-Bedingungen oder als Sortierschlüssel (ORDER BY) häufig verwendet werden. Spalten, die kaum in Abfragen vorkommen, bringen als Index nichts außer Overhead. Auch Spalten mit sehr wenigen verschiedenen Werten (z. B. boolesche Flags) sind oft ineffektiv, weil der Index fast so viele Zeilen umfasst wie die Tabelle – da gewinnt man kaum etwas. Zudem: Viele einzelne Indizes können vom Optimizer nicht kombiniert werden (er nutzt meistens nur einen Index pro Tabelle pro Teil des Plans). Daher lieber gezielt Indizes anlegen, die mehrere nützliche Spalten kombinieren, statt jede Spalte einzeln. Schließlich belasten unnötige Indizes – wie erwähnt – die Schreibperformance und verschwenden Speicher. -
Frage: Was ist ein gefilterter Index und wann ist er sinnvoll?
Antwort: Ein gefilterter Index ist ein Index mit einer WHERE-Bedingung, sodass er nur einen Teil der Zeilen indexiert. Sinnvoll ist das, wenn Sie regelmäßig nur auf einen bestimmten Ausschnitt der Daten zugreifen. Beispiel: Eine Spalte „Status“ hat Werte ‚Aktiv‘ und ‚Inaktiv‘. Wenn 95% der Datensätze ‚Aktiv‘ sind, aber Sie oft nach ‚Inaktiv‘ filtern (die selten sind), dann kann ein gefilterter Index WHERE Status = ‚Inaktiv‘ sehr effizient sein. Er ist klein (enthält nur 5% der Zeilen) und beschleunigt genau die Abfragen, die diese seltenen Fälle suchen. Gefilterte Indizes sparen Speicher und Update-Aufwand, weil sie nicht die ganzen Daten abdecken. Weitere Szenarien: Indizes nur für „neue“/aktuelle Daten (z. B. Jahresindex für aktuelle Jahr) oder für besondere Werte (z. B. Index auf Spalte X nur wo X IS NOT NULL, falls NULLs häufig sind und nie gesucht werden). Wichtig: Die Abfragen müssen das Filterprädikat möglichst exakt enthalten, sonst benutzt der Optimizer den Index evtl. nicht. -
Frage: Wann sollte man einen Columnstore Index nutzen?
Antwort: Columnstore-Indizes lohnen sich bei analytischen Workloads – sprich, wenn Sie sehr große Tabellen haben und Abfragen typischerweise viele Zeilen verarbeiten (z. B. Aggregationen über Millionen von Zeilen), aber nur auf einigen Spalten operieren. Data Warehouse-Szenarien sind klassisch: z. B. eine Faktentabelle mit 100 Mio. Zeilen, auf der Summen und Durchschnitt berechnet werden. Columnstore-Indizes liefern hier drastisch bessere Performance (durch Kompression, spaltenweises Lesen und Batch-Processing). Sie erzielen oft 10-fache Geschwindigkeiten und enorme Speicherersparnis. Nicht geeignet sind Columnstores für OLTP-ähnliche Zugriffe (einzelne Zeilen anhand Schlüssel abrufen, häufig kleine Updates). Zwar lassen sich Columnstores auch updaten (seit SQL 2016), aber ihre Stärke sind Bulk-Loads und Read-Only-Analysen. Als Faustregel: Wenn Abfragen > 1 Mio. Zeilen scannen oder komplexe Analysen laufen und die Performance nicht reicht, sollte man Columnstore in Betracht ziehen. Für normale kleine Tabellen oder rein transaktionale Suchen ist ein klassischer Index besser. -
Frage: Was ist der Unterschied zwischen einem eindeutigen Index und einem Unique-Constraint?
Antwort: Ein Unique-Constraint (UNIQUE-Schlüssel in der Tabellendefinition) stellt sicher, dass eine Spalte oder Spaltenkombination nur einzigartige Werte enthält. Im Hintergrund implementiert SQL Server einen Unique-Constraint tatsächlich mithilfe eines eindeutigen Indexes. Das heißt, technisch ist ein UNIQUE INDEX und ein UNIQUE CONSTRAINT nahezu identisch – beide erzeugen einen Index, der Duplikate verhindert. Der Unterschied liegt eher in der Verwendungsabsicht: Der UNIQUE-Constraint gehört zum Datenmodell (er sagt: dieses Attribut darf sich nicht doppeln – z. B. eine E-Mail-Adresse pro User nur einmal). Ein Unique-Index kann man auch ohne Constraint setzen, oft zu Performance-Zwecken (z. B. man weiß, eine Spalte ist faktisch unique und möchte dem Optimizer helfen, aber es ist kein Geschäftsregel-Constraint). In SQL Server sind Unique Constraints aber lieber zu verwenden, wenn es um Datenintegrität geht, weil sie auch dem ORM/Tools signalisieren, dass hier Eindeutigkeit herrscht. Unterm Strich: Kein großer praktischer Unterschied – ein Unique Constraint erzeugt einfach einen entsprechenden Index. Zu erwähnen: Ein Unique-Index kann auch auf einer View erstellt werden (um eine Indexed View zu ermöglichen), Unique Constraint hingegen nur auf Tabellen. -
Frage: Sollten Fremdschlüssel-Spalten immer indiziert werden?
Antwort: Meistens ja, das ist empfehlenswert. Ein Fremdschlüssel (FK) verweist auf einen Primärschlüssel einer anderen Tabelle. Typischerweise führt man JOINS zwischen Parent und Child Tabelle auf diesen Schlüsseln aus (z. B. alle Bestellungen (Child) zu einem Kunden (Parent)). Wenn die Child-Tabelle (Bestellungen) keinen Index auf der FK-Spalte (CustomerID) hat, muss SQL Server beim Join alle Bestellungen sequenziell durchsuchen, was langsam ist. Mit Index auf CustomerID kann er gezielt die relevanten Bestellungen finden. Auch für das Enforcen der referentiellen Integrität bei Lösch- und Update-Operationen ist ein Index hilfreich (SQL Server prüft dann schneller, ob ein zu löschender Eltern-Datensatz noch Kinder hat). Daher: In den meisten Fällen legt man auf einer Fremdschlüssel-Spalte einen nicht gruppierten Index an. Ausnahme: Wenn die Tabelle winzig ist oder der FK nie in Abfragen benutzt wird. Aber oft merkt man schnell, dass es gebraucht wird, sobald die Daten wachsen. -
Frage: Was sind „Missing Index“-Empfehlungen und sollte man sie immer umsetzen?
Antwort: Das sind Vorschläge des SQL Servers für potenziell nützliche Indizes. Der Optimizer generiert diese, wenn er eine Query plant und dabei feststellt „Ein Index auf Spalte X würde diese Query beschleunigen“. In Ausführungsplänen erscheinen sie in grüner Schrift oder man kann sie über die DMV sys.dm_db_missing_index_details & Co auslesen. Man sollte sie allerdings nicht blind alle umsetzen. Warum? Der Optimizer gibt Vorschläge pro Abfrage, ohne Gesamtbild. Das führt zu Überschneidungen (viele ähnliche Indizes mit kleinen Variationen) und oft sehr breiten Indizes (alle vorkommenden Spalten als Include). Besser ist: Nutzen Sie die Empfehlungen als Ausgangspunkt. Prüfen Sie, welche Abfragen dahinterstecken und ob ein Index sinnvoll ist. Man kann häufig einen Index designen, der mehrere Missing-Index-Vorschläge abdeckt. Zudem priorisieren: Der „Impact“ Wert hilft, abzuschätzen, was viel bringt. Also: Missing Indexes sind wertvoll zur Identifikation, wo evtl. Indizes fehlen – aber die entscheidende Design- und Auswahlentscheidung sollte ein Entwickler/DBA treffen, nicht stumpf der Maschine überlassen. -
Frage: Wie kann ich die Nutzung von Indizes überwachen und ungenutzte Indizes finden?
Antwort: Über die DMV sys.dm_db_index_usage_stats. Dort steht für jeden Index, wie oft er durch Benutzerabfragen genutzt wurde (Spalten user_seeks, user_scans, user_lookups) und wie oft er modifiziert wurde (user_updates). Diese Zähler gelten seit dem letzten Server-Neustart bzw. Index-Rebuild. Mit einer geeigneten Abfrage (siehe Abschnitt 6 oben) kann man Indizes finden, die nie gelesen aber oft geschrieben wurden. Solche Indizes sind vermutlich ungenutzt. Außerdem kann man mit Query Store (ab SQL 2016) für einzelne Abfragen sehen, welche Indexe benutzt wurden. Oder Tools wie sp_BlitzIndex (von Brent Ozar) nutzen, die Indizes nach Nutzung, Grösse, etc. analysieren und Empfehlungen geben. Zusammengefasst: Die Index Usage Stats sind der direkteste Weg. Wichtig ist, die Beobachtungsperiode ausreichend groß zu wählen – wenn der Server gerade erst gestartet wurde, sind die Zähler alle auf Null. Man sollte also regelmäßig oder dauerhaft sammeln, um ein Bild über Tage/Wochen zu haben. -
Frage: Was bedeutet der Füllfaktor (Fillfactor) beim Erstellen eines Indexes?
Antwort: Der Fillfactor ist eine Prozentangabe, wie voll jede Index-Seite initial gefüllt werden soll. Beispielsweise Fillfactor 90 bedeutet: beim Erstellen/Rebuild eines Index lässt SQL Server auf jeder Seite etwa 10% Platz frei. Dieser freie Platz dient dazu, zukünftige Einfügungen in die sortierte Reihenfolge aufzufangen, ohne sofort eine neue Seite anlegen zu müssen (was einen teuren Page Split bedeuten würde). Ein Fillfactor ungleich 0 (bzw. 100) ist vor allem sinnvoll, wenn erwartet wird, dass zwischen vorhandenen Schlüsselwerten neue Werte eingefügt werden (z. B. bei nachträglicher Einfügung von Daten mit älteren Zeitstempeln, oder wenn ein Index nicht auf einer strikt steigenden ID basiert). Durch den freien Platz können einige Inserts erfolgen, bevor eine Seite voll ist. Der Nachteil eines niedrigen Fillfactors: Der Index wird größer (mehr Seiten, weil ja Luft drauf ist) und sehr viele leere Stellen bedeuten auch etwas mehr Leseaufwand. Standardmäßig ist Fillfactor 0/100 – also komplett füllen (SQL Server-weit kann man einen Default einstellen). Man sollte Fillfactor bewusst setzen, wenn man Probleme mit Fragmentierung durch Inserts beobachtet. Übliche Werte liegen zwischen 70 und 95, je nach Bedarf. Es ist ein Mittel, Page Splits vs. Dichte auszubalancieren. -
Frage: Braucht jede Tabelle einen Clustered Index?
Antwort: Es ist nicht zwingend erforderlich, aber in den allermeisten Fällen ja, vorteilhaft. Eine Tabelle ohne Clustered Index (Heap) kann funktionieren, aber hat einige Nachteile:- Kein spezifischer Sortierzwang -> gewisses Chaos auf dem Datenträger nach vielen Operationen (Heaps fragmentieren anders, haben sog. Forwarding Records nach Updates, was zu indirekten Zugriffen führen kann).
- Nicht gruppierte Indizes auf Heaps sind etwas weniger effizient, da ihre Zeiger ein RID (Row Identifier) sind, der sich ändern kann, wenn sich die Seite ändert (bei Clustered haben sie den Schlüssel als Referenz, was stabiler sein kann).
- Viele Optimierungsratgeber (und Microsoft selbst) empfehlen, möglichst einen Clustered Index zu haben, es sei denn, ein begründeter Spezialfall spricht dagegen. Spezialfälle, wo Heaps sinnvoll sein können: Sehr kleine Tabellen (keine spürbare Auswirkung), reine Insert-Only-Workloads wo Daten nie aktualisiert oder gelesen werden bevor ein Batch woanders hingeschoben wird (Staging tables), oder wenn man bewusst Seitengehalt streuen will. Aber das sind Ausnahmen. Kurz gesagt: Im Zweifel ja – definieren Sie einen passenden Clustered Index, um die Vorteile (Sortierung, Eindeutigkeit, effiziente Clustered Key Lookups etc.) mitzunehmen.
-
Frage: Was ist ein Volltextindex und wie unterscheidet er sich von normalen Indizes?
Antwort: Ein Volltextindex dient der performanten Suche in großen Textinhalten (z. B. Dokumententext, Beschreibungstexte), wo man nach Wörtern oder Phrasen suchen will, ggf. mit linguistischer Aufbereitung. Er unterscheidet sich stark von einem normalen (B-Baum) Index:- Er wird von der separaten Full-Text Engine verwaltet und indexiert Wörter (Tokens) statt exakt die bitweisen Werte. D.h., der Text „Datenbanken sind großartig“ würde im Volltextindex Einträge „Datenbanken“, „großartig“ (und evtl. Grundformen davon) erhalten. Ein normaler Index auf einer VARCHAR-Spalte hingegen indexiert zeichenweise die ganze Zeile, was für „enthält Wort X“ kaum hilft.
- Volltextindizes ermöglichen komplexe Suchabfragen wie CONTAINS (Wort kommt vor), FREETEXT (ähnliche Bedeutung) etc. Normale Indizes ermöglichen lediglich =, >,<, LIKE ‚abc%‘ (Präfix-Suche).
- Interne Struktur: Volltextindex ist eher wie ein Invertiertes Wörterbuch (jedes Wort -> Liste von Dokument-IDs wo es vorkommt), während ein normaler Index ein sortierter Baum nach dem gesamten Feldinhalt ist.
- Ein Volltextindex muss speziell gepflegt werden (per Volltextkatalog, und die Datenbank-Engine aktualisiert ihn asynchron). Zusammengefasst: Für reine Textsuche im Fließtext ist ein Volltextindex das Mittel der Wahl, ein normaler Index bringt dort wenig (vor allem für Suchbegriffe mitten im Text). Allerdings ist ein Volltextindex nur für Suche nach Wörtern geeignet – er ersetzt keine normalen Indizes für strukturierte Abfragen.
-
Frage: Worin besteht der Unterschied zwischen INDEX REORGANIZE und REBUILD?
Antwort: Beide dienen der Index-Wartung, aber mit Unterschieden:- REORGANIZE: Führt eine defragmentierung der bestehenden Indexseiten durch. Es läuft immer online (auch in Standard Edition), sprich die Tabelle ist währenddessen verfügbar. Reorganize arbeitet schrittweise und nimmt wenig Transaktionslog in Anspruch. Es konsolidiert Seiten, sortiert die Blattebene neu, aber ändert die Indexstruktur nicht grundlegend. Es lässt die Statistik unverändert. Reorganize kann man auch abbrechen, es macht dann einfach bis dahin das, was geschafft ist.
- REBUILD: Baut den Index komplett neu auf, als ob man ihn frisch erstellt. Standardmäßig offline (in Enterprise kann man WITH ONLINE = ON angeben für Online-Rebuild). Rebuild verwendet einen neuen Index in Tempdb (bzw. intern getrennt) und tauscht dann um – ergo benötigt es mehr CPU, evtl. viel Tempdb-Space und erzeugt eine größere Transaktion (Log). Ergebnis ist ein brandneuer Index – komplett defragmentiert, Statistiken sind aktualisiert, Fillfactor wird neu angewandt. Rebuild kann nicht schrittweise fortgesetzt werden (außer man nutzt SQL 2019 Resumable und pausiert/resumed). Rebuild ist umfassender und oft auch schneller als ein umfassendes Reorganize, aber eben invasiver. Wann was? Oft sagt man: unter ~10-30% Fragmentation -> Reorganize reicht; über ~30% -> Rebuild. Und Rebuild auf jeden Fall, wenn man die Stats frisch haben will oder Fillfactor ändern muss (das geht nur per Rebuild). In Wartungsplänen wird oft nach dieser Logik verfahren.
-
Frage: Welche neuen Index-Funktionen gibt es in SQL Server 2019 und 2022?
Antwort: Ab SQL Server 2019 kamen einige Neuerungen:- Resumable Online Index Create/Rebuild: Möglichkeit, Indexerstellungen und -neuaufbauten zu pausieren und später fortzusetzen, inkl. im Online-Modus (siehe Abschnitt 7). Sehr hilfreich für die Wartung großer Indizes ohne langen Lock.
- OPTIMIZE_FOR_SEQUENTIAL_KEY: Neue Indexoption, um Insert-Engpässe bei sequentiellen Schlüsseln zu entschärfen (vor allem relevant für Hochlast-Insert-Szenarien).
- Verbesserungen bei Columnstore-Indizes: Schnellere Bulkloads, weniger Memory overhead, Online-Betrieb beim Erstellen von Clustered Columnstore Indizes.
- System-Views für Missing Indexes erweitert: z. B. sys.dm_db_missing_index_group_stats_query (ab 2019) zeigt die letzten Abfragen zu Missing Index Vorschlägen, was das Tuning erleichtert.
- SQL Server 2022 JSON Index: Möglichkeit, JSON-Daten effizient zu indizieren (experimentell in 2022 eingeführt, mit JSON-Datentyp). Damit können JSON_VALUE/JSON_QUERY Abfragen deutlich beschleunigt werden, ähnlich wie XML-Indizes für XML.
- Query Store forcierte Pläne & automatische Index-Tuning (Azure): Nicht direkt neue Indextypen, aber neue Möglichkeiten, problematische Plan-Änderungen (die evtl. durch Indizes beeinflusst sind) zu erkennen und stabil zu halten; sowie in Cloud-Umgebungen automatisches Hinzufügen/Entfernen von Indizes auf Basis der Nutzungsdaten. Insgesamt geht der Trend dahin, mehr Automation zu bieten und spezielle Indexfunktionen für neue Datenformate (JSON) oder Leistungshindernisse (Latch-Bottlenecks bei Inserts) bereitzustellen. Als Entwickler sollte man sich beim Upgrade mit den Release Notes vertraut machen, um zu sehen, welche Verbesserungen man nutzen kann – oft steckt der Gewinn in solchen Details.
Mit diesen ausführlichen Erklärungen und Tipps sind Sie bestens gerüstet, um das Thema Indizes in SQL Server (ab Version 2019) erfolgreich in der Praxis anzuwenden. Indizes sind ein mächtiges Werkzeug – richtig eingesetzt verbessern sie die Performance erheblich, falsch oder zu viele eingesetzt können sie aber auch schaden. Finden Sie die Balance, überwachen Sie Ihr System und passen Sie an, dann werden Indizes zu Ihren größten Verbündeten beim Datenbank-Tuning! Viel Erfolg in der Praxis.
Weitere Beiträge zum Thema SQL Server
Azure SQL für IT-Entscheider
1. Management Summary Azure SQL bezeichnet eine Familie von Microsofts Cloud-Datenbankdiensten, die SQL Server-Technologie in Azure als Service bereitstellen. Dazu gehören Azure SQL Database (ein einzeldatenbankbasierter PaaS-Dienst für moderne Anwendungen), Azure SQL...
Azure SQL für Entwickler
Management Summary Azure SQL (PaaS) bietet Softwareentwicklern eine fully-managed SQL-Plattform in der Cloud – mit integrierter Hochverfügbarkeit, automatischen Backups und einfacher Skalierbarkeit. Im Vergleich zu einer selbstverwalteten SQL Server-Instanz entfallen...
NUMA – Grundlagen und Anwendung in SQL Server 2022
Grundlagen von NUMA (Non-Uniform Memory Access) Was ist NUMA? NUMA (Nicht-uniformer Speicherzugriff) ist eine Architektur für Mehrprozessorsysteme, bei der jeder Prozessor über einen eigenen lokalen Arbeitsspeicher verfügt. Alle Prozessoren teilen sich zwar...
NUMA, MAXDOP und Co.: Die größten Fehler und Mythen bei der SQL-Server-Konfiguration
Einleitung In der Datenbankadministration von Microsoft SQL Server gibt es eine Reihe von Konfigurationsthemen – insbesondere rund um NUMA (Non-Uniform Memory Access), MAXDOP (Max Degree of Parallelism) und verwandte Einstellungen – bei denen immer wieder typische...
Microsoft SQL Server unter Linux – Strategische Analyse und Praxisleitfaden
1. Management Summary Microsofts Entscheidung, SQL Server auch unter Linux anzubieten, markiert einen strategischen Wandel mit weitreichenden Auswirkungen für IT-Entscheider. Erstmals steht damit eine der führenden relationalen Datenbankplattformen...
Blockgröße für SQL Server richtig auswählen (Windows und Linux)
Einleitung: Die Wahl der optimalen Blockgröße (auch Allocation Unit Size oder Dateisystem-Blockgröße genannt) für Datenträger ist ein wichtiger, oft unterschätzter Faktor beim Betrieb von Microsoft SQL Server unter Windows und Linux. Die Blockgröße eines Dateisystems...
Wartungspläne für Microsoft SQL Server
Management Summary Wartung sichert Verfügbarkeit und Datenintegrität: Geplante Wartungsarbeiten in SQL Server zielen darauf ab, die Verfügbarkeit von Datenbanken hoch zu halten und Datenintegrität zu gewährleisten. Sie minimieren Ausfallzeiten und Risiken und...
Virtualisierung von SQL Server, Best Practices
Management Summary Virtualisierung von Microsoft SQL Server ermöglicht es Unternehmen, Datenbank-Workloads effizienter bereitzustellen und zu verwalten. Durch Konsolidierung mehrerer SQL-Server-Instanzen auf weniger Hardware steigern Organisationen die Auslastung und...
SQL Performance-Analyse (hypothetisches Beispiel)
Management Summary Die Performance-Analyse einer Microsoft SQL-Server-Instanz (Version 2019) hat CPU- und I/O-Engpässe als Hauptprobleme identifiziert. In Spitzenzeiten lag die CPU-Auslastung dauerhaft über 90 %, und die Speicher-I/O-Latenz der Datenbanken überschritt...
Indexoptimierung bei SQL Server – Leitfaden für IT-Verantwortliche und DBAs
Einleitung: Wozu dienen Indizes im SQL Server? Indizes sind essenziell, um SQL Server Abfragen zu beschleunigen und die Datenbank-Performance zu verbessern. Ein Index funktioniert ähnlich wie das Inhaltsverzeichnis eines Buches: Anstatt eine Tabelle vollständig zu...