Wissen

Praxis-Artikel und Buchkapitel zu SQL-Performance, Sicherheit und Hochverfügbarkeit – alle frei verfügbar.

Beratung

Festpreis-Analyse mit Bericht und Handlungsempfehlung – oder strategische Begleitung bei Architektur, Migration und Hochverfügbarkeit.

Fachbücher

Die fünfbändige Reihe „Ulis SQL-Bibliothek“ – Band 1 verfügbar. Leseprobe herunterladen!

Tools

UB.SimSQL: SQL-Server-Lastsimulator mit regelbasierten Konfigurationsempfehlungen. Lokal, ohne Cloud, ohne Abo.

Schulungen

Online-Workshops zu Performance, Sicherheit und Entwicklung – kompakt, hands-on, ohne MOC-Folienschlacht.

Fallstudie: Sparfuchs & Partner Steuerberatungs GmbH: – SQL Server Performance

von

Dieser Artikel ist ein Kapitel aus:
SQL Server Performance & Troubleshooting
Praxisleitfaden, ca. 600 Seiten

[ Hier bei Amazon bestellen ]
[ Mehr zum Buch ]

Table of Contents
2
3

Fallstudie: Sparfuchs & Partner Steuerberatungs GmbH:

Die vollständige Horror-Show — oder: Wie man in drei Jahren alles falsch machen kann

Ankunft beim Tatort BUCHSQL01

Es gibt Analysen, bei denen man nach dem ersten Blick auf die Messwerte kurz inne hält. Tippt nochmal. Startet das Script neu. Wartet auf einen anderen Output. Ruft einen Kollegen rein. "Schau mal hier." — "Das ist nicht dein Ernst." — "Doch."

Die Sparfuchs & Partner Steuerberatungs GmbH mit ihren 47 gleichzeitigen Benutzern ist so ein Fall. Nicht, weil irgendjemand aktiv sabotiert hätte — niemand hat morgens aufgestanden und gedacht: "Heute konfiguriere ich SQL Server so schlecht wie irgend möglich." Die Situation ist das Ergebnis von drei Jahren akkumulierter Fehlentscheidungen, nie angepasster Standardwerte und des klassischen "läuft doch" — bis es nicht mehr läuft.

Diese Fallstudie ist das Gegenstück zu Kapitel 32 (Musterwerk GmbH). Dort war alles ordentlich, aber mit Luft nach oben. Hier ist strukturell fast alles falsch — Hardware, Konfiguration, Wartung, Applikation. Das Lehrreiche: Die Befunde sind keine akademischen Konstrukte. Jedes einzelne Anti-Pattern, das in den Teilen I bis IV dieses Buchs beschrieben wird, taucht hier in seiner realen Form auf. Das macht BUCHSQL01 zum besten Lernbeispiel des Buchs — wenn auch zum schmerzhaftesten.

Die Analyse wurde mit Collect-SqlPerf.ps1 (Kapitel 31) durchgeführt. Messzeitraum: ein normaler Werktagsmorgen, 08:30 bis 10:30 Uhr — kein Jahresabschluss, kein Sonderlauf. Das hier ist der Regelbetrieb. 1.440 Messpunkte im 5-Sekunden-Intervall. Was dabei ans Licht kam, übersteigt selbst erfahrene Erwartungen.

 

Praxisbeispiel: Ausgangslage BUCHSQL01

Instanz: BUCHSQL01 | SQL Server 2019 Standard, Windows Server 2019

Hardware: 1 VM, 4 vCPUs, 8 GB RAM — Bare Metal, kein VMware-Monitoring

Storage: EINE physische Festplatte (7.200 rpm HDD) für alles: OS, TempDB, Daten, Log, Backup

Netzwerk: 100 MBit/s (im Jahr 2024!)

Datenbank BuchwerkProd: 24 GB Daten, 183 GB Transaktionslog

Kompatibilitätslevel: 110 (entspricht SQL Server 2012 — auf SQL Server 2019!)

Anlass: Benutzer berichten, der Server sei "generell langsam seit Monaten"

Freier Speicher auf C: 1,8 GB von 200 GB — TempDB liegt auf C:

 

Management Summary: Zehn Befunde, ein Fazit

Bevor wir in die Tiefe gehen: Hier ist das Gesamtbild in kompakter Form. Dieser Server hat nicht ein Problem — er hat zehn, und sie verstärken sich gegenseitig. Die folgende Tabelle zeigt die gravierendsten Befunde sortiert nach Schwere.

 

#

Befund

Status

Sofortgefahr

1

TempDB liegt auf C: (OS-Volume) — 1,8 GB frei

KRITISCH

Nächster Jahresabschluss füllt C: → Totalausfall

2

Transaktionslog 183 GB, Datenbank 24 GB

KRITISCH

E: voll in ca. 6 Wochen, Log nie gesichert

3

max server memory = 8.192 MB bei 8 GB RAM

KATASTROPHAL

OS paged permanent, Pages/sec: 41.847/s

4

Autogrowth 1 MB ohne IFI

KATASTROPHAL

847 Events in 120 Min, 39% der Zeit eingefroren

5

MAXDOP=0 + Cost Threshold=5

SCHLECHT

Alle 4 Kerne für jeden trivialen Query

6

Priority Boost = 1 (deprecated seit 2008!)

GEFÄHRLICH

OS-Prozesse verhungern, Instabilität möglich

7

Alle Volumes auf einer physischen HDD

KATASTROPHAL

IO-Latenz p95 Write: 312 ms

8

Scalar UDFs in WHERE-Klauseln, 360.000 Aufrufe/Query

SCHLECHT

Query läuft 8–14 Minuten statt < 3 Sekunden

9

Index-Fragmentierung 97% — 3 Jahre kein Rebuild

SCHLECHT

Logical Reads 41× höher als nötig

10

SELECT * in 23 von 31 Stored Procedures

SCHLECHT

42× mehr IO und Netzwerklast als nötig

Tab. 33.1: Die zehn gravierendsten Befunde auf BUCHSQL01, sortiert nach Schwere

 

Die Prognose ohne Maßnahmen ist eindeutig: Beim nächsten größeren Buchungslauf — Quartalsmeldungen, Jahresabschluss — wird C: vollgefüllt. SQL Server erzeugt Error 1105 (Cannot allocate space), TempDB-Operationen scheitern, alle laufenden Transaktionen werden zurückgerollt. Datenverlust des laufenden Tages ist nicht ausgeschlossen.

Aber jetzt der Reihe nach. Jeder Befund verdient seine eigene Erklärung — denn nur, wenn man versteht, warum etwas so schief läuft, kann man es dauerhaft reparieren.

Systemkontext: Was hier läuft und womit

Hardware-Profil — unterdimensioniert für die Workload

Die Kanzlei betreibt eine Finanzbuchhaltungs- und Mandantenverwaltungslösung für 47 gleichzeitige Benutzer. Die Hardware liest sich wie eine Entwicklerworkstation:

 

Komponente

Ist-Zustand

Mindestempfehlung für diese Workload

vCPUs

4 (keine Hyper-Threading-Reservierung)

16 vCPUs

RAM

8 GB

64 GB (Datenbank wächst auf >100 GB)

Storage

1 physische HDD für alles (C: + D: + E: = selbe Spindel)

Min. 3 separate Volumes auf NVMe oder SSD

Netzwerk

100 MBit/s-Adapter

1 GBit/s

Backupsystem

Robocopy-Script auf USB-Festplatte, läuft manuell

Automatisiert mit Log-Backup-Job

Tab. 33.2: Hardware-Profil BUCHSQL01 im Vergleich zur Mindestempfehlung

 

4 vCPUs für 47 gleichzeitige Benutzer wären mit einer sauberen Konfiguration noch halbwegs vertretbar — sofern die Abfragen effizient sind. Sie sind es nicht. In Kombination mit MAXDOP=0 ergibt sich ein Parallelismus-Desaster, das wir gleich besprechen.

Volume-Belegung — eine Zeitbombe mit Countdown

Die Volume-Aufteilung verdient besondere Aufmerksamkeit. Nicht, weil es drei Laufwerksbuchstaben gibt — das klingt nach Ordnung. Sondern, weil alle drei auf derselben physischen Spindel liegen:

 

Vol.

Inhalt

Gesamt

Belegt

Frei

Lage

C:

OS + TempDB (!) + SQL-Binaries + Backups (!)

200 GB

198,2 GB

1,8 GB

KRITISCH — TempDB + Backup auf OS-Volume!

D:

BuchwerkProd.mdf (24 GB) + Archiv.mdf (6 GB)

120 GB

97 GB

23 GB

Noch ok, aber Autogrowth unkontrolliert

E:

BuchwerkProd_log.ldf (183 GB!) + Archiv_log.ldf

250 GB

214 GB

36 GB

Log wächst weiter — in ca. 6 Wochen voll

Tab. 33.3: Volume-Belegung BUCHSQL01 — alle drei Laufwerke auf derselben physischen HDD

 

C: hat 1,8 GB freien Speicher. TempDB hat eine Autogrowth-Einstellung von 10%. Beim nächsten Autogrowth-Ereignis wächst TempDB um 1,24 GB — dann bleiben 0,56 GB für Windows. Windows benötigt mindestens 300–500 MB für Auslagerungs- und Schreibpuffer. Der nächste Absturz ist kein "ob" sondern ein "wann".

 

Warnung: TempDB auf dem OS-Volume — ein garantierter Ausfall

TempDB auf C: ist einer der häufigsten Konfigurationsfehler überhaupt. Wenn TempDB das OS-Volume füllt, stirbt Windows — und damit SQL Server und alle laufenden Transaktionen.

Das Verschieben von TempDB erfordert einen SQL Server-Neustart, ist aber trivial (Verweis: Kapitel 13 erklärt TempDB-Dimensionierung und korrekte Platzierung vollständig).

Kein Wartungsfenster der Welt ist zu teuer, um dieses Risiko zu eliminieren. Das ist eine Maßnahme für dieses Wochenende.

 

SQL Server-Konfiguration: Das Gruselkabinett

Die folgenden Einstellungen wurden in sp_configure vorgefunden. Einige davon sind Standardwerte, die nie angepasst wurden. Andere wurden aktiv in eine schlechte Richtung geändert, vermutlich in dem Glauben, die Performance zu verbessern:

 

Setting

Ist

Empfehlung

Status

Was das anrichtet

max server memory

8.192 MB

5.500 MB

KATASTROPHAL

0 MB für OS — permanentes Auslagern

min server memory

4.096 MB

0 MB

SCHLECHT

SQL gibt Speicher nie frei, auch nachts nicht

MAXDOP

0 (unbegrenzt)

2

SCHLECHT

Alle 4 Kerne für jeden Query — CXPACKET-Stau

Cost Threshold for Parallelism

5

50

SCHLECHT

Parallelisiert jeden 3-Zeilen-Lookup

Priority Boost

1 (aktiviert)

0 (aus)

GEFÄHRLICH

OS-Prozesse verhungern — offiziell deprecated seit SQL 2008

Data Autogrowth

1 MB

1.024 MB

KATASTROPHAL

Hunderte Wachstumsereignisse/Stunde, kein IFI

Log Autogrowth

1 MB

512 MB

KATASTROPHAL

Gleich wie Data — verursacht tausende VLFs

Recovery Model

FULL

FULL (ok) + Log-Backup!

GEFÄHRLICH

FULL ohne Log-Backup = unkontrolliertes Log-Wachstum

TempDB-Dateien

1

4 (= Anzahl vCPUs)

SCHLECHT

Allokierungs-Contention bei Mehrbenutzerbetrieb

TempDB-Pfad

C:\Windows\Temp (!)

Eigenes Volume

KATASTROPHAL

TempDB auf OS-Laufwerk

Instant File Initialization

Deaktiviert

Aktiviert

SCHLECHT

Jedes Wachstumsereignis nullt die Datei — massive Einfrierungen

Kompatibilitätslevel

110 (SQL Server 2012)

150 (SQL 2019)

SCHLECHT

Verpasst 7 Jahre Optimizer-Verbesserungen

Query Store

OFF

ON

MITTEL

Keine Plan-Historie, keine Diagnose möglich

Tab. 33.4: sp_configure-Gruselkabinett auf BUCHSQL01

 

Befund 1: IO-Katastrophe — eine Spindel für alles

Ein relationales Datenbanksystem hat fundamental unterschiedliche IO-Muster für seine drei Hauptkomponenten. Daten (.mdf) brauchen zufällige Reads und Writes — der Lesekopf springt zwischen verschiedenen Datenbereichen. Das Transaktionslog (.ldf) arbeitet sequenziell — immer ans Ende des Logs. TempDB benötigt intensive Kurzzeit-IO für temporäre Objekte und Spills.

Wenn alle drei auf derselben physischen Disk laufen, konkurrieren sie ständig um die Schreib- und Leseköpfe. Jede Log-Write-Operation verdrängt eine Data-Read-Operation und umgekehrt. Die Disk-Queue-Tiefe steigt auf chronisch 12–18 (normal: < 2). Das Ergebnis sehen wir in sys.dm_io_virtual_file_stats:

-- IO-Latenzen pro Datenbankdatei auslesen
-- Stalls > 20ms für Reads, > 5ms für Writes sind ein Warnsignal
SELECT
    DB_NAME(vfs.database_id)       AS Datenbank,
    mf.physical_name               AS DateiPfad,
    vfs.io_stall_read_ms           AS ReadStall_ms,
    vfs.num_of_reads               AS ReadAnzahl,
    CASE WHEN vfs.num_of_reads > 0
         THEN vfs.io_stall_read_ms / vfs.num_of_reads
         ELSE 0 END                AS AvgRead_ms,
    vfs.io_stall_write_ms          AS WriteStall_ms,
    vfs.num_of_writes              AS WriteAnzahl,
    CASE WHEN vfs.num_of_writes > 0
         THEN vfs.io_stall_write_ms / vfs.num_of_writes
         ELSE 0 END                AS AvgWrite_ms
FROM sys.dm_io_virtual_file_stats(NULL, NULL) vfs
JOIN sys.master_files mf
    ON vfs.database_id = mf.database_id
    AND vfs.file_id    = mf.file_id
ORDER BY WriteStall_ms DESC;

 

-- Ergebnis BUCHSQL01 (Auszug):
-- BuchwerkProd  D:\BuchwerkProd.mdf      AvgRead: 94ms   AvgWrite: 312ms
-- BuchwerkProd  E:\BuchwerkProd_log.ldf  AvgRead: 12ms   AvgWrite: 198ms
-- tempdb        C:\Windows\Temp\tempdb.mdf AvgRead: 78ms AvgWrite: 287ms

IO-Latenz p95 Write: 312 ms. Zum Vergleich: Auf NVMe wären das 0,03 ms — Faktor 10.000. Jedes einzelne COMMIT wartet im Schnitt 198 ms auf den Log-Flush. Eine Abfrage über 1.000 Seiten, die auf SSD in einer Sekunde fertig wäre, braucht hier 94 Sekunden allein für Disk-Reads. Und dann sind die Seiten beim nächsten Aufruf längst aus dem Buffer Pool verdrängt — weil der Buffer Pool selbst kollabiert ist. Dazu gleich mehr.

 

Volume

Inhalt

Read avg

Write avg

Write p95

Einordnung

C:

OS + TempDB + Backup

82 ms

134 ms

312 ms

KATASTROPHAL — Backup + TempDB + OS konkurrieren

D:

Data (.mdf-Dateien)

81 ms

128 ms

287 ms

KATASTROPHAL — gleiche physische Disk wie C:

E:

Log (.ldf-Dateien)

12 ms

87 ms

214 ms

KRITISCH — Log-Writes konkurrieren mit Daten-Reads

Tab. 33.5: IO-Latenzen pro Volume — alle auf derselben physischen HDD

 

Auf einer SSD wären dieselben Zugriffe alle unter 1 ms — die Hardware-Investition würde die IO-Problematik zu über 90% beseitigen. Das ist die wichtigste Erkenntnis dieses Befunds: Manchmal ist Software-Optimierung nicht die Antwort. Manchmal muss man einfach eine SSD einbauen. Kapitel 10 erklärt IO-Latenzen, Richtwerte und die Diagnose mit sys.dm_io_virtual_file_stats ausführlich.

 

Hinweis: IO-Latenz — die Richtwerte aus Kapitel 10

Read avg < 5 ms, Write avg < 2 ms, p95 < 10 ms für produktive OLTP-Systeme.

Auf BUCHSQL01: Read avg 82 ms, Write avg 134 ms, p95 312 ms — Faktor 15 bis 60 über dem Richtwert.

Wichtig: Diese Werte gelten für den Messzeitraum im Normalbetrieb. Beim Jahresabschluss werden sie noch deutlich höher.

Eine einfache SSD (< 200 Euro) für das Data-Volume würde diese Werte auf unter 1 ms reduzieren.

 

Befund 2: 847 Autogrowth-Events in 120 Minuten — der Einfrierungsregen

In der Zeitreihe tauchen 847 Autogrowth-Ereignisse in 120 Minuten auf — im Schnitt sieben pro Minute. Jedes Autogrowth-Event friert alle Sessions ein, die auf denselben Datenbankdateien arbeiten. Da Instant File Initialization nicht aktiv ist (natürlich nicht), muss Windows bei jedem Wachstum um 1 MB die neuen Seiten mit Nullen füllen. Auf dieser HDD dauert das 50–800 ms pro Ereignis.

Das Error Log des SQL Servers dokumentiert den Einfrierungsregen präzise. Ein Auszug aus dem Messzeitraum:

-- Autogrowth-Ereignisse aus dem Error Log lesen
EXEC xp_readerrorlog 0, 1, N'autogrow';

 

-- Ergebnis BUCHSQL01 (Auszug aus 120 Minuten):
-- 08:31:14: D:\BuchwerkProd.mdf wächst 1 MB  (Einfrierung: 214 ms)
-- 08:31:47: E:\BuchwerkProd_log.ldf wächst 1 MB  (Einfrierung: 187 ms)
-- 08:32:03: D:\BuchwerkProd.mdf wächst 1 MB  (Einfrierung: 231 ms)
-- 08:32:11: E:\BuchwerkProd_log.ldf wächst 1 MB  (Einfrierung: 203 ms)
-- ... 843 weitere Ereignisse in den nächsten 118 Minuten ...

 

-- Kumulierte Einfrierungszeit: ~47 Minuten in 120 Messminuten
-- Das entspricht 39% der Messdauer = reinem Stillstand

39% der Betriebszeit steht der Server durch Autogrowth-Ereignisse still. Sämtliche ausstehenden Transaktionen werden in dieser Zeit nicht verarbeitet. Das erklärt, warum trotz dauerhafter CPU-Vollauslastung die Batch-Rate so niedrig ist: Der Server ist permanent beschäftigt — aber nicht mit Arbeit.

Die Wurzel liegt in der Konfiguration: Datendatei-Autogrowth = 1 MB, Log-Autogrowth = 1 MB. Das sind Standardwerte aus der SQL Server 7.0-Ära. Kein DBA, der das Jahr 2000 bewusst erlebt hat, sollte noch 1-MB-Autogrowth konfigurieren. Kapitel 10 erklärt die Auswirkungen von Autogrowth auf IO ausführlich — hier sehen wir sie in Echtzeit.

Und jetzt die Zahl, die besonders wehtut: Rechnet man jeden Autogrowth-Event mit durchschnittlich 200 ms Einfrierungszeit — das ist der Median der gemessenen Werte — kommt man auf 169.400 ms = 2,8 Minuten reiner Systemstillstand. In der Stunde. Jeden Tag. Seit drei Jahren.

-- Autogrowth-Konfiguration korrigieren (nach Log-Backup und Shrink)

 

-- Datendatei auf 40 GB vorbelegen, Wachstum auf 1 GB
USE master;
ALTER DATABASE BuchwerkProd
    MODIFY FILE (
        NAME = N'BuchwerkProd',
        SIZE = 40960MB,        -- 40 GB vorbelegen, verhindert Wachstum im Betrieb
        FILEGROWTH = 1024MB    -- 1 GB Schritte statt 1 MB — 1.000× seltener Wachstum
    );

 

-- Log-Datei nach Shrink auf 8 GB vorbelegen, Wachstum auf 512 MB
ALTER DATABASE BuchwerkProd
    MODIFY FILE (
        NAME = N'BuchwerkProd_log',
        SIZE = 8192MB,         -- 8 GB nach Log-Schrumpfung und Neuanlage
        FILEGROWTH = 512MB     -- Seltener wachsen, weniger VLFs entstehen
    );

 

Tipp: Instant File Initialization aktivieren — kostenlose Performance

IFI verhindert das Zero-Fill bei Datei-Autogrowth. Statt Sekunden dauert das Wachstum Millisekunden.

Aktivierung: SQL Server-Dienstkonto in "Volumes und Verzeichnisse verwalten" in der Local Security Policy eintragen.

Erfordert SQL Server-Neustart — im nächsten Wartungsfenster einplanen.

Nach IFI + korrektem Autogrowth: 0 Autogrowth-Events pro Tag (bei großzügiger Vorbelegung).

Hinweis: Gilt nur für Datendateien (.mdf, .ndf). Das Transaktionslog (.ldf) wird immer mit Nullen beschrieben — das ist absichtlich so.

 

Befund 3: max server memory = 8.192 MB bei 8 GB RAM — das OS verhungert

Dieser Befund gehört zu den Klassikern. Er ist in Kapitel 11 ausführlich erklärt, aber hier sehen wir ihn in seiner extremsten Form: SQL Server darf 100% des physischen RAMs nutzen. Für Windows bleiben rechnerisch 0 MB — in der Praxis sind es 0 bis 200 MB, je nach Moment. Ja, wirklich. 0 MB für Windows. Wir lügen nicht.

-- max server memory Status prüfen
SELECT name, value_in_use, description
FROM sys.configurations
WHERE name IN ('max server memory (MB)', 'min server memory (MB)');

 

-- Ergebnis BUCHSQL01:
-- max server memory (MB) = 8192  ← entspricht exakt dem physischen RAM!
-- min server memory (MB) = 4096  ← SQL gibt auch nachts keinen Speicher frei

 

-- OS-Speicherstatus prüfen
SELECT
    physical_memory_in_use_kb / 1024   AS SQLMemory_MB,
    available_physical_memory_kb / 1024 AS OSVerfuegbar_MB,
    page_fault_count                    AS Seitenfehler
FROM sys.dm_os_process_memory;

 

-- Ergebnis: SQLMemory_MB = 7.987, OSVerfuegbar_MB = 48 — 48 MB für Windows!

Die Auswirkungen sind messbar und drastisch. Der Page Life Expectancy (PLE) — also die durchschnittliche Verweildauer einer Seite im Buffer Pool — liegt bei 4,3 Sekunden. Der Richtwert für 8 GB Buffer Pool ist mindestens 300 Sekunden (Kapitel 11 erklärt die Berechnung). 4,3 Sekunden bedeutet: Jede Seite wird nach durchschnittlich 4,3 Sekunden wieder aus dem Cache verdrängt. Jede Abfrage, die eine Seite benötigt, die vor 5 Sekunden noch im Cache war, muss sie von der Festplatte neu lesen. Bei 82 ms durchschnittlicher Read-Latenz.

 

Indikator

Min

Avg

p95

Max

Einordnung

Page Life Expectancy (s)

1

4,3

12,1

847

TOTAL VERSAGEN — kein stabiler Buffer Pool

Available MBytes (OS)

0 MB

48 MB

187 MB

412 MB

16-mal auf 0 MB! OS hat zeitweise keinen RAM

Pages/sec (OS Memory)

2.847

41.847

68.203

94.812

Dauerhaftes OS-Paging — Faktor 400× zu hoch

Total Server Memory

7.812 MB

7.987 MB

8.104 MB

8.192 MB

SQL nutzt alles — OS hat nichts

Lazy Writes/sec

14

287

512

847

Lazy Writer läuft dauerhaft heiß

Memory Grants Pending

0

3,2

8

14

Queries warten dauerhaft auf Memory Grants

Tab. 33.6: Memory-Indikatoren BUCHSQL01 — PLE von 4,3 Sekunden bedeutet Totalversagen

 

Available MBytes erreicht 16-mal während der Messperiode den Wert 0 MB. Das bedeutet: Windows hat zeitweise buchstäblich keinen freien Arbeitsspeicher mehr. In diesem Zustand kann Windows keine neuen Prozesse starten, keine Systemdienste ausführen und keine Netzwerkpakete puffern. Antwortzeiten der Benutzer erreichen in diesen Momenten 30–60 Sekunden oder Verbindungs-Timeouts.

Pages/sec von 41.847 bedeutet: Windows lagert im Durchschnitt 41.847 Seiten pro Sekunde auf die Festplatte aus — oder liest sie von dort zurück. Der Richtwert für ein gesundes System: unter 100. Hier: Faktor 400 darüber. Die ohnehin schon überlastete HDD wird zusätzlich mit OS-Paging-IO bombardiert. Disk hilft Disk auf die Sprünge, wie man so schön nicht sagt.

 

Warnung: min server memory = 4.096 MB — ein oft übersehenes Problem

min server memory von 4 GB bedeutet: SQL Server gibt niemals weniger als 4 GB RAM frei — auch nachts, auch, wenn niemand arbeitet.

Das Problem: Windows kann auch nachts nicht genug Speicher für Wartungsaufgaben bekommen (Windows Update, Backup, Systemindexierung).

Korrekte Einstellung: min server memory = 0 (Standardwert). SQL Server fordert Speicher dynamisch an und gibt ihn dynamisch zurück.

Auf BUCHSQL01 wurde min server memory offenbar aus demselben Missverständnis gesetzt wie max server memory: "Mehr ist besser." Es ist nicht besser.

 

Die Sofortmaßnahme ist einfach und kostet nichts: max server memory auf 5.500 MB setzen. Das gibt Windows 2,5 GB für OS, Netzwerkstack und Backup-Prozesse. Nach dieser Einstellung steigt der PLE auf durchschnittlich 620 Sekunden — 144× besser, ohne eine Zeile Code zu ändern, ohne neue Hardware.

-- max server memory korrekt setzen
-- Faustformel: Physischer RAM minus 2–4 GB für OS (je nach Last)
-- Bei 8 GB physisch: 8.192 minus 2.500 = 5.692 → auf 5.500 MB runden
EXEC sp_configure 'max server memory (MB)', 5500;
RECONFIGURE;

 

-- min server memory zurück auf 0 (Standardwert)
EXEC sp_configure 'min server memory (MB)', 0;
RECONFIGURE;

 

-- Wirkung: Sofort. Kein Neustart erforderlich.
-- PLE steigt binnen Minuten von 4 auf >300 Sekunden.
-- Pages/sec sinkt von 41.847 auf <500.
-- Available MBytes stabilisiert sich bei >1.500 MB.

Befund 4: Priority Boost = 1 — der deprecated Klassiker

Priority Boost aktiviert. Ja, wirklich. Diese Einstellung ist seit SQL Server 2008 offiziell als "nicht unterstützt und nicht empfohlen" markiert — also seit 16 Jahren. Wer sie heute noch aktiviert hat, hat entweder eine sehr alte Installationsanleitung befolgt oder sehr kreative Optimierungsideen.

Priority Boost versetzt den SQL Server-Prozess (sqlservr.exe) in die Windows-Prioritätsklasse REALTIME — die höchste verfügbare. Das bedeutet: SQL Server-Threads werden gegenüber ALLEN anderen Windows-Threads priorisiert, einschließlich der Systemthreads für Netzwerkstack, Disk-Manager und Memory Manager. Das Ergebnis: Windows-Systemdienste können ihre Arbeit nicht rechtzeitig erledigen. Das führt zu sporadisch höheren Disk-Latenzen, Netzwerkproblemen und im schlimmsten Fall zu OS-Instabilität.

 

Warnung: Priority Boost — oder: Wie man den Motor übertaktet

Priority Boost aktivieren ist wie den Motor auf 150% Drehzahl übertakten — klingt nach mehr Power, endet mit Motorschaden.

Auf BUCHSQL01: kein messbarer Performance-Gewinn durch Priority Boost, aber erhöhtes Stabilitätsrisiko.

Sporadisch messbar: Höhere Disk-Latenzen in Momenten, in denen Windows-Systemdienste auf ihre CPU-Zeit warten.

Deaktivierung erfordert SQL Server-Neustart — im nächsten Wartungsfenster einplanen.

 

-- Priority Boost Status prüfen
SELECT name, value_in_use
FROM sys.configurations
WHERE name = 'priority boost';
-- value_in_use = 1: aktiviert — sofort deaktivieren!
-- value_in_use = 0: deaktiviert — so soll es sein

 

-- Priority Boost deaktivieren
EXEC sp_configure 'priority boost', 0;
RECONFIGURE;

 

-- WICHTIG: Erfordert SQL Server-Neustart, um vollständig wirksam zu werden.
-- Einen Neustart im nächsten Wartungsfenster einplanen.
-- Diese Einstellung sollte in keiner Produktionsumgebung aktiviert sein.

Befund 5: Transaktionslog 183 GB bei 24 GB Datenbank — das Log-Disaster

Das Transaktionslog ist 7,6-mal größer als die Datenbank. Das ist kein Rechenfehler. Das Verhältnis Datenbank zu Log sollte in einem gesunden System bei maximal 0,5:1 liegen — also das Log kleiner als die Hälfte der Datenbank. Hier: 7,6:1 in die falsche Richtung.

Die Ursache ist trivial und gleichzeitig gefährlich: Recovery Model ist FULL — absolut korrekt für eine Steuerberatungskanzlei mit Compliance-Anforderungen. FULL Recovery Model ist die richtige Wahl. Aber FULL Recovery Model ohne Log-Backup ist das schlimmste mögliche Setup. Das Log kann nicht rotieren (log_reuse_wait_desc = LOG_BACKUP), weil SQL Server darauf wartet, dass ein Log-Backup die alten Transaktionen freigibt. Das letzte Log-Backup: nie. msdb zeigt keinen einzigen Log-Backup-Eintrag. Nicht "keinen in den letzten 30 Tagen" — keinen jemals.

-- Log-Status und Wartegrund prüfen
SELECT
    name              AS Datenbank,
    log_reuse_wait_desc,        -- LOG_BACKUP = wartet auf Log-Backup
    recovery_model_desc         -- FULL = Log-Backup ist Pflicht
FROM sys.databases
WHERE name = 'BuchwerkProd';

 

-- Ergebnis:
-- log_reuse_wait_desc = LOG_BACKUP
-- recovery_model_desc = FULL

 

-- Letztes Log-Backup aus msdb abfragen
SELECT TOP 1
    database_name, backup_finish_date, type
FROM msdb.dbo.backupset
WHERE database_name = 'BuchwerkProd'
  AND type = 'L'
ORDER BY backup_finish_date DESC;
-- Ergebnis: 0 Zeilen. Kein einziges Log-Backup jemals.

 

-- VLF-Anzahl prüfen (Vorsicht: dauert auf 48.312 VLFs lange!)
USE BuchwerkProd;
SELECT COUNT(*) AS VLF_Anzahl FROM sys.dm_db_log_info(DB_ID());
-- Ergebnis: 48.312 VLFs (normal: < 50)

48.312 VLFs — warum das so schlimm ist

Das Transaktionslog von SQL Server ist intern in sogenannte Virtual Log Files (VLFs) aufgeteilt. Bei einer gesunden Konfiguration gibt es 8–32 VLFs pro Log-Datei. Das 1-MB-Autogrowth hat über drei Jahre 48.312 VLFs erzeugt — jedes einzelne Autogrowth-Ereignis hat neue VLFs angelegt. Kapitel 6 erklärt den VLF-Mechanismus ausführlich, einschließlich der Faustregel für Ziel-VLF-Anzahl.

Die Auswirkungen sind dreifach: Erstens dauert der SQL Server-Start länger, weil alle VLFs beim Startup durchgescannt werden (Crash Recovery). Bei 48.000 VLFs sind das mehrere Minuten extra Startup-Zeit. Zweitens scannt SQL Server bei der Log-Backup-Validierung alle VLFs sequenziell — ein erstes Log-Backup dieser Datei würde bei der vorliegenden Disk-Performance voraussichtlich 2–6 Stunden dauern. Drittens wird jedes Log-Sequencing-Lookup langsamer.

 

Metrik

Wert

Normal

Bedeutung

Log-Datei-Größe

183 GB

1–5 GB

7,6× größer als die Datenbank

log_reuse_wait_desc

LOG_BACKUP

NOTHING

Log kann nicht rotieren, weil kein Log-Backup existiert

VLF-Anzahl

48.312

< 50

Durch 1-MB-Wachstum: 48.000 winzige VLFs entstanden

Log-Wachstum/Stunde

ca. 1,2 GB

0 (nach Backup)

Ohne Maßnahmen: E: voll in ca. 6 Wochen

Letztes Log-Backup

Nie

Täglich

msdb zeigt keinen einzigen Log-Backup-Eintrag

RPO (aktuelles Risiko)

Seit letztem Full-Backup

< 15 Min

Datenverlust von mehreren Tagen möglich

Tab. 33.7: Transaktionslog-Katastrophe auf BUCHSQL01 in Zahlen

 

Eine Steuerberatungskanzlei mit Compliance-Anforderungen, die seit 18 Monaten kein Log-Backup gemacht hat, hat ein ernsthaftes rechtliches Problem. Das ist keine Übertreibung. Wenn ein Disk-Ausfall morgen passiert, ist der Datenverlust alles seit dem letzten Full-Backup — möglicherweise eine Woche, möglicherweise mehr.

Die Maßnahme ist klar, aber anspruchsvoll in der Ausführung. Das erste Log-Backup wird lange dauern — 48.312 VLFs müssen verarbeitet werden. Wichtig: Das Backup läuft online, der Server bleibt verfügbar. Aber er wird während des Backups noch langsamer. Ein Abendwartungsfenster ist Pflicht.

-- Schritt 1: Erstes Log-Backup (komprimiert, sonst füllt sich E: noch schneller)
-- Hinweis: Kann bei 183 GB Log und 48.000 VLFs 2–6 Stunden dauern!
BACKUP LOG BuchwerkProd
    TO DISK = 'E:\Backup\BuchwerkProd_log_erstbackup.bak'
    WITH COMPRESSION, STATS = 5;

 

-- Schritt 2: Nach erfolgreichem Backup Log schrumpfen
-- SHRINKFILE ist hier ausnahmsweise vertretbar — pathologische Ausgangssituation
-- Danach sofort korrekt vorbelegen!
USE BuchwerkProd;
DBCC SHRINKFILE (BuchwerkProd_log, 2048);  -- Ziel: 2 GB als Ausgangsbasis

 

-- Schritt 3: VLF-Anzahl nach Shrink prüfen
SELECT COUNT(*) AS VLF_Anzahl FROM sys.dm_db_log_info(DB_ID());
-- Ziel: < 64 VLFs nach Schrumpfung und Neuanlage

 

-- Schritt 4: Log-Backup-Job im SQL Agent einrichten
-- Alle 15 Minuten — damit läuft das Log nie wieder unkontrolliert voll
-- RPO sinkt von "unbekannt (Tage)" auf 15 Minuten

 

Hinweis: Wann ist SHRINKFILE erlaubt?

Kapitel 6 erklärt, warum SHRINKFILE fast immer ein Anti-Pattern ist: Es erzeugt Fragmentierung und verursacht künftig erneute Autogrowth-Events.

Auf BUCHSQL01 ist SHRINKFILE ausnahmsweise vertretbar — weil das Log so pathologisch groß ist (183 GB für 24 GB Datenbank) und danach sofort korrekt neu vorbelegt wird.

Faustregel: SHRINKFILE nur dann, wenn danach unmittelbar eine großzügige Vorbelegung folgt und ein Log-Backup-Job läuft.

Nach dieser Maßnahme: VLF-Anzahl von 48.312 auf 16, Log-Größe von 183 GB auf 8 GB.

 

Befund 6: MAXDOP=0 und Cost Threshold=5 — der Parallelismus-Kollaps

Mit MAXDOP=0 darf jeder einzelne Query alle 4 CPU-Kerne verwenden. Mit Cost Threshold=5 entscheidet SQL Server, einen Query zu parallelisieren, sobald sein geschätzter serieller Ausführungsaufwand den Wert 5 überschreitet. Das ist extrem niedrig — schon ein simpler SELECT auf 500 Zeilen erreicht Cost 5. Kapitel 5 erklärt MAXDOP-Kalkulation und Cost Threshold ausführlich.

Das Ergebnis auf einem 4-Kern-System: Jede einzelne Abfrage — auch triviale Lookups — fordert sofort alle 4 Kerne an. Da 47 Benutzer gleichzeitig Abfragen stellen, wollen permanent 47 Queries × 4 Kerne = 188 Kern-Slots belegt sein. Verfügbar sind: 4. Es entsteht ein massiver CXPACKET-Wait-Stau, der die Durchsatzrate auf einem Bruchteil der theoretisch möglichen Leistung hält.

 

Indikator

Min

Avg

p95

Max

Einordnung

% Processor Time (System)

12%

78,4%

94,1%

99,8%

Dauervollast — System am Limit

% Processor Time (sqlservr)

24%

312,7%

387,4%

399,2%

399% = alle 4 Kerne dauerhaft ausgelastet

Processor Queue Length

2

8,4

17

31

Chronischer Scheduler-Stau: avg. 8 Threads warten

Batch Requests/sec

3

87

214

634

Niedrig — Server so langsam, Batches stauen sich

SQL Compilations/sec

0

3,4

12,1

47,2

Hoch — Ad-hoc-Queries + kein Plan-Caching

Full Scans/sec

1

34,7

121,4

487,2

Sehr hoch — 97% Fragmentierung, keine Index-Pflege

Workfiles Created/sec

0

12,4

67,3

284,1

Erhöht — Memory so knapp, Queries spillen in TempDB

Tab. 33.8: CPU- und Throughput-Metriken BUCHSQL01 — alle 4 Kerne dauerhaft überlastet

 

Der Processor Queue Length liegt im Durchschnitt bei 8,4. Das bedeutet: Jederzeit warten 8 Threads auf CPU-Zeit, die sie gerade nicht bekommen. Ein Wert > 2 gilt als kritisch. Ein Wert von 8,4 im Durchschnitt — nicht als Spitze, als Durchschnitt — ist der Beweis, dass die CPU-Kapazität fundamental zu gering ist. Ein gut konfigurierter Server mit MAXDOP=2 und Cost Threshold=50 würde dieselbe Hardware auf circa den dreifachen Durchsatz bringen.

-- MAXDOP und Cost Threshold korrigieren
-- MAXDOP=2: Maximal 2 Kerne pro Query — sinnvoll für 4-Kern-System mit 47 Nutzern
EXEC sp_configure 'max degree of parallelism', 2;

 

-- Cost Threshold=50: Parallelismus nur für wirklich teure Queries
-- Statt 5 (zu niedrig, fast alles wird parallelisiert) → 50 (guter Startwert)
EXEC sp_configure 'cost threshold for parallelism', 50;
RECONFIGURE;

 

-- Soforteffekt: CXPACKET-Waits sinken drastisch, Durchsatz steigt.
-- Kein Neustart erforderlich.

 

-- Ergänzend: Optimize for Ad Hoc Workloads aktivieren
-- Verhindert das Aufblähen des Plan-Cache durch Ad-hoc-Queries
EXEC sp_configure 'optimize for ad hoc workloads', 1;
RECONFIGURE;

Befund 7: Kompatibilitätslevel 110 auf SQL Server 2019 — sieben Jahre verpasst

SQL Server 2019 läuft auf Kompatibilitätslevel 110. Das entspricht SQL Server 2012. Das bedeutet: Der Cardinality Estimator 70 — aus der SQL Server 7.0-Ära — wird verwendet. Alle CE-Verbesserungen seit 2014 (CE 120, CE 130, CE 140, CE 150) werden ignoriert. Alle Intelligent Query Processing Features von SQL Server 2019 sind nicht verfügbar. Sieben Jahre Optimizer-Verbesserungen — nicht genutzt.

Warum noch auf Level 110? Die Antwort lautet immer gleich: "Wir hatten Angst, dass etwas bricht." Das ist verständlich — Änderungen im Cardinality Estimator können zu Plan Regression führen. Kapitel 16 erklärt dieses Risiko und wie man es mit dem Query Store (Kapitel 19) als Sicherheitsnetz managt.

Auf BUCHSQL01 gibt es einen besonders relevanten Effekt durch den Wechsel auf Level 150: UDF-Inlining. Ab Kompatibilitätslevel 150 kann SQL Server 2019 Scalar UDFs automatisch inlinen — also die Funktionslogik direkt in den Abfrageplan integrieren, ohne Row-by-Row-Ausführung. Das betrifft genau fn_GetSteuersatz und fn_GetMandantTyp — die teuersten Funktionen auf diesem Server.

-- Schritt 1: Query Store aktivieren als Sicherheitsnetz
-- (Ermöglicht Forced Plans bei Plan Regression nach CE-Wechsel)
ALTER DATABASE BuchwerkProd
    SET QUERY_STORE = ON
    (OPERATION_MODE = READ_WRITE, QUERY_CAPTURE_MODE = ALL);

 

-- Schritt 2: Kompatibilitätslevel auf 150 erhöhen
ALTER DATABASE BuchwerkProd SET COMPATIBILITY_LEVEL = 150;
-- Sofort wirksam, kein Neustart erforderlich.
-- Vorher in Testumgebung prüfen — Änderungen im Optimizer sind möglich.

 

-- Schritt 3: Prüfen ob UDF-Inlining aktiv ist
SELECT name, is_inlineable
FROM sys.sql_modules
WHERE OBJECT_NAME(object_id) IN ('fn_GetSteuersatz', 'fn_GetMandantTyp');
-- is_inlineable = 1: Funktion wird automatisch geinlined
-- is_inlineable = 0: Funktion kann nicht geinlined werden (komplexe Logik)

Befund 8: fn_GetSteuersatz — 360.000 Aufrufe pro Query

In drei der am häufigsten genutzten Prozeduren (usp_GetBuchungsliste, usp_GetMandantenReport, usp_GetJahresabschluss) sind benutzerdefinierte Skalarfunktionen in WHERE-Klauseln eingebettet. Das sieht auf den ersten Blick harmlos aus — schließlich sind es doch "nur" zwei kleine Funktionen:

-- Typisches Muster in usp_GetBuchungsliste:
SELECT b.BuchNr, b.Datum, b.Betrag, b.Konto
FROM   dbo.Buchungen b
WHERE  b.MandantNr = @MandantNr
   AND dbo.fn_GetSteuersatz(b.Konto, b.Datum) = @GesuchterSatz
   AND dbo.fn_GetMandantTyp(b.MandantNr)      IN ('A', 'B')
ORDER BY b.Datum DESC;

 

-- Was hier passiert (bei Compat Level 110 — kein UDF-Inlining):
-- fn_GetSteuersatz und fn_GetMandantTyp sind Scalar UDFs.
-- SQL Server kann diese NICHT in den Plan integrieren.
-- Für jede einzelne Zeile in dbo.Buchungen wird jede Funktion separat ausgeführt.
-- Bei 180.000 Buchungssätzen des Mandanten = 360.000 Funktionsaufrufe pro Query.
-- Row-by-Row statt Set-based — das ist das Grundproblem.

 

-- Laufzeit messen mit sys.dm_exec_function_stats
SELECT
    OBJECT_NAME(object_id) AS FunktionsName,
    execution_count,
    total_worker_time / 1000000.0 AS GesamtCPU_Sekunden,
    total_elapsed_time / 1000000.0 AS GesamtLaufzeit_Sekunden
FROM sys.dm_exec_function_stats
WHERE database_id = DB_ID('BuchwerkProd')
ORDER BY total_worker_time DESC;

 

-- Ergebnis: fn_GetSteuersatz — 847 CPU-Sekunden pro Stunde allein für diese Funktion

sys.dm_exec_function_stats zeigt: total_worker_time für fn_GetSteuersatz = 847.000.000 µs pro Stunde — 847 CPU-Sekunden pro Stunde allein für diese eine Funktion. Die Abfrage dauert 8–14 Minuten statt unter 3 Sekunden. Das ist der direkte Effekt von Row-by-Row-Ausführung auf einer Tabelle mit 4,8 Millionen Zeilen. Kapitel 26 erklärt Scalar UDFs als Performance-Anti-Pattern vollständig, inklusive dem Vergleich mit Inline TVFs.

 

Prozedur

Ausführungszeit Ist

Sollte sein

Hauptursache

usp_GetBuchungsliste

8–14 Minuten

< 3 Sekunden

Scalar UDF × 360.000 Aufrufe + 97% Fragmentierung

usp_GetMandantenReport

22–47 Minuten

< 10 Sekunden

SELECT * + 2 Scalar UDFs + CURSOR-Schleife

usp_GetJahresabschluss

3–8 Stunden

< 15 Minuten

Alle oben genannten Probleme kombiniert

Tab. 33.9: Ausführungszeiten der kritischsten Prozeduren — und was sie sein sollten

 

Die gute Nachricht: Nach Anheben des Kompatibilitätsniveaus auf 150 werden fn_GetSteuersatz und fn_GetMandantTyp automatisch geinlined — ohne Code-Änderung. Die Prozeduren werden ohne Anpassung drastisch schneller. Für Fälle, bei denen UDF-Inlining nicht möglich ist (komplexe Logik, Rekursion, externe Abhängigkeiten), beschreibt Kapitel 26 die Umschreibung als Inline-TVF — die performante Alternative.

Befund 9: Index-Fragmentierung 97% — drei Jahre ohne Pflege

Kein einziges Index-Rebuild oder -Reorganize seit dem initialen Setup vor drei Jahren. Das Ergebnis kann man in sys.dm_db_index_physical_stats ablesen:

 

Tabelle

Zeilen

Fragmentierung

Index-Reads (Soll)

Index-Reads (Ist)

dbo.Buchungen

4.812.347

97,4%

~12.000 Pages

~487.000 Pages (41×!)

dbo.Buchungspositionen

18.447.213

96,8%

~89.000 Pages

~2.847.000 Pages (32×!)

dbo.Mandanten

84.312

94,2%

~800 Pages

~9.400 Pages (12×!)

dbo.Kontenrahmen

312.847

91,7%

~3.200 Pages

~29.400 Pages (9×!)

dbo.Steuersätze

12.483

87,3%

~150 Pages

~1.060 Pages (7×!)

Tab. 33.10: Index-Fragmentierung auf den Haupttabellen — 97% entsprechen 41× mehr Logical Reads

 

97% Fragmentierung auf der größten Tabelle bedeutet: Anstatt Daten kompakt in aufeinanderfolgenden Seiten zu speichern, sind die Daten über tausende nicht-zusammenhängende Speicherbereiche verteilt. SQL Server muss für einen Scan, der theoretisch 12.000 Seiten lesen sollte, 487.000 Seiten lesen — 41-mal mehr. Bei jeder dieser Seiten kann ein Disk-Head-Movement nötig sein. Das ist der Grund für die katastrophalen Full-Scan-Zeiten.

Ein einmaliges Index-Rebuild auf den Haupttabellen würde bei den aktuellen Disk-Latenzen (avg. 82 ms) voraussichtlich 18–36 Stunden dauern. Das Rebuild sollte daher erst nach der Hardware-Aufrüstung (SSD) durchgeführt werden — auf SSD dauert derselbe Vorgang 1–3 Stunden. ONLINE-Rebuild ist in SQL Server Standard Edition nicht verfügbar; das Rebuild muss im Wartungsfenster erfolgen. Kapitel 17 erklärt Index-Wartungsstrategien vollständig.

-- Fragmentierung aller Indizes prüfen
-- avg_fragmentation_in_percent > 30: Rebuild empfohlen
-- avg_fragmentation_in_percent 10-30: Reorganize ausreichend
SELECT
    OBJECT_NAME(ips.object_id) AS Tabelle,
    i.name                     AS IndexName,
    ips.avg_fragmentation_in_percent AS Fragmentierung_Pct,
    ips.page_count             AS Seiten
FROM sys.dm_db_index_physical_stats(
    DB_ID('BuchwerkProd'), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i
    ON i.object_id = ips.object_id
    AND i.index_id = ips.index_id
WHERE ips.avg_fragmentation_in_percent > 30
  AND ips.page_count > 1000
ORDER BY ips.avg_fragmentation_in_percent DESC;

 

-- Nach SSD-Umbau: Index-Rebuild im Wartungsfenster
-- Danach wöchentlicher Wartungsplan (Reorganize bei <30%, Rebuild bei >30%)

Befund 10: SELECT * in 23 von 31 Stored Procedures — die Daten-Orgie

23 von 31 gespeicherten Prozeduren verwenden SELECT * ohne Spaltenliste. Die Haupttabellen haben zwischen 140 und 280 Spalten — darunter viele NVARCHAR(MAX)- und VARBINARY-Felder für Notizfelder und Dokumentanhänge. Typisches Nutzungsmuster: Die Anwendung benötigt 3–5 Spalten, liest aber alle 183 mit.

 

Szenario

Gelesene Spalten

Bytes/Zeile

Bytes/Query (600.000 Zeilen)

Overhead

Was benötigt wird (mit Spaltenliste)

4 Spalten

~80 Bytes

~48 MB

Baseline

Was tatsächlich gelesen wird (SELECT *)

183 Spalten

~3.400 Bytes

~2.040 MB

42× mehr IO und Memory

Tab. 33.11: SELECT * Overhead — 42× mehr IO für dieselbe Funktionalität

 

Das hat drei direkte Auswirkungen. Erstens liest SQL Server 42-mal mehr Disk-IO als nötig — auf der ohnehin überlasteten Spindel ein erheblicher Faktor. Zweitens müssen 2.040 MB statt 48 MB über das 100-MBit/s-Netzwerk übertragen werden: Bei 100 MBit/s sind das 163 Sekunden statt 3,8 Sekunden für diesen Transfer. Drittens müssen 2.040 MB in den Buffer Pool passen statt 48 MB — der ohnehin auf PLE=4 s komprimierte Buffer Pool wird weiter verdrängt.

Die Maßnahme ist Entwicklungsarbeit: Schritt für Schritt die Prozeduren mit der höchsten Aufruffrequenz umschreiben, priorisiert nach sys.dm_exec_procedure_stats. Kapitel 24 (Result-Set-Design) und Kapitel 21 (SARGability) beschreiben SELECT * als Anti-Pattern ausführlich.

Wait Statistics: Die vollständige Diagnoseliste

Die Wait Statistics (Kapitel 9) zeigen auf BUCHSQL01 keine klare Diagnose — sie zeigen alle Diagnosen gleichzeitig. Jeder Wait Type in dieser Liste hat eine eigene Geschichte, und sie alle spielen zusammen. Das ist der Unterschied zum Musterwerk-Fall aus Kapitel 32: Dort hatten wir einen dominanten Wait Type. Hier ist es eine Sammlung.

 

Wait Type

Kumuliert (s)

Ursache

Maßnahme

CXPACKET

48.341

MAXDOP=0 + CTP=5 parallelisiert alles

MAXDOP=2, CTP=50

PAGEIOLATCH_SH

41.287

PLE=4s + 97% Frag. + Spindel-IO

SSD + Index-Rebuild

PAGEIOLATCH_EX

27.834

Autogrowth-Events + Lazy Writer rasend aktiv

Autogrowth auf 1 GB + IFI

LCK_M_S + LCK_M_X

22.841

Kein RCSI, lange Transaktionen durch langsame Queries

RCSI aktivieren + Queries optimieren

SOS_SCHEDULER_YIELD

14.204

Scalar UDFs, Cursor-Schleifen, SELECT *

UDFs umschreiben, Cursor eliminieren

PAGELATCH_EX (TempDB)

12.847

1 TempDB-Datei für 4 Kerne + TempDB auf C:

4 TempDB-Dateien auf eigenem Volume

WRITELOG

9.841

1-MB-Autogrowth erzwingt häufige Log-Allokierung

Log-Autogrowth auf 512 MB + IFI

IO_COMPLETION

8.412

Spindel-IO mit avg. 82 ms Read-Latenz

SSD-Storage zwingend erforderlich

ASYNC_NETWORK_IO

1.842

SELECT * sendet 140–280 Spalten über 100 MBit/s

Spalten einschränken, Netzwerk aufrüsten

Tab. 33.12: Wait Statistics BUCHSQL01 — vollständige Diagnoseliste mit Maßnahmen

 

CXPACKET kumulierte Wartezeit: 48.341 Sekunden. Die Messperiode dauert 7.200 Sekunden (120 Minuten). Das bedeutet: Über alle Sessions summiert haben Threads in diesen 120 Minuten 48.341 Sekunden auf parallele Partner gewartet — das 6,7-Fache der Messzeit. Für einen 4-Kern-Server ist das ein Zeichen totalen Parallelismus-Kollapses.

PAGEIOLATCH_SH: 41.287 Sekunden Wartezeit. Pro Session (47 Nutzer) sind das durchschnittlich 878 Sekunden Disk-Wartezeit in 120 Minuten. Die Benutzer verbringen etwa 12% ihrer aktiven Zeit damit, auf Disk-Reads zu warten — allein für IO, ohne die anderen Wait Types einzurechnen.

SOS_SCHEDULER_YIELD mit 14.204 Sekunden kumulierter Wartezeit ist ein direkter Fingerabdruck der Scalar UDFs und Cursor-Schleifen. Signal Waits als Anteil an den Gesamtwaits liegen bei 34% — das ist CPU-Hunger (Kapitel 9 erklärt den Unterschied zwischen Signal Waits und Resource Waits ausführlich). Die 4 Kerne sind buchstäblich nicht genug.

Maßnahmenplan: Priorisiert nach Dringlichkeit

Phase 1: Sofortmaßnahmen — heute, noch vor dem nächsten Buchungslauf

Diese Maßnahmen kosten nichts, erfordern keinen Hardware-Kauf und bringen sofort messbare Verbesserung. Reihenfolge ist wichtig — Log-Backup zuerst, weil das E:-Volume sonst in 6 Wochen voll ist.

 

#

Maßnahme

Aufwand

Downtime?

Effekt

1

Log-Backup erzwingen + Log-Backup-Job einrichten

2–6 Std.

Nein

E: schrumpft, Log rotiert, RPO gesichert

2

max server memory auf 5.500 MB, min auf 0

5 Min

Nein

PLE von 4s auf >300s, OS-Paging stoppt

3

Priority Boost deaktivieren

5 Min

Neustart nötig

OS-Stabilität steigt, kein Stabilitätsrisiko mehr

4

MAXDOP=2, Cost Threshold=50

5 Min

Nein

CPU-Durchsatz +50–100%

5

Autogrowth-Werte korrigieren + Datei vorbelegen

10 Min

Nein

Einfrierungsregen stoppt sofort

6

Kompatibilitätslevel auf 150 erhöhen

10 Min

Nein

UDF-Inlining aktiv, Queries schneller

7

Backup-Ziel von C: auf E: verschieben

30 Min

Nein

C: läuft nicht mehr voll durch Backups

Tab. 33.13: Phase-1-Maßnahmen — Sofortmaßnahmen ohne Hardware-Kauf

 

Phase 2: Kurzfristig — in den nächsten 2–4 Wochen

 

#

Maßnahme

Abhängigkeit

Downtime?

Effekt

8

Neues Volume F: für TempDB (SSD, min. 50 GB)

Hardware-Beschaffung

Nein (Beschaffung)

TempDB weg von C:

9

TempDB auf F: verschieben, 4 Dateien anlegen

Maßnahme 8

Ja (SQL-Neustart)

TempDB-Waits –80%, C: sicher

10

RAM-Upgrade auf min. 32 GB

Hardware

Ja

PLE > 3.000s, Paging stoppt vollständig

11

IFI aktivieren (Dienstkonto in Security Policy)

Maßnahme 10

Ja (SQL-Neustart)

Autogrowth ohne Einfrierung

12

SSD für D: und E: (Data + Log-Volumes)

Hardware

Ja

IO von 82 ms avg auf < 1 ms avg

13

Netzwerk auf 1 GBit/s aufrüsten

Hardware

Nein

ASYNC_NETWORK_IO-Waits verschwinden

Tab. 33.14: Phase-2-Maßnahmen — Hardware-Investitionen mit hohem ROI

 

Phase 3: Mittelfristig — in den nächsten 1–3 Monaten

 

#

Maßnahme

Voraussetzung

Aufwand

Effekt

14

Index-Rebuild aller Haupttabellen

SSD-Umbau (Maßnahme 12)

4–8 Std.

Fragmentierung 97% → < 5%

15

SELECT * aus allen 23 Prozeduren entfernen

Entwicklungszeit

1–3 Tage

IO –80–90% pro Query

16

Cursor in usp_GetMandantenReport durch Set-based ersetzen

Entwicklungszeit

0,5 Tage

Prozedur: 47 Min → < 30 Sek

17

RCSI auf BuchwerkProd aktivieren

Maßnahme 14 empfohlen

1 Std.

Blocking-Ketten verschwinden

18

Index-Wartungsplan einrichten (wöchentlich)

Maßnahme 14

1 Std.

Dauerhaft niedrige Fragmentierung

19

Query Store-Monitoring und Baseline einrichten

Compat Level 150

2 Std.

Planstabilität, Frühwarnung bei Regression

Tab. 33.15: Phase-3-Maßnahmen — Entwicklungsarbeit und dauerhafte Prozesse

 

Vorher/Nachher: Die Zahlen nach 3 Monaten

Nach Abschluss aller drei Phasen — Hardware-Upgrade (SSD, RAM), Konfigurationsbereinigung und Entwicklungsarbeit — sehen die Messwerte so aus. Alle Zahlen basieren auf tatsächlich gemessenen Werten nach der Umsetzung:

 

Metrik

Vorher

Nachher

Faktor

IO-Latenz p95 Write

312 ms

0,8 ms

390× besser

IO-Latenz avg Read

82 ms

0,3 ms

273× besser

Page Life Expectancy

4,3 s

6.200 s

1.442× besser

Pages/sec (OS-Paging)

41.847/s

< 50/s

836× besser

usp_GetBuchungsliste

8–14 Minuten

< 2 Sekunden

~400× schneller

usp_GetMandantenReport

22–47 Minuten

< 30 Sekunden

~60× schneller

usp_GetJahresabschluss

3–8 Stunden

15–30 Minuten

~12× schneller

VLF-Anzahl

48.312

16

3.019× weniger

Log-Größe

183 GB

8 GB

23× kleiner

Autogrowth-Events/Tag

~6.000

0

vollständig eliminiert

Freier Speicher C:

1,8 GB (0,9%)

> 100 GB

kein Absturzrisiko mehr

Processor Queue Length avg

8,4

1,2

7× besser

Tab. 33.16: Vorher/Nachher-Vergleich nach vollständiger Maßnahmenumsetzung

 

Der erste Jahresabschluss nach den Maßnahmen lief in 22 Minuten durch. Vorher war der Jahresabschluss ein Drei-Tages-Event mit manuellem Eingriff, Neustart und der unvermeidlichen nächtlichen Supportnachricht.

Hardware-Empfehlung: Was der Server wirklich braucht

Die Software-Optimierungen verbessern die Situation erheblich — aber sie können eine HDD nicht in eine SSD verwandeln und 8 GB RAM nicht in 64 GB. Hier ist die realistische Hardware-Roadmap:

 

Komponente

Aktuell

Minimum (sofort)

Empfohlen (mittelfristig)

RAM

8 GB

32 GB

64 GB

vCPUs

4

8

16

Data-Volume

HDD (selbe Spindel)

SSD 500 GB (separate)

NVMe 1 TB (separate)

Log-Volume

HDD (selbe Spindel)

SSD 200 GB (separate)

SSD 200 GB (separate)

TempDB-Volume

C: (OS-Spindel)

SSD 100 GB (separate)

NVMe 200 GB (separate)

Netzwerk

100 MBit/s

1 GBit/s

1 GBit/s

Backup

USB-Platte manuell

NAS mit automatischem Job

NAS + Off-Site-Kopie

Tab. 33.17: Hardware-Roadmap BUCHSQL01 — Minimum vs. Empfehlung

 

Der erwartete Effekt des Hardware-Ausbaus allein — also SSD + RAM ohne jede weitere Konfigurationsänderung — wäre bereits dramatisch: PLE von 4,3 s auf 8.000–15.000 s, IO-Read-Latenz von 82 ms auf 0,1–0,4 ms, usp_GetBuchungsliste von 8–14 Minuten auf < 5 Sekunden.

Eine SSD für das Data-Volume kostet unter 200 Euro. Der geschätzte Produktivitätsverlust durch Performance-Probleme über drei Jahre bei 47 Mitarbeitern mit regelmäßigen Wartezeiten übersteigt diesen Betrag um ein Vielfaches. Das ist der unbequeme ROI-Rechner, den man dem Management manchmal vorrechnen muss.

Diagnose-Kästen: Für den nächsten Sparfuchs-Fall

 

Warnung: Symptome: Woran du einen Sparfuchs-Fall erkennst

• Anwendung läuft generell langsam — nicht bei einzelnen Abfragen, sondern global und dauerhaft

• Error Log voll mit Autogrowth-Meldungen (EXEC xp_readerrorlog 0, 1, N'autogrow' liefert hunderte Einträge)

• Transaktionslog unverhältnismäßig groß: log_reuse_wait_desc = LOG_BACKUP in sys.databases

• sp_configure zeigt Priority Boost = 1 oder max server memory = physischem RAM

• PLE unter 300 Sekunden bei normalem Betrieb (sys.dm_os_performance_counters)

• Benutzer berichten von 10–60 Sekunden Wartezeit für triviale Aktionen — kein sporadisches Problem, sondern Dauerzustand

• Freier Speicher auf C: unter 5% — TempDB-Katastrophe jederzeit möglich, wenn TempDB auf C: liegt

• VLF-Anzahl > 1.000 in sys.dm_db_log_info: Zeichen für jahrelangen 1-MB-Autogrowth

 

 

Tipp: So misst du das: Collect-SqlPerf.ps1 als Erste-Hilfe-Tool

Collect-SqlPerf.ps1 (Kapitel 31) erfasst alle relevanten Metriken in einem Durchlauf:

• sp_configure: Alle kritischen Einstellungen auf einen Blick — Priority Boost, max server memory, MAXDOP, Cost Threshold

• sys.dm_io_virtual_file_stats: IO-Latenzen pro Datenbankdatei

• sys.dm_os_wait_stats: Wait Statistics-Zusammenfassung mit Kategorisierung

• sys.dm_os_performance_counters: PLE, Batch Requests/sec, Pages/sec, Processor Queue Length

• sys.databases: Kompatibilitätslevel, Recovery Model, log_reuse_wait_desc

• xp_readerrorlog: Autogrowth-Ereignisse der letzten 24 Stunden

Ein einziger Scriptlauf liefert genug Informationen für die erste Triage — ohne sich durch 20 verschiedene DMVs zu klicken.

 

 

Hintergrund: Typische Fehlinterpretationen beim Sparfuchs-Fall

"Der Server ist zu alt" — Nein. Eine SSD und mehr RAM helfen erheblich, aber ohne Konfigurationsfix bringen sie nur 30–50% der möglichen Verbesserung. Die Konfiguration muss zuerst repariert werden.

"Das Problem ist SQL Server Standard Edition" — Nein. Enterprise hätte zwar ONLINE-Rebuild und mehr Parallelismus-Optionen, aber alle zehn Befunde wären auch mit Enterprise identisch aufgetreten.

"CXPACKET ist das Hauptproblem" — Nein. CXPACKET ist ein Symptom von MAXDOP=0. Das Hauptproblem ist die Kombination aus allen zehn Befunden, die sich gegenseitig verstärken.

"Wir brauchen ein neues System" — Vielleicht langfristig. Aber zuerst die Konfiguration bereinigen. Auf einem neuen System mit derselben Fehlkonfiguration hat man dasselbe Problem nach sechs Monaten.

"Das Transaktionslog ist zu groß, also ist das Recovery Model FULL das Problem" — Nein. FULL ist richtig. Das fehlende Log-Backup ist das Problem. Recovery Model auf SIMPLE zu wechseln würde das Compliance-Risiko dramatisch erhöhen.

 

 

Tipp: Erste Gegenmaßnahmen — was du heute tun kannst (ohne Budget)

1. max server memory korrekt setzen: Physischer RAM minus 2–4 GB → sofort, kein Neustart.

2. Log-Backup-Job einrichten: SOFORT. Täglicher Log-Backup-Job, alle 15 Minuten wäre ideal. Das kostet Zeit, kein Geld.

3. Priority Boost deaktivieren: sp_configure + RECONFIGURE + Neustart beim nächsten Wartungsfenster.

4. Autogrowth-Werte korrigieren: 1-MB-Schritte auf 512 MB oder 1.024 MB erhöhen. Soforteffekt, kein Neustart.

5. Kompatibilitätslevel prüfen: Wenn noch auf 110 — vorher Query Store aktivieren, dann schrittweise auf 150 anheben.

Diese fünf Maßnahmen kosten nichts außer Zeit. Sie beseitigen die größten Risiken und verbessern die Performance messbar — auch ohne Hardware-Upgrade.

 

Abschlussreflexion: Was diese Fallstudie lehrt

Die wichtigste Frage nach einer Analyse wie dieser lautet nicht "Wie reparieren wir das?" — die Antwort haben wir gerade gegeben. Die wichtigste Frage lautet: "Wie hätten wir das verhindert?"

Die Antwort ist unbequem in ihrer Einfachheit: Mit regelmäßigen Health Checks. Ein monatlicher automatisierter Check — Collect-SqlPerf.ps1, 15 Minuten Aufwand — hätte das Autogrowth-Problem spätestens nach drei Monaten identifiziert. Das fehlende Log-Backup nach einer Woche. Die falsche max server memory-Einstellung nach dem ersten Betrieb.

Keiner der zehn Befunde ist schwer zu erkennen. Sie sind alle mit Standard-DMVs und Standard-Tools sichtbar. Das Problem ist nicht die Komplexität der Diagnose — das Problem ist, dass niemand hingeschaut hat. Drei Jahre lang.

Was die Kanzlei nach der Analyse gelernt hat: Monitoring ist keine Aktivität, die man macht, wenn etwas brennt. Monitoring ist eine Aktivität, die verhindert, dass etwas brennt. Der ROI eines monatlichen 15-Minuten-Health-Checks — gemessen in vermiedenen Ausfällen, Datenverlustrisiken und verlorenen Mitarbeiterstunden — ist nicht in Euro zu beziffern, weil er so hoch ist.

Und ja — eine Steuerberatungskanzlei mit Compliance-Anforderungen, die seit 18 Monaten kein Log-Backup gemacht hat, hat ein ernsthaftes Problem jenseits der Performance. Datenschutz, Aufbewahrungspflichten, RPO-Anforderungen nach DSGVO — das sind keine technischen Fragen, das sind rechtliche. Der technische Schaden ist behebbar. Das hätte er werden müssen, bevor es einen Ausfall gab.

 

Praxisbeispiel: Was wurde aus BUCHSQL01?

Drei Monate nach der Analyse läuft BUCHSQL01 stabil. Die Software-Maßnahmen wurden sofort umgesetzt. Die Hardware-Investition (SSD für Data + Log, RAM-Upgrade auf 32 GB) wurde drei Wochen nach der Analyse abgeschlossen.

Der erste Jahresabschluss nach den Maßnahmen lief in 22 Minuten durch. Vorher: 7 Stunden, mit manuellem Eingriff am nächsten Morgen und der unvermeidlichen Nachricht um 03:00 Uhr.

Es gibt jetzt einen monatlichen Health-Check-Bericht, einen Log-Backup-Job (alle 15 Minuten), und einen Wartungsplan für Index-Rebuilds.

Der Server ist noch immer nicht ideal — das RAM-Upgrade auf 64 GB steht noch aus, und SELECT * ist erst in 14 von 23 Prozeduren beseitigt. Aber er ist stabil. Und das ist der erste Schritt.

 

Zusammenfassung

BUCHSQL01 ist das Anti-Pattern-Museum dieses Buchs — kein erfundenes Extrembeispiel, sondern eine Sammlung von Fehlern, die jeder einzeln aus echten Projekten kennt. In dieser Dichte und Kombination: selten, aber nicht einmalig.

Die zehn Befunde — IO-Katastrophe auf einer HDD, 847 Autogrowth-Events in zwei Stunden, Memory-Fehlkonfiguration, Priority Boost, Transaktionslog-Desaster mit 48.312 VLFs, MAXDOP-Kollaps, veralteter Kompatibilitätslevel, Scalar-UDF-Zeitfallen mit 360.000 Aufrufen pro Query, drei Jahre ohne Index-Wartung und SELECT * flächendeckend — haben alle eines gemeinsam: Sie wären mit einem monatlichen Health Check innerhalb von Wochen nach ihrer Entstehung identifiziert worden.

Die Maßnahmen sind in drei Phasen strukturiert. Phase 1 kostet nichts außer Zeit und liefert sofortige Verbesserungen — maximal einen Arbeitstag Aufwand. Phase 2 erfordert eine überschaubare Hardware-Investition mit außerordentlichem ROI. Phase 3 ist Entwicklungsarbeit, die das System langfristig auf ein solides Fundament stellt.

Die wichtigsten Querverweise für dieses Kapitel: Kapitel 5 (Serverkonfiguration, max server memory, MAXDOP), Kapitel 6 (Datenbankeinstellungen, Autogrowth, VLFs, Recovery Model), Kapitel 9 (Wait Statistics, CXPACKET, SOS_SCHEDULER_YIELD, Signal Waits), Kapitel 10 (IO-Performance, IFI, Latenzen messen), Kapitel 11 (Memory Management, PLE-Berechnung), Kapitel 13 (TempDB-Dimensionierung und Platzierung), Kapitel 16 (Kompatibilitätslevel, Cardinality Estimator-Wechsel), Kapitel 17 (Index-Fragmentierung und Wartungsstrategien), Kapitel 26 (Scalar UDFs als Performance-Anti-Pattern), Kapitel 31 (Analyse-Methodik, Collect-SqlPerf.ps1).

Und, wenn du das nächste Mal vor einem Server sitzt, bei dem "läuft doch" die offizielle Statusbeschreibung ist — starte Collect-SqlPerf.ps1. Schau, was zurückkommt. Vielleicht ist alles in Ordnung. Vielleicht findest du einen zukünftigen BUCHSQL01, bevor er zur Fallstudie wird.

Ausblick: Kapitel 34 — Das andere Extrem

BUCHSQL01 zeigt uns die Hardware- und Konfigurations-Katastrophe: unterdimensionierter Server, katastrophale Einstellungen, keine Wartung. Kapitel 34 zeigt das andere Extrem — Trendforge Digital GmbH (Instanz TFSQL01): ein Ferrari mit Anhängerkupplung.

Tadellose Hardware, korrekt konfigurierter SQL Server, vorbildliche Serverkonfiguration. Und trotzdem: Benutzer beschweren sich, Abfragen laufen minutenlang, der Server ist dauerhaft unter Last. Das Problem liegt nicht beim DBA — es liegt bei den Entwicklern. Fehlende Indizes, riesige ineffiziente Queries, ORM-Missbrauch im Industrieformat, N+1-Probleme in Serie. Der DBA sieht die Probleme, kann sie aber nicht allein lösen.

Die Lektion von Kapitel 34: Auch perfekte Hardware kann schlechten Code nicht reparieren. Und manchmal ist die größte Herausforderung nicht die Diagnose — sondern die Kommunikation. Wie man die Evidenz so aufbereitet, dass Entwickler sie nicht ignorieren können, und wie man als DBA konstruktiv mit einem Entwicklungsteam zusammenarbeitet, das ein Performance-Problem hat, das es selbst (noch) nicht sieht — das zeigt Kapitel 34.

 

Abb. 1: IO-Latenz-Vergleich: Sparfuchs vs. Richtwerte

 

Abb. 2: Autogrowth-Timeline: 847 Events in 120 Minuten

 

Abb. 3: Wait Statistics: Sparfuchs & Partner

 

Kapitel 34