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
