Index Maintenance oder dm_db_index_physical_stats ist langsam

By Frank Kalis

Posted on Feb 2, 2011 von in SQL Server

Dass Indizes im SQL Server ihre Daseinsberechtigung haben ist nichts wirklich Neues. Dass diese Indizes hin und wieder auch gepflegt werden müssen, mag vielleicht manche Leute überraschen, die sich durch die ansonstige Pflegeleichtigkeit des SQL Servers haben einlullen lassen. Denkt man jedoch in Ruhe darüber nach, wird man einsehen, dass dies notwendig ist und dass es Sinn macht, dass SQL Server diese Aufgabe nicht automatisch einfach blind nach Schema F erledigt.

Dazu sind die verschiedenen Umgebungen in denen SQL Server eingesetzt wird einfach zu unterschiedlich, um alle in punkto Index-Wartung über einen Kamm scheren zu können. Was in der einen Umgebung vielleicht signifikante Auswirkungen auf die Performance hat, mag in einer anderen Umgebung akzeptabel sein, da dort gänzlich andere Abfragen laufen, denen ein nicht optimal gewarteter Index nicht viel ausmacht.

Doch schon allein das Wort "optimal" im vorherigen Satz kann man kontrovers diskutieren. Was ist "optimal"? Microsoft gibt zum Beispiel die Empfehlung ab, bei einer Fragmentierung zwischen 5% und 30% den Index zu reorganisieren und ab 30% aufwärts, den Index neu zu erstellen (nachzulesen hier). So weit, so gut. Nur, wie bei jeder Empfehlung sollte man sie von Zeit zu Zeit in seiner eigenen Umgebung kritisch hinterfragen, ob sie immer noch Sinn macht oder angepasst werden muss. Erst recht gilt dies, wenn man liest, mit welchen wissenschaftlichen Methoden diese Werte ermittelt wurden: Where do the Books Online index fragmentation thresholds come from? :-)

Wir haben hier eine Tabelle, in die brutto jeden Tag zwischen 10 und 20 Millionen Zeilen eingefügt werden. Nächtens läuft eine Maintenance Prozedur, die mehrere Millionen Zeilen, die älter als x Tage sind wieder löscht. Der Nettozuwachs in der Tabelle liegt so zwischen 3 und 5 Millionen Zeilen pro Tag. Diese Tabelle enthält zur Zeit ca. 975 Millionen Zeilen.

Die Fragmentierung festzustellen ist seit SQL Server 2005 ziemlich einfach geworden durch die neu eingeführte dynamische Managementfunktion sys.dm_db_index_physical_stats. Hier ist ein Teil des Skriptes, mit dem wir irgendwann einmal anfangen haben:

SELECT
OBJECT_SCHEMA_NAME(FRAG.[object_id]) + '.' + OBJECT_NAME(FRAG.[object_id]),
SIX.[name],
FRAG.avg_fragmentation_in_percent,
FRAG.page_count
FROM
sys.dm_db_index_physical_stats
(
DB_ID(), --use the currently connected database
0, --Parameter for object_id.
DEFAULT, --Parameter for index_id.
0, --Parameter for partition_number.
DEFAULT --Scanning mode. Default to "LIMITED", which is good enough
) FRAG
JOIN
sys.indexes SIX ON FRAG.[object_id] = SIX.[object_id] AND FRAG.index_id = SIX.index_id
WHERE
--don't bother with heaps, if we have these anyway outside staging tables.
FRAG.index_type_desc <> 'HEAP' AND
(
--Either consider only those indexes that need treatment
(FRAG.page_count > @IPageCnt AND FRAG.avg_fragmentation_in_percent > @IMinFragmentation)
OR
--or do everything when it is MaintenanceDay
@IsMaintenanceDay = 1
)
ORDER BY
FRAG.avg_fragmentation_in_percent DESC;

Die ermittelten Zeilen werden in eine temporäre Tabelle geschrieben und dann nacheinander abgearbeitet. Dabei wird nach jedem Durchlauf geschaut, ob innerhalb des täglichen Zeitfenster noch Zeit ist, eine weitere Operation anzustarten oder nicht.

In der WHERE Klausel filtern wir auf mehrere Kriterien:
@IPageCnt = Standardwert ist 128. Alles darunter ignorieren wir. Das Ergebnis rechtfertigt hier nicht den Aufwand.
@IMinFragmentation = 10%. Alles unter 10% wird ebenfalls ignoriert. Unter 10% konnten wir keinen signifikanten Einfluss auf die Gesamtperformance beobachten.
@IsMaintenanceDay = Einmal pro Woche haben wir ein Zeitfenster, in dem wir alle Indizes warten können. Zumindest haben wir das anfangs mal gedacht...

Abhängig von der ermittelten Fragmentierung haben wir dann entweder ein REORGANIZE oder ein REBUILD gemacht, so wie es die obige Empfehlung nahegelegt hat. Das hat solange funktioniert, bis wir dann mal eines Montag morgens festgestellen mussten, dass unser Index-Job nach 5 Stunden noch nicht fertig war, was den Backup Job, der dann gescheduled war nicht gerade erfreute.

Mehrere Anmerkungen dazu:
ALTER INDEX ... REORGANIZE ist single-threaded, d.h. egal wie viele Prozessoren die Maschine hat, diese Operation verwendet immer nur einen einzigen davon. Das macht das Warten von grossen Indizes über REORGANIZE ziemlich schnell zum Geduldsspiel.
ALTER INDEX...REBUILD kann Parallelismus verwenden, läuft also deutlich schneller verbraucht aber auch mehr Resourcen.
Nachzulesen in Konfigurieren von Parallelindexvorgänge

Okay, das kann man noch durch etwas Research selber herausbekommen und es erklärt natürlich auch zum gewissen Grad, dass man plötzlich zu Laufzeiten kommt, die in die Stunden gehen, wenn man die grössten Indizes aufgrund ihrer Fragmentierung "nur" reorganisiert.

Umso erstaunlicher war es jedoch herauszufinden, dass ein nicht gerade unerheblicher Anteil an dieser Laufzeit auf das Konto von sys.dm_db_index_physical_stats selbst geht, also auf das Konto der reinen Ermittlung der Fragmentierung. Uns war zwar bewusst, dass im LIMITED Modus alle Seiten oberhalb des Leaf Levels gescannt werden müssen, aber irgendwie hätten wir nicht erwartet, dass dies derart lange dauern würde. In unserem Fall benötigt die Funktion schon mal locker ~30 Minuten, um überhaupt ein Ergebnis zu liefern. Das ist fast die Hälfte unseres gesamten täglichen Wartungsfensters, die dafür draufgeht, die Fragmentierung einer einzigen grossen Tabelle zu ermitteln. Für die anderen wesentlich kleineren Tabellen beträgt die gesamte Laufzeit nur ca. 3 - 4 Minuten, was akzeptabel ist. Nur bevor jemand aufmerkt: Ja, wir verwenden den LIMITED Modus bereits...

Nachdem wir das erst einmal herausgefunden hatten, war der nächste naheliegende Schritt, zu überlegen, ob und wie man sys.dm_db_index_physical_stats "schneller" machen kann. Eine der Ueberlegungen die dabei aufkam, war folgende: Wir müssen die grosse Tabelle aus sys.dm_db_index_physical_stats irgendwie auszuschliessen!

Um das Ganze flexibel zu halten, wollten wir eine separate Tabelle verwenden, in der wir die object_id und die index_id zusammen mit einem datetime Wert der letzten Wartung speichern und dann über ein WHERE NOT EXISTS, die grosse Tabelle aus sys.dm_db_index_physical_stats auszuschliessen und separat zu handhaben. Aber auch das war überraschenderweise nicht wirklich von Erfolg gekrönt. Die Laufzeit wurde nicht kürzer wie erwartet. Auf der Suche nach einer Begründung bin ich auf diesen Blog Eintrag von Paul Randal gestossen: Inside sys.dm_db_index_physical_stats. Wie man dort nachlesen kann unterstützt diese DMF kein Predicate Pushdown, was in unserem Fall bedeute, dass unabhängig von unserer smarten WHERE NOT EXISTS Klausel, die Fragmentierung erst ermittelt wird und danach die WHERE Klausel angewendet wird. Eine noch eindeutigere Erklärung für dieses Verhalten findet sich hier:

Most dynamic management views support “predicate push-down,” where the only data processed is that which matches the predicate in the WHERE clause. However, sys.dm_db_index_physical_stats is a function, not a view, so it can’t do this. This means you have to manually filter and only ask the function to process those indexes you know have the potential to be fragmented and may require rebuilding or reorganizing.

Damit war dieses Konstrukt auch wieder hinfällig.

Dann hatten wir diese Idee:

SELECT
    OBJECT_SCHEMA_NAME(FRAG.[object_id]) + '.' + OBJECT_NAME(FRAG.[object_id]),
    SIX.[name],
    FRAG.avg_fragmentation_in_percent,
    FRAG.page_count
FROM
    sys.indexes SIX
    CROSS APPLY
    sys.dm_db_index_physical_stats
    (
        DB_ID(),        --use the currently connected database
        SIX.object_id,  --Parameter for object_id.
        SIX.index_id,   --Parameter for index_id.
        0,              --Parameter for partition_number.
        DEFAULT         --Scanning mode. Default to "LIMITED", which is good enough
    ) FRAG
WHERE
    SIX.object_id <> OBJECT_ID('...grosse Tabelle...')
ORDER BY
    FRAG.avg_fragmentation_in_percent DESC;

Der Hintergedanke hier war, über die sys.indexes in der WHERE Klausel zu filtern und die grosse Tabelle damit auszuschliessen und gleichzeitig die DMF mit den Parametern object_id und index_id aus sys.indexes aufzurufen. Die Idee war leider nur bis zum ersten Test genial:

Msg 413, Level 16, State 1, Line 1
Correlated parameters or sub-queries are not supported by the inline function "sys.dm_db_index_physical_stats".
Msg 413, Level 16, State 1, Line 1
Correlated parameters or sub-queries are not supported by the inline function "sys.dm_db_index_physical_stats".

Um diesen Fehler bei der Verwendung von CROSS APPLY kann man den hier vorgeschlagenen Workaround einsetzen: Indexes From Every Angle: Using CROSS APPLY with sys.dm_db_index_physical_stats. Nicht gelöst wird dadurch jedoch das Problem des Predicate Pushdowns.

Wir haben uns jetzt für einen anderen (Hybrid-) Ansatz entschieden, bei dem in einer Tabelle für alle Indizes der Datenbank object_id und index_id zusammen mit page_count und einem Zeitstempel der letzten Wartung gespeichert wird.

Für Indizes mit weniger als 1.000.000 Seiten wird die Funktion sys.dm_db_index_physical_stats mit der object_id und index_id aus der neuen Tabelle als Parameter aufgerufen und dann abhängig vom Grad der Fragmentierung der Index neu erstellt oder nicht. Sind wir noch nicht am Ende unseres Zeitfenster wird eine weitere Zeile aus unserer Tabelle abgerufen, die Fragmentierung ermittelt, und ggfs. neu erstellt, ansonsten endet die Prozedur.

Für Indizes mit mehr als 1.000.000 Seiten wird an jedem Maintenance Day genau ein Index neu erstellt und zwar der, dessen Pflege schon am längsten zurückliegt. Bei diesen Indizes stellen wir vorher nicht den Grad der Fragmentierung fest, aber z. Zt. beträgt der Zyklus 3 Wochen für einen Index, bis er wieder an der Reihe ist. Wir haben diese Indizes über mehrere Wochen beobachtet und können berechtigterweise davon ausgehen, dass sich "genügend" Fragmentierung innerhalb dieser 3 Wochen angesammelt hat, um die Neuerstellung zu rechtfertigen.

Wir haben uns auch dafür entschieden, Indizes stets neu zu erstellen statt sie zu reorganisieren. Das mag zwar manchmal etwas "zu viel" sein, aber wir haben uns wegen der Möglichkeit des Parallelismus dafür entschieden. Um uns aber dadurch keine neuen Probleme zu schaffen, ermitteln wir vorher die Anzahl der Prozessoren und verwenden nur die Hälfte davon im ALTER INDEX Statement. Damit sind immer noch genügend Resourcen für andere Prozesse und Operationen währen unserer Index Maintenance vorhanden.

Hier noch einige weiterführende Links zum Thema:

http://social.msdn.microsoft.com/forums/en-US/transactsql/thread/e53f959d-ddb0-44dc-981c-bcf8131dc8ed

http://www.sqlmag.com/blogs/sql-server-questions-answered/sql-server-questions-answered/tabid/1977/entryid/12869/Default.aspx

Mich würde interessieren, wie andere ihre Indizes pflegen und würde mich über entsprechende Kommentare freuen.

Change History:
  • 18.02.2011: Link zum Workaround bei Verwendung von CROSS APPLY ergänzt.

Dieser Eintrag wurde eingetragen von und ist abgelegt unter SQL Server. Tags: , , ,

4 Kommentare

Benutzerwertungen
5 Stern:
 
(2)
4 Stern:
 
(0)
3 Stern:
 
(0)
2 Stern:
 
(0)
1 Stern:
 
(0)
2 Bewertungen
Durschn. Benutzerwertung:
(5.0)
Dirk Hondong
Hallo Frank, für die Index-Wartung verwende ich das Index Defag Script (http://sqlfool.com/2010/04/index-defrag-script-v4-0/) von Michelle Ufford. Dies ist für meine Zwecke mehr als praktikabel, da man mit Hilfe der zu übergebenden Parameter sehr gut steuern kann, wann welche Indizes angepackt werden sollen. Bisher hab ich aber nur Erfahrungen in Test-Systemen damit sammeln können, da für die produktiven Systeme noch die Wartungsfenster definiert werden müssen. Ich verfolge aber auch einen hybriden Ansatz (das Script bietet die Option). Beim ersten Lauf wird ermittelt, welche Indizes überhaupt gewartet werden müssen und in eine Tabelle festgehalten. Danach wird die Tabelle abgearbeitet. Dabei gebe ich der Prozedur noch eine Laufzeit von x Minuten mit. Über dieses Limit hinaus wird dann kein neuer Index mehr angepackt, bis es zum nächsten Aufruf kommt. Dann wird erst die Tabelle weiter abgearbeitet, bis eine erneute Ermittlung der Fragemtierung erfolgt. Gruß Dirk
17.02.11 @ 10:55
Hallo Dirk, danke fürs Feedback! Ich kenne das Skript von Michelle und auch das von Ola Hallengren und habe vorher auch überlegt, ob ich nicht eines davon verwenden soll anstatt das Rad neu zu erfinden. Das "Problem" mit diesen Skripten jedoch ist, dass sie "general-purpose" sind und versuchen alle Eventualitäten abzudecken und damit auch Sachen, von denen ich weiss, dass wir sie sehr wahrscheinlich nicht in unseren Datenbanken haben werden (zum Beispiel LOB Spalten und/oder XML Indexes). Anfangs habe ich dann versucht, diese Spezialitäten aus den Skripten rauszunehmen, habe dann aber festgestellt, dass ich dafür länger brauche als was eigenes zu schreiben. Ausserdem brauche ich das nur für 3 Datenbanken auf einem Server. Unsere Produktions-DBAs haben ihre eigenen Skripts, die sie auf allen ihren Servern laufen lassen. Dein Ablauf klingt so wie unser war, bevor wir festgestellt haben, dass das Feststellen der Fragmentierung bei grösseren Tabellen richtig lange dauern kann. Abhängig von der Grösse Deiner Indexes würde ich das im Auge behalten, ob das mit dem Wartungsfenster so hinkommt oder wie uns, aus dem Ruder läuft. :-)
17.02.11 @ 13:19
Holger Schmeling
Hallo Frank, ich bin ganz Deiner Meinung, was den mehr oder weniger bedenkenlosen Einsatz von fertigen Skripten angeht. Immerhin ist das ja auch Software und damit besteht eine gewisse Wahrscheinlichkeit für enthaltene Fehler :) Generell bin ich immer skeptisch, wenn man fertige Lösungen einsetzt ohne groß darüber nachzudenken, was da wirklich passiert. Dennoch ist vor allem das Skript von OH irgendwie allgegenwärtig, und ich denke auch, dass es gut ist - eben nur nicht in jedem Fall, womit wir wieder dabei wären, dass man überlegen muss, ob. Aus meiner Erfahrung gibt es noch einen leidigen Punkt - und wahrscheinlich eine schlechte Nachricht für Dich. Ich hatte nun bereits mehrfach den Fall, dass ein Index nicht auf der Blatt-Ebene, sondern auf der darüberliegenden Ebene fragmentiert war. Die Auswirkung waren nicht ausreichend aufgefüllte Seiten in einer Nicht-Blatt-Ebene, und das hat letztlich die Tiefe des Indexbaumes um eins erhöht - sagen wir von drei auf vier. Jeder Index-Seek dauert also 25% länger. Hast Du neben dem Wert avg_fargmentation_percent einmal den Wert von avg_space_user_in_percent beobachtet? Leider bekommst Du diesen Wert nicht mit der Option 'LIMITED' heraus - da musst Du zumindest 'SAMPLED' verwenden. Viele Grüße, Holger
22.02.11 @ 08:32
Hallo Holger, danke für das Feedback! Ja, wir haben die anderen Werte ebenfalls beobachtet. Ich hätte vielleicht im Beitrag erwähnen, dass wir sämtliche Scanning Mode vorher ausprobiert und die Ergebnisse verglichen haben. Dabei war der Wert für avg_page_space_used_in_percent stets deutlich über 90.
22.02.11 @ 15:54


Formular wird geladen...