Die Ausgangslage ist fast immer dieselbe: Es wurde einmalig richtig viel Platz frei. Ein Archivlauf, eine gelöschte Protokolltabelle oder wie in unserem Beitrag zur xRM-Dateiablage ein Dokumentbestand, der aus der Datenbank in eine Dateiablage gewandert ist. Danach zeigt der SQL Server mehrere Gigabyte freien Platz in der Datei, aber die MDF auf der Platte ist unverändert groß. Also DBCC SHRINKFILE, und dann passiert eines von drei Dingen: Es tut sich gar nichts, es wird nur ein Bruchteil frei, oder der Befehl läuft stundenlang und blockiert dabei die halbe Anwendung.
Dieser Beitrag sammelt die Gründe dafür, jeweils mit dem, was Du dagegen tun kannst. Die Belege stammen aus der Dokumentation von Microsoft, nicht aus Forenwissen.
Vorweg, weil es die wichtigste Aussage ist: Ein Shrink ist keine Wartungsaufgabe. Microsoft schreibt in der Doku zu DBCC SHRINKFILE ausdrücklich, dass Dateien, die durch den normalen Geschäftsbetrieb wachsen, keinen Shrink brauchen. Wer regelmäßig schrumpft und dann zusieht, wie die Datei wieder wächst, verbrennt nur Rechenzeit und zerlegt sich die Indizes. Die Doku wird an dieser Stelle deutlich: Wächst die Datei nach dem Verkleinern wieder auf dieselbe Größe, war der Platz für den normalen Betrieb nötig, und jedes dieser Wachstumsereignisse kostet zusätzlich Zeit und E/A-Last. Ein Shrink, der sich alle paar Wochen wiederholt, macht die Datenbank also langsamer statt kleiner. AUTO_SHRINK gehört aus demselben Grund auf OFF. Sinnvoll ist ein Shrink nach einer einmaligen, großen Löschaktion, und danach nicht mehr.
Schritt null: Nachsehen, wie viel überhaupt frei ist
Bevor Du über Ursachen nachdenkst, klär die Zahlen. Die folgende Abfrage steht so in der Microsoft-Doku und zeigt Dir pro Datei den tatsächlich freien Platz:
SELECT name,
size / 128.0 AS GroesseMB,
CAST(FILEPROPERTY(name, 'SpaceUsed') AS int) / 128.0 AS BelegtMB,
size / 128.0 - CAST(FILEPROPERTY(name, 'SpaceUsed') AS int) / 128.0 AS FreiMB
FROM sys.database_files;Genauso aufschlussreich ist das Ergebnis, das DBCC SHRINKFILE selbst ausgibt. Die Spalte MinimumSize nennt die Untergrenze der Datei, EstimatedPages die Größe, auf die der SQL Server die Datei realistisch schrumpfen zu können glaubt. Liegt EstimatedPages nahe an CurrentSize, brauchst Du über das Zielmaß nicht weiter zu diskutieren. Alle Werte sind 8-KB-Seiten, geteilt durch 128 ergibt Megabyte.
Grund 1: Gelöscht heißt nicht freigegeben
Wenn Du Zeilen löschst, verschwinden sie nicht sofort von der Seite. Der SQL Server markiert sie nur, das sind die sogenannten Ghost Records. Der Ghost Cleanup räumt sie später im Hintergrund weg, und erst danach ist der Platz für den Shrink überhaupt verwertbar.
Zwei Details aus der Doku erklären, warum das dauern kann. Erstens gibt es pro Instanz genau einen Ghost-Cleanup-Thread für alle Datenbanken. Zweitens überspringt der Prozess eine Datenbank, wenn er keine gemeinsame Sperre darauf bekommt, und versucht es erst im nächsten Durchlauf wieder. Nach dem Löschen von Millionen Zeilen ist die Aufräumarbeit also nichts, was in einer Minute erledigt ist.
Wie viel noch offen ist, zeigt Dir:
SELECT DB_NAME(database_id) AS Datenbank,
SUM(ghost_record_count) AS Ghosts
FROM sys.dm_db_index_physical_stats(NULL, NULL, NULL, NULL, 'SAMPLED')
GROUP BY database_id
ORDER BY Ghosts DESC;Was hilft: Abwarten und die Zahl beobachten. Wer nicht warten will, baut die Indizes der betroffenen Tabelle neu auf. Ein Rebuild erzeugt die Seiten neu und lässt die Ghosts dabei weg, das steht so in der Doku. Ein Rebuild braucht allerdings Platz in der Datei, insofern ist die Reihenfolge Rebuild zuerst und Shrink danach die richtige.
Grund 2: Die Datei kann nicht unter ihre Mindestgröße
Eine Datei lässt sich nicht kleiner machen als ihre Mindestgröße. Das ist entweder die Größe bei der Erstellung oder die letzte Größe, die per ALTER DATABASE oder einem früheren Shrink ausdrücklich gesetzt wurde. Microsoft nennt das Beispiel einer Datenbank, die mit 10 MB angelegt wurde und auf 100 MB gewachsen ist: Kleiner als 10 MB wird sie nicht, auch wenn alle Daten gelöscht sind.
Praktisch relevant wird das andersherum: Wurde die Datei irgendwann bewusst auf 80 GB gesetzt, ist das die Untergrenze, bis ein Shrink sie neu festlegt. Der Wert steht als MinimumSize im Ergebnis des Shrink-Befehls.
Grund 3: TRUNCATEONLY schneidet nur ab
Hier liegt das häufigste Missverständnis. Die drei Varianten machen sehr unterschiedliche Dinge. Eines vorweg zur Schreibweise: Die Zielgröße wird in Megabyte angegeben und meint die Größe, die die Datei danach haben soll, nicht den Betrag, um den sie kleiner wird. 20000 heißt also „verkleinere die Datei auf rund 20 GB". Lässt Du die Zahl weg oder setzt sie auf 0, geht der SQL Server auf die Erstellungsgröße der Datei zurück.
| Aufruf | Was passiert |
|---|---|
DBCC SHRINKFILE (datei, 20000) | Verschiebt belegte Seiten vom Ende der Datei in freie Bereiche am Anfang und gibt den Platz danach an das Betriebssystem zurück. Das ist der teure, aber wirksame Weg. |
DBCC SHRINKFILE (datei, TRUNCATEONLY) | Gibt nur den freien Platz am Ende der Datei zurück und verschiebt nichts. Die Datei schrumpft exakt bis zum letzten belegten Block, also der Einheit aus acht zusammenhängenden Seiten. Liegt dort eine einzige belegte Seite, passiert praktisch nichts. |
DBCC SHRINKFILE (datei, NOTRUNCATE) | Verschiebt die Seiten nach vorne, gibt den Platz aber nicht zurück. Die Datei sieht hinterher unverändert aus, laut Doku „scheint sie nicht zu schrumpfen". Sinnvoll nur als vorbereitender Schritt. |
Wenn also TRUNCATEONLY nichts bringt, ist das kein Fehler, sondern die dokumentierte Funktionsweise. Und noch ein Detail, das in der Doku ausdrücklich steht: Gibst Du zusätzlich eine Zielgröße zusammen mit TRUNCATEONLY an, wird der freie Platz am Ende unter Umständen gar nicht freigegeben. Entweder Zielgröße oder TRUNCATEONLY, nicht beides.
Grund 4: LOB-Daten machen den Shrink zäh
Genau der Fall, der nach einer Dokumentmigration auftritt. Inhalte aus varbinary(max), nvarchar(max) oder xml liegen nicht in der Datenzeile, sondern in eigenen LOB-Seiten, die über Verweisketten hängen. Ein Shrink muss diese Seiten einzeln verschieben, und das ist der langsamste Teil der Übung. Für komprimierte Columnstore-Segmente dokumentiert Microsoft sogar, dass LOB-Seiten vor SQL Server 2025 überhaupt nicht verschoben werden können.
Was hilft: Die LOB-Daten vorher verdichten, statt sie vom Shrink einsammeln zu lassen. Dafür gibt es einen dokumentierten Weg, nämlich ALTER INDEX REORGANIZE mit LOB_COMPACTION:
ALTER INDEX PK_BCSPjmDokumente ON dbo.BCSPjmDokumente
REORGANIZE WITH (LOB_COMPACTION = ON);Laut Doku verdichtet die Option alle Seiten mit LOB-Daten und kann so die Datenmenge auf der Platte reduzieren. Bei einem gruppierten Index betrifft das alle LOB-Spalten der Tabelle. Der Standardwert ist ohnehin ON, die ausdrückliche Angabe dient nur der Klarheit. Wichtig ist die Einschränkung am anderen Ende: Bei einem Heap, also einer Tabelle ohne gruppierten Index, läuft die LOB-Kompaktierung ins Leere. Tabellen, aus denen Du große Mengen LOB-Daten gelöscht hast, sollten deshalb einen gruppierten Index haben.
Grund 5: Der Shrink wird blockiert
Drei Blockaden sind dokumentiert und begegnen einem regelmäßig.
Snapshot-Transaktionen. Läuft parallel eine Transaktion unter einer zeilenversionsbasierten Isolationsstufe, wartet der Shrink auf deren Ende. Der SQL Server schreibt dazu die Meldung 5203 ins Fehlerprotokoll, in der ersten Stunde alle fünf Minuten, danach stündlich. Welche Transaktion es ist, zeigt sys.dm_tran_active_snapshot_database_transactions. Du hast dann drei Möglichkeiten: Die blockierende Transaktion beenden, den Shrink beenden oder warten.
Sperren auf IAM-Seiten. Der Shrink braucht beim Verschieben oder Löschen von Index Allocation Maps eine Schemaänderungssperre, während normale Abfragen darauf eine Schemastabilitätssperre halten. Das Ergebnis beschreibt Microsoft deutlich: Lang laufende Abfragen blockieren den Shrink, und neue Abfragen stellen sich hinter dem Shrink an. In einer aktiven Sage-Umgebung reicht dafür eine einzige große Auswertung.
Seit SQL Server 2022 gibt es dafür einen Schalter. Er lässt den Shrink im Zweifel abbrechen, statt die Anwender warten zu lassen:
DBCC SHRINKFILE (datei, 20000) WITH WAIT_AT_LOW_PRIORITY (ABORT_AFTER_WAIT = SELF);Bekommt der Shrink die Sperre binnen einer Minute nicht, bricht er mit Fehler 49516 ab. Die Alternative ABORT_AFTER_WAIT = BLOCKERS beendet stattdessen die blockierenden Transaktionen der Anwender. Das willst Du in einer produktiven Warenwirtschaft eher nicht.
Eine laufende Sicherung. Kurz und bündig aus der Doku: Du kannst eine Datenbank nicht schrumpfen, während sie gesichert wird, und umgekehrt nicht sichern, während ein Shrink läuft. Wer den Shrink abends startet und um 22 Uhr die Vollsicherung anspringen lässt, hat die Ursache damit schon gefunden. Auch Dateien hinzufügen oder entfernen geht während einer Sicherung nicht.
Ein Sonderfall noch: Eine schreibgeschützte Datenbank lässt sich nicht schrumpfen. Das steht bei den Dateigruppenoptionen von ALTER DATABASE.
Grund 6: Beim Protokoll gelten eigene Regeln
Wenn die LDF nicht kleiner wird, ist die Ursache fast nie der Shrink selbst. Verkürzen und Schrumpfen sind zwei verschiedene Dinge. Das Abschneiden gibt Platz innerhalb der Datei frei, die physische Datei wird dadurch nicht kleiner. Schrumpfen kann nur, was vorher abgeschnitten wurde, und laut Doku nur, solange mindestens eine virtuelle Protokolldatei frei ist.
Warum das Abschneiden ausbleibt, verrät eine einzige Abfrage. Microsoft führt die möglichen Werte unter Faktoren, die das Abschneiden des Protokolls verzögern können auf:
SELECT name, recovery_model_desc, log_reuse_wait_desc
FROM sys.databases
WHERE name = 'MeineDatenbank';Die häufigsten Werte und was sie bedeuten:
| log_reuse_wait_desc | Bedeutung |
|---|---|
LOG_BACKUP | Es fehlt eine Protokollsicherung. Der Dauerbrenner bei Datenbanken im vollständigen Wiederherstellungsmodell, bei denen nur die Vollsicherung eingerichtet wurde. Entweder Du sicherst das Protokoll regelmäßig, oder das Wiederherstellungsmodell passt nicht zu dem, was Du tatsächlich brauchst. |
ACTIVE_TRANSACTION | Eine Transaktion ist offen. Das verhindert das Abschneiden in jedem Wiederherstellungsmodell, auch im einfachen. Dazu zählen auch zurückgestellte Transaktionen, deren Rollback an einer fehlenden Ressource hängt. |
ACTIVE_BACKUP_OR_RESTORE | Eine Sicherung oder Wiederherstellung läuft gerade. |
REPLICATION | Transaktionen sind noch nicht an die Verteilungsdatenbank übergeben. Relevant überall dort, wo Replikation im Spiel ist. |
AVAILABILITY_REPLICA | Ein sekundäres Replikat einer Verfügbarkeitsgruppe hängt hinterher. |
CHECKPOINT und OLDEST_PAGE | Routinefälle rund um Prüfpunkte. Meist kurzlebig, bei indirekten Prüfpunkten kann die älteste Seite das Abschneiden verzögern. |
Und selbst wenn alles stimmt, schrumpft das Protokoll nur bis zur Grenze einer virtuellen Protokolldatei. Kleiner als eine solche Einheit geht nicht, und liegt der aktive Teil des Protokolls hinten in der Datei, gibt der SQL Server frei, was geht, und sagt Dir in einer Meldung, was zu tun ist. Nach der nächsten Sicherung klappt es dann. Zwei Läufe sind hier normal, kein Fehler.
Grund 7: EMPTYFILE auf der falschen Datei
Wer eine Datei ganz loswerden will, nutzt DBCC SHRINKFILE (datei, EMPTYFILE). Die Option verschiebt die Daten in die anderen Dateien derselben Dateigruppe und markiert die Datei so, dass nichts Neues mehr hineingeschrieben wird. Anschließend lässt sie sich per ALTER DATABASE ... REMOVE FILE entfernen, und zwar laut Doku nur dann, wenn sie leer ist.
Zwei Stolperfallen dabei:
- Gibt es in der Dateigruppe keine zweite Datei mit genug Platz, hat der Inhalt kein Ziel und der Lauf scheitert. In der Praxis quittiert der SQL Server das mit der Meldung 2555, dass sich der Inhalt der Datei nicht an einen anderen Ort verschieben lässt. Auf der primären Datendatei scheitert es ohnehin, dort liegen Systeminformationen der Datenbank.
- Brichst Du einen
EMPTYFILE-Lauf ab, bleibt die Datei nicht gesperrt. Die Doku weist ausdrücklich darauf hin, dass sie in dem Fall nicht markiert bleibt und wieder Daten aufnehmen kann. Ein abgebrochener Lauf ist also kein Halbzustand, auf den Du Dich verlassen kannst.
Der Shrink läuft ewig: Fortschritt sichtbar machen
Ein Shrink über eine große Datei dauert Stunden, und das ist normal. Blind warten musst Du trotzdem nicht, denn der Fortschritt wird in sys.dm_exec_requests gemeldet. Die Spalte percent_complete ist laut Doku unter anderem für DBCC SHRINKFILE und DBCC SHRINKDATABASE gefüllt:
SELECT session_id, command, percent_complete,
total_elapsed_time / 1000 AS SekundenGesamt,
wait_type, blocking_session_id
FROM sys.dm_exec_requests
WHERE command LIKE 'DBCC%';Gut zu wissen: Du darfst jederzeit abbrechen. Die bereits geleistete Arbeit bleibt erhalten, das garantiert die Doku für Shrink-Operationen ausdrücklich. Ein Shrink in mehreren Etappen über mehrere Wartungsfenster ist deshalb ein völlig legitimes Vorgehen. In kleinen Schritten vorzugehen, also von 100 GB auf 90, dann auf 80, ist bei großen Dateien ohnehin angenehmer als ein einziger großer Sprung, weil Du nach jeder Etappe entscheiden kannst.
Für wirklich große Datenbanken empfiehlt Microsoft in den Best Practices inzwischen das ShrinkDriver-Skript aus den offiziellen Beispielen. Es macht aus dem Shrink einen einzigen beobachtbaren Vorgang, der mehrere Dateien parallel bearbeitet und nach einer Unterbrechung wieder aufsetzt.
Die Alternative, wenn gar nichts geht
Manchmal ist der sauberere Weg, gar nicht erst zu schrumpfen, sondern umzuziehen. Du legst eine neue Dateigruppe mit einer neuen Datei an, verschiebst die großen Tabellen dorthin, indem Du deren gruppierten Index mit CREATE INDEX ... WITH (DROP_EXISTING = ON) ON [NeueDateigruppe] neu erstellst, und leerst die alte Datei anschließend mit EMPTYFILE. Das Ergebnis ist eine kompakte, frisch aufgebaute Struktur statt eines fragmentierten Rests.
Der Preis ist hoch: Du brauchst zwischenzeitlich Platz für beide Stände, die Aktion gehört in ein Wartungsfenster, und bei einer Herstellerdatenbank wie der Sage 100 solltest Du vorher klären, ob eine eigene Dateigruppe unterstützt wird. Für Datenbanken jenseits weniger hundert Gigabyte ist es trotzdem oft der planbarere Weg.
Nach dem Shrink: Die Indizes sind hinüber
Das ist keine Nebenwirkung, sondern eine Garantie. Microsoft formuliert es in der Anleitung zum Verkleinern einer Datenbank so: Die verschobenen Daten können an jeder beliebigen freien Stelle in der Datei landen, das erzeugt Indexfragmentierung und bremst Abfragen, die größere Bereiche lesen. Ein Shrink erhält den Fragmentierungsgrad nicht, er verschlechtert ihn.
Nach dem Shrink gehört deshalb eine Indexpflege dazu. Und weil ein Rebuild Platz in der Datei braucht, wächst die Datei dabei womöglich wieder ein Stück. Genau davor warnt die Doku auch. Rechne also nicht mit dem letzten Gigabyte, sondern lass bewusst Luft. Wer nach dem Shrink sofort wieder schrumpft, weil der Rebuild die Datei vergrößert hat, dreht sich im Kreis.
Das Vorgehen in der richtigen Reihenfolge
- Freien Platz messen und ein realistisches Ziel festlegen, mit bewusster Reserve statt auf Kante.
- Ghost Records prüfen und der Aufräumarbeit Zeit geben.
- Bei großen Mengen LOB-Daten die betroffenen Indizes mit
LOB_COMPACTIONreorganisieren. - Beim Protokoll zuerst
log_reuse_wait_descklären, nicht schrumpfen. - Den Shrink in ein Fenster legen, in dem keine Sicherung und keine Indexpflege läuft, und in Etappen arbeiten.
- Ab SQL Server 2022 mit
WAIT_AT_LOW_PRIORITYarbeiten, damit der Shrink nachgibt und nicht die Anwender. - Danach die Indizes neu aufbauen und die Dateigröße einmal bewusst setzen, statt sie wieder wachsen zu lassen.
Fazit
Ein Shrink, der nichts bringt, ist selten ein Defekt. In den meisten Fällen ist der Platz noch nicht wirklich frei, liegt hinten in der Datei fest, hängt in LOB-Ketten oder wird von einer Sperre blockiert. Die Diagnose besteht aus drei Abfragen: freier Platz je Datei, ghost_record_count und log_reuse_wait_desc. Zusammen mit dem Ergebnis des Shrink-Befehls selbst erklären sie fast jeden Fall.
Genauso wichtig ist die Erwartungshaltung. Ein Shrink ist eine einmalige Aufräumaktion nach einer einmaligen Löschaktion, er kostet Laufzeit, erzeugt Fragmentierung und braucht danach eine Indexpflege. Wenn Du das einplanst, ist es eine saubere Sache. Wenn Du es nebenbei im laufenden Betrieb versuchst, wird es eine lange Nacht.
Wie es überhaupt so weit kommt, dass mehrere Gigabyte Dokumente in einer Sage-Datenbank liegen, steht in unserem Beitrag zur xRM-Dateiablage. Wenn Du bei einer konkreten Datenbank nicht weiterkommst, schau Dir auch unseren Sage 100 Health-Check an oder melde Dich direkt bei uns.