SQL Index und Stats

Index-Fragmentierung & Statistik-Pflege in MS SQL Server

Wer SQL Server administriert, stolpert früher oder später über zwei Themen, die Performance direkt beeinflussen:

Praxisnah erklärt – inkl. Kombi-Maintenance Script

  • Index-Fragmentierung
  • veraltete Statistiken (Statistics)

Beides hängt nicht technisch zusammen, wirkt aber gemeinsam auf Ausführungspläne und I/O-Last. In diesem Artikel zeige ich den praktischen Zusammenhang – inklusive zweier Script-Varianten, die Fragmentierung prüfen, Indizes warten und Statistics gleich mit aktualisieren.

Locker erklärt, aber produktionsnah.


Warum Fragmentierung überhaupt relevant ist

Bei vielen Inserts, Updates und Deletes entstehen Lücken und Seiten-Splits in Indizes. Die Daten liegen dann nicht mehr logisch sortiert hintereinander, sondern verteilt.

Folgen können sein:

  • mehr I/O bei Scans
  • schlechtere Cache-Effizienz
  • längere Laufzeiten bei Reports / Batch-Jobs

Faustregeln:

FragmentierungMaßnahme
< 5 %ignorieren
5–30 %REORGANIZE
> 30 %REBUILD

Warum Statistics genauso wichtig sind

Statistics beschreiben die Datenverteilung für den Query Optimizer. Sind sie alt oder “dirty”, entstehen:

  • falsche Cardinality-Schätzungen
  • schlechte Join-Strategien
  • unnötige Scans / Sorts

Wichtig:
Index-Maintenance aktualisiert Stats nicht immer automatisch:

AktionStats danach
REBUILDautomatisch frisch (Fullscan)
REORGANIZEunverändert

Deshalb ist eine Kombi-Wartung sinnvoll.


Script-Variante 1

Kombi-Maintenance (Index + Stats)

Diese Version:

  • prüft Fragmentierung
  • entscheidet REBUILD / REORGANIZE
  • aktualisiert danach die betroffenen Stats
USE [DEINE DB];
GO
SET NOCOUNT ON;

DECLARE 
    @RebuildThreshold  float = 30.0,
    @ReorgThreshold    float = 10.0,
    @MinPageCount      int   = 500;

DECLARE 
    @SchemaName sysname,
    @TableName  sysname,
    @IndexName  sysname,
    @Frag       float,
    @PageCount  int,
    @sql        nvarchar(max);

DECLARE IndexCursor CURSOR LOCAL FAST_FORWARD FOR

SELECT
    s.name,
    o.name,
    i.name,
    ips.avg_fragmentation_in_percent,
    ips.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i
  ON i.object_id = ips.object_id AND i.index_id = ips.index_id
JOIN sys.objects o
  ON o.object_id = i.object_id
JOIN sys.schemas s
  ON s.schema_id = o.schema_id
WHERE o.type = 'U'
  AND ips.index_id > 0
  AND ips.page_count >= @MinPageCount
  AND i.is_disabled = 0
  AND i.is_hypothetical = 0
  AND ips.avg_fragmentation_in_percent >= @ReorgThreshold;

OPEN IndexCursor;
FETCH NEXT FROM IndexCursor
INTO @SchemaName, @TableName, @IndexName, @Frag, @PageCount;

WHILE @@FETCH_STATUS = 0
BEGIN
    SET @sql = NULL;

    IF @Frag >= @RebuildThreshold
    BEGIN
        SET @sql = N'
        ALTER INDEX ' + QUOTENAME(@IndexName) + N'
        ON ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + N'
        REBUILD;

        UPDATE STATISTICS ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) +
        N' (' + QUOTENAME(@IndexName) + N') WITH FULLSCAN;';
    END
    ELSE IF @Frag >= @ReorgThreshold
    BEGIN
        SET @sql = N'
        ALTER INDEX ' + QUOTENAME(@IndexName) + N'
        ON ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + N'
        REORGANIZE;

        UPDATE STATISTICS ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) +
        N' (' + QUOTENAME(@IndexName) + N');';
    END

    IF @sql IS NOT NULL
        EXEC sys.sp_executesql @sql;

    FETCH NEXT FROM IndexCursor
    INTO @SchemaName, @TableName, @IndexName, @Frag, @PageCount;
END

CLOSE IndexCursor;
DEALLOCATE IndexCursor;

Eigenschaften

  • Vollautomatisch
  • Wartet nur Indizes über Schwellwert
  • Aktualisiert Stats gezielt
  • Produktionsnah einsetzbar

Script-Variante 2

Laufzeitoptimiert (Top-N Wartung)

Wenn Datenbanken groß sind, kann Variante 1 lange laufen. Deshalb begrenzen viele Admins pro Wartungsfenster die Anzahl der Indizes.

Diese Version bearbeitet nur die „schlimmsten“ N:

USE [Deine DB];
GO
SET NOCOUNT ON;

DECLARE 
    @RebuildThreshold  float = 30.0,
    @ReorgThreshold    float = 10.0,
    @MinPageCount      int   = 500,
    @TopN              int   = 50;

DECLARE 
    @SchemaName sysname,
    @TableName  sysname,
    @IndexName  sysname,
    @Frag       float,
    @PageCount  int,
    @sql        nvarchar(max);

DECLARE IndexCursor CURSOR LOCAL FAST_FORWARD FOR

SELECT TOP (@TopN)
    s.name,
    o.name,
    i.name,
    ips.avg_fragmentation_in_percent,
    ips.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i
  ON i.object_id = ips.object_id AND i.index_id = ips.index_id
JOIN sys.objects o
  ON o.object_id = i.object_id
JOIN sys.schemas s
  ON s.schema_id = o.schema_id
WHERE o.type = 'U'
  AND ips.index_id > 0
  AND ips.page_count >= @MinPageCount
  AND ips.avg_fragmentation_in_percent >= @ReorgThreshold
ORDER BY ips.avg_fragmentation_in_percent DESC, ips.page_count DESC;

OPEN IndexCursor;
FETCH NEXT FROM IndexCursor
INTO @SchemaName, @TableName, @IndexName, @Frag, @PageCount;

WHILE @@FETCH_STATUS = 0
BEGIN
    IF @Frag >= @RebuildThreshold
        SET @sql = N'ALTER INDEX ' + QUOTENAME(@IndexName) + N'
                     ON ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + N'
                     REBUILD;';
    ELSE
        SET @sql = N'ALTER INDEX ' + QUOTENAME(@IndexName) + N'
                     ON ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + N'
                     REORGANIZE;';

    EXEC sys.sp_executesql @sql;

    FETCH NEXT FROM IndexCursor
    INTO @SchemaName, @TableName, @IndexName, @Frag, @PageCount;
END

CLOSE IndexCursor;
DEALLOCATE IndexCursor;

Vorteile

  • planbare Wartungsdauer
  • geeignet für Nachtjobs
  • reduziert Log- und I/O-Spitzen

Praxis-Lessons Learned

Ein paar Dinge, die in der Realität gern übersehen werden:

1. PageCount-Filter ist entscheidend
Zu hoch → nichts passiert
Zu niedrig → Wartung dauert ewig

2. Kleine Indizes bringen kaum Performancegewinn
Aber kosten trotzdem I/O bei Wartung.

3. Fragmentierung ≠ Performanceproblem
Oft sind Stats oder fehlende Indizes relevanter.

4. REBUILD erzeugt viel Log
Bei knappen Logfiles oder Storage-Issues vorsichtig einsetzen.


Typischer Wartungsfahrplan

So fahren es viele produktiv:

FrequenzAktion
täglichTop-N Reorg + Stats
wöchentlichgroße Rebuilds
monatlichFullscan Stats

Fazit

Index-Fragmentierung und Statistics sollten nicht getrennt betrachtet werden. Erst die Kombination sorgt dafür, dass:

  • physische Datenstruktur sauber ist
  • der Optimizer korrekte Pläne erzeugt

Mit den gezeigten Scripts lässt sich eine solide, automatisierte Wartung aufbauen — von „Quick Win“ bis produktionsreif skalierbar.

Und wie immer gilt:
Schwellenwerte, PageCount-Filter und Laufzeitbegrenzung an die eigene Umgebung anpassen — nicht blind übernehmen.


Wenn du das selbst nachbauen willst, starte konservativ:

  • Reorg ab 10 %
  • Rebuild ab 30–40 %
  • PageCount ≥ 500 oder 1000

Und dann messen, nicht raten.

Happy Index Gardening 🌱

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert