SQL Server: Ungenutzte und redundante Indizes sicher erkennen
Stellen Sie sich vor, jede Änderung einer Kundenadresse müsste nicht nur im Kundenstamm, sondern auch in zwölf Karteikästen nachgetragen werden. Einige davon öffnet seit Monaten niemand. Genau so können überflüssige Indizes Ihren SQL Server beschäftigen: Sie beschleunigen keine Abfrage mehr, verursachen aber weiterhin Arbeit.
Die Schwierigkeit liegt im Wort „überflüssig“. Ein Index ohne registrierte Lesezugriffe kann tatsächlich nutzlos sein – oder den Jahresabschluss retten. Deshalb braucht Indexbereinigung drei Dinge: einen belastbaren Messzeitraum, einen präzisen Definitionsvergleich und einen kontrollierten Rückweg.
Was überflüssige Indizes bei INSERT, UPDATE und DELETE kosten
Ein klassischer Rowstore-Index organisiert Daten als B-Baum: eine verzweigte Suchstruktur, ähnlich dem Register eines Buches. Ein Nonclustered-Index ist dabei eine zusätzliche Struktur neben den eigentlichen Tabellendaten.
Diese Struktur muss aktuell bleiben:
- INSERT: Neue Zeilen erzeugen Einträge in allen zutreffenden Indizes.
- DELETE: Zugehörige Indexeinträge müssen entfernt beziehungsweise zunächst zur späteren Bereinigung markiert werden.
- UPDATE: Ändern sich Schlüsselspalten, INCLUDE-Spalten oder die Zugehörigkeit zu einem gefilterten Index, kann zusätzliche Indexpflege erforderlich werden. Nicht jedes UPDATE verändert jeden Index.
INCLUDE-Spalten sind mitgeführte Nutzdaten, die Abfragen direkt aus dem Index lesen können, ohne zur Tabellenzeile zurückzugehen.
Die Pflege kostet CPU, zusätzliche Schreibzugriffe und Einträge im Transaktionsprotokoll. Bei Platzmangel können Seitenteilungen entstehen: SQL Server verteilt Indexeinträge auf weitere Speicherseiten. Auch Speicherplatz, Backups und Indexwartung werden größer.
Entscheidend ist deshalb nicht die Anzahl der Indizes, sondern ihr Verhältnis von Nutzen zu Pflegekosten.
Den Messzeitraum richtig wählen
Die Dynamic Management View, kurz DMV, sys.dm_db_index_usage_stats liefert Nutzungszähler. Sie ist jedoch kein dauerhaftes Fahrtenbuch.
Beim Neustart der SQL-Server-Instanz werden die Zähler zurückgesetzt. Auch das Entfernen beziehungsweise Schließen einer Datenbank kann deren Einträge verschwinden lassen, etwa bei aktiviertem AUTO_CLOSE.
Den Instanzstart prüfen Sie so:
SELECT sqlserver_start_time
FROM sys.dm_os_sys_info;
„Seit drei Tagen nicht gelesen“ bedeutet wenig, wenn vor drei Tagen ein Wartungsneustart stattfand. Berücksichtigen Sie Monatsabschlüsse, Quartalsberichte, nächtliche Schnittstellen und seltene Supportabfragen.
Speichern Sie deshalb regelmäßig Snapshots der Zähler zusammen mit Erfassungszeitpunkt und Instanzstart. Bei Availability Groups müssen außerdem die relevanten Replikate betrachtet werden: Lesezugriffe auf einem Secondary erscheinen nicht automatisch in der Statistik des Primary.
Für diese Server-DMVs benötigen Sie passende Berechtigungen: typischerweise VIEW SERVER STATE, ab SQL Server 2022 VIEW SERVER PERFORMANCE STATE. Auch ausreichende Metadatensichtbarkeit ist wichtig.
Ungenutzte Indizes finden – auch ohne DMV-Eintrag
Die folgende Abfrage läuft in der untersuchten Datenbank. Sie betrachtet bewusst nur aktive, nicht eindeutige Nonclustered-Rowstore-Indizes und schützt bereits mehrere sensible Kategorien:
SELECT
s.name AS schema_name,
t.name AS table_name,
i.name AS index_name,
CASE WHEN u.index_id IS NULL
THEN N'Kein DMV-Eintrag'
ELSE N'Keine registrierten Benutzer-Lesezugriffe'
END AS finding,
COALESCE(u.user_seeks, 0) AS user_seeks,
COALESCE(u.user_scans, 0) AS user_scans,
COALESCE(u.user_lookups, 0) AS user_lookups,
COALESCE(u.user_updates, 0) AS user_updates,
u.last_user_seek,
u.last_user_scan,
u.last_user_lookup,
u.last_user_update
FROM sys.tables AS t
JOIN sys.schemas AS s
ON s.schema_id = t.schema_id
JOIN sys.indexes AS i
ON i.object_id = t.object_id
LEFT JOIN sys.dm_db_index_usage_stats AS u
ON u.database_id = DB_ID()
AND u.object_id = i.object_id
AND u.index_id = i.index_id
WHERE t.is_ms_shipped = 0
AND i.type = 2
AND i.is_disabled = 0
AND i.is_hypothetical = 0
AND i.is_unique = 0
AND i.is_primary_key = 0
AND i.is_unique_constraint = 0
AND COALESCE(u.user_seeks, 0) = 0
AND COALESCE(u.user_scans, 0) = 0
AND COALESCE(u.user_lookups, 0) = 0
ORDER BY COALESCE(u.user_updates, 0) DESC,
s.name, t.name, i.name;
Der LEFT JOIN ist entscheidend: Ein Index ohne DMV-Eintrag bleibt sichtbar. Ein INNER JOIN würde ausgerechnet diese Kandidaten ausblenden.
Seeks sind gezielte Suchzugriffe, Scans durchlaufen Indexbereiche, Lookups holen ergänzende Daten. Hier werden Benutzerzugriffe ausgewertet, nicht sämtliche internen Systemzugriffe.
Wichtig: user_updates zählt Pflegeoperationen durch Anweisungen, nicht geänderte Zeilen oder physische Schreibvorgänge. Ein DELETE über viele Zeilen zählt nicht entsprechend viele Updates. Die Sortierung liefert daher Hinweise, keine exakte Kostenrechnung.
Doppelte Indizes per T-SQL erkennen
Zwei ähnlich benannte Indizes sind noch keine Duplikate. Verglichen werden müssen insbesondere:
- Schlüsselspalten in ihrer Reihenfolge, einschließlich ASC/DESC;
- INCLUDE-Spalten als Menge, deren Reihenfolge unerheblich ist;
- Filterdefinition und Eindeutigkeit.
Diese Abfrage benötigt SQL Server 2017 oder neuer und einen geeigneten Kompatibilitätsgrad für die sortierte Aggregation:
WITH Definitions AS
(
SELECT
i.object_id,
i.index_id,
i.name,
i.is_unique,
i.is_primary_key,
i.is_unique_constraint,
i.data_space_id,
i.filter_definition,
k.key_columns,
n.include_columns
FROM sys.indexes AS i
JOIN sys.tables AS t
ON t.object_id = i.object_id
CROSS APPLY
(
SELECT STRING_AGG(
CAST(CONCAT(ic.column_id, N':',
ic.is_descending_key) AS nvarchar(max)),
N','
) WITHIN GROUP (ORDER BY ic.key_ordinal)
AS key_columns
FROM sys.index_columns AS ic
WHERE ic.object_id = i.object_id
AND ic.index_id = i.index_id
AND ic.key_ordinal > 0
) AS k
CROSS APPLY
(
SELECT STRING_AGG(
CAST(ic.column_id AS nvarchar(max)), N','
) WITHIN GROUP (ORDER BY ic.column_id)
AS include_columns
FROM sys.index_columns AS ic
WHERE ic.object_id = i.object_id
AND ic.index_id = i.index_id
AND ic.is_included_column = 1
) AS n
WHERE i.type = 2
AND i.is_disabled = 0
AND i.is_hypothetical = 0
AND t.is_ms_shipped = 0
)
SELECT
OBJECT_SCHEMA_NAME(a.object_id) AS schema_name,
OBJECT_NAME(a.object_id) AS table_name,
a.name AS index_a,
b.name AS index_b,
a.is_unique,
a.is_primary_key AS a_is_primary_key,
b.is_primary_key AS b_is_primary_key,
a.is_unique_constraint AS a_is_unique_constraint,
b.is_unique_constraint AS b_is_unique_constraint
FROM Definitions AS a
JOIN Definitions AS b
ON b.object_id = a.object_id
AND b.index_id > a.index_id
AND b.is_unique = a.is_unique
AND b.data_space_id = a.data_space_id
AND b.key_columns = a.key_columns
AND ISNULL(b.include_columns, N'')
= ISNULL(a.include_columns, N'')
AND ISNULL(b.filter_definition, N'')
COLLATE Latin1_General_100_BIN2
= ISNULL(a.filter_definition, N'')
COLLATE Latin1_General_100_BIN2;
Die Ausgabe zeigt Definitionspaare, keine automatisch freigegebenen Löschkandidaten. Der Filtervergleich ist textuell: Logisch gleichwertige, unterschiedlich formulierte Filter werden nicht zuverlässig als gleich erkannt.
Prüfen Sie zusätzlich Partitionierung, Komprimierung und Indexoptionen. Implizit mitgeführte Clustered-Schlüssel erfordern bei komplexeren Vergleichen ebenfalls Aufmerksamkeit.
Überlappende Indizes analysieren
Ein Index auf (KundenID, Bestelldatum) überschneidet sich mit einem Index auf (KundenID). Er ist aber nicht zwangsläufig der bessere Ersatz: Der schmalere Index benötigt möglicherweise weniger Speicherseiten und kann bestimmte Abfragen günstiger bedienen.
Umgekehrt ersetzt (Bestelldatum, KundenID) nicht gleichwertig (KundenID, Bestelldatum). Die führende Schlüsselspalte bestimmt wesentlich, welche Suchbedingungen effizient unterstützt werden.
Auch INCLUDE ist kein Ersatz für eine Schlüsselposition: Eine mitgeführte Spalte deckt Datenbedarf ab, bietet aber nicht dieselben Such- und Sortiereigenschaften.
Vergleichen Sie deshalb reale Ausführungspläne und Messwerte im Query Store, dem persistenten Speicher für Abfragen, Pläne und Laufzeitstatistiken. Besonders gefilterte Indizes verdienen eine Einzelprüfung: Ein kleiner Index für offene Aufträge kann trotz geringer Zugriffszahl äußerst wertvoll sein.
Löschkandidaten absichern
Vor jeder Änderung gilt:
- Unique-Indizes schützen: Sie verhindern doppelte Schlüsselwerte, selbst ohne registrierte Lesezugriffe.
- PRIMARY KEY und UNIQUE Constraints schützen: Die zugehörigen Indizes sichern Datenintegrität und können für Fremdschlüssel relevant sein.
- Clustered-Indizes ausnehmen: Ihr Deaktivieren macht die zugrunde liegenden Tabellendaten unzugänglich und beeinträchtigt abhängige Strukturen.
- Abhängigkeiten prüfen: Explizite Index-Hints, Plan Guides und erzwungene Pläne können auf den Index angewiesen sein.
Sichern Sie das vollständige Erstellungsskript einschließlich Filter, INCLUDE-Spalten, Partitionierung und Optionen. Ein gespeicherter Indexname allein ist kein Rückweg.
Geeignete Nonclustered-Indizes zuerst deaktivieren
Für geeignete, nicht eindeutige Nonclustered-Indizes ist Deaktivieren ein kontrollierbarer Test. Mit an Ihre Datenbank angepassten Objektbezeichnungen:
ALTER INDEX [IX_Auftrag_Kunde]
ON [dbo].[Auftrag] DISABLE;
Die Definition bleibt erhalten, die Indexdaten werden entfernt. SQL Server kann den Index nicht mehr verwenden und muss ihn bei Datenänderungen nicht pflegen.
Das ist trotzdem ein produktiver Eingriff: Index-Hints können Fehler verursachen, Abfragen plötzlich langsamer werden. Deaktivieren Sie deshalb jeweils wenige Kandidaten in einem abgestimmten Wartungsfenster.
Überwachen Sie Laufzeiten, CPU, logische Reads, Timeouts und kritische Geschäftsprozesse über einen repräsentativen Zeitraum.
Der Rückweg lautet:
ALTER INDEX [IX_Auftrag_Kunde]
ON [dbo].[Auftrag] REBUILD;
Ein REBUILD baut die Struktur vollständig neu auf. Planen Sie dafür Laufzeit, Speicherplatz, Transaktionsprotokoll und mögliche Sperren ein. Es ist kein sofortiger Einschalter.
Fazit
Ein ungenutzter Index ist zunächst eine Hypothese. Belastbar wird sie durch ausreichend lange Messung, sorgfältigen Definitionsvergleich und geprüfte Abhängigkeiten.
Schützen Sie Integritätsregeln, testen Sie geeignete Nonclustered-Indizes zunächst durch Deaktivieren und halten Sie den Wiederaufbau bereit. So entfernen Sie unnötige Schreibarbeit, ohne versehentlich das Register herauszureißen, das beim nächsten Monatsabschluss gebraucht wird.
Stand: November 2026. Funktionen, Versionen und Preise können sich seitdem geändert haben.
Sie möchten tiefer einsteigen? In unseren Schulungen vermitteln wir SQL Server, Power BI und KI praxisnah – online oder bei Ihnen vor Ort. Fragen? Kontaktieren Sie uns oder buchen Sie ein kostenloses Info-Gespräch.