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.

SQL Server Architektur & Internals: – 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

SQL Server Architektur & Internals:

Was unter der Haube passiert — und warum das deine Performance betrifft

Warum ein DBA die Architektur verstehen muss

Es gibt zwei Arten von DBAs. Die erste Art tippt Befehle ein, die sie irgendwo gelesen hat, und hofft, dass es klappt. Die zweite Art versteht, was passiert, wenn sie Enter drückt — und kann deshalb Probleme lösen, die die erste Art nur mit einem Schulterzucken quittiert. Dieses Kapitel macht aus dir die zweite Art.

SQL Server ist keine Black Box. Es ist ein hochkomplexes System mit klaren Schichten, definierten Verantwortlichkeiten und vorhersehbarem Verhalten — wenn man weiß, wie es aufgebaut ist. Wer den Buffer Pool versteht, weiß warum die Page Life Expectancy plötzlich einbricht. Wer den Query Optimizer kennt, weiß warum ein Plan Cache voll mit Single-Use-Plans den Server ausbremst. Wer WAL begriffen hat, schläft nachts ruhiger.

Dieses Kapitel legt das Fundament für alle folgenden Kapitel in Teil III und IV. Du wirst hier Begriffe lernen, auf die wir in Kapitel 9 (Wait Statistics), Kapitel 11 (Memory Management), Kapitel 15 (Ausführungspläne) und Kapitel 18 (Parameter Sniffing) immer wieder zurückgreifen. Betrachte es als die technische Einweisung, bevor du das erste Mal das Cockpit betrittst.

 

Hinweis: Voraussetzungen

Dieses Kapitel setzt voraus, dass du weißt, was eine Datenbank ist und grundlegende SQL-Operationen kennst. Wir erklären nicht, was ein SELECT ist — wir erklären, was SQL Server damit macht.

Querverweise: Die NUMA-Architektur aus Kapitel 1 spielt hier eine zentrale Rolle — insbesondere beim SQLOS und der NUMA-Awareness des Buffer Pools. Virtualisierungsaspekte (Kapitel 3) werden in den jeweiligen Abschnitten explizit erwähnt.

 

Die große Übersicht: Relational Engine vs. Storage Engine

SQL Server besteht im Kern aus drei großen Blöcken: der Relational Engine, der Storage Engine und dem SQLOS. Diese drei Teile kommunizieren miteinander, haben aber klar getrennte Zuständigkeiten — und genau diese Trennung erklärt viele Performance-Phänomene.

 

Abb. 4.1: SQL Server Architektur-Überblick: Relational Engine, Storage Engine, SQLOS und Buffer Pool

Die Relational Engine ist zuständig für alles, was mit dem Verarbeiten von Abfragen zu tun hat: Parsen, Optimieren, Planen, Ausführen. Sie nimmt SQL-Text entgegen und gibt Ergebnismengen zurück. Die Storage Engine dagegen ist für Datenspeicherung und -zugriff verantwortlich: B-Tree-Navigation, Locking, Transaktionen, Write-Ahead Logging.

Zwischen beiden steht der Buffer Pool — der gemeinsame Speicher, in dem alle 8-KB-Datenpages cached werden. Die Relational Engine fordert Pages an, die Storage Engine liefert sie (entweder aus dem Buffer Pool oder von Disk), und der SQLOS koordiniert, welcher Worker Thread wann auf welchem Scheduler läuft.

 

Definition: Page (8 KB)

SQL Server liest und schreibt Daten immer in Einheiten von 8 KB — sogenannten Pages. Eine Page enthält typischerweise mehrere Tabellenzeilen. Auch, wenn du nur eine einzige Zeile anforderst, lädt SQL Server die gesamte 8-KB-Page in den Buffer Pool.

Das ist kein Designfehler, sondern Absicht: Nachfolgende Zugriffe auf Zeilen derselben Page sind kostenlos, weil die Page bereits im RAM liegt. Dieses Prinzip heißt "Locality of Reference" — und es ist der Grund, warum sequenzieller Zugriff fast immer schneller ist als Einzelzeilen-Zugriff.

 

Datenseiten und Extents: Die physische Struktur der Daten

Um wirklich zu verstehen, wie SQL Server mit Daten umgeht, muss man eine Ebene tiefer gehen: auf die physische Struktur der Datendateien. Alles — jede Tabelle, jeder Index, jede temporäre Struktur — ist in Pages und Extents organisiert. Wer das nicht kennt, stolpert irgendwann über Autogrowth-Probleme, Fragmentierung oder TempDB-Contention, ohne zu verstehen, warum.

Aufbau einer 8-KB-Page

Jede Page in SQL Server ist exakt 8.192 Bytes groß — aufgeteilt in drei logische Bereiche: den Page Header, den Datenbereich und das Row Offset Array am Ende der Page.

 

Bereich

Größe

Inhalt

Page Header

96 Bytes

Page-ID, Typ, Prüfsumme, LSN (Log Sequence Number), freier Speicher

Datenbereich

bis 8.060 Bytes

Die eigentlichen Zeilen (Rows) oder Index-Einträge

Row Offset Array

variabel

Zeiger auf den Anfang jeder Zeile auf der Page — rückwärts am Ende gespeichert

Tab. 4.1: Aufbau einer SQL Server Data Page

 

Der Page Header enthält unter anderem die LSN des letzten Redo-Log-Eintrags, der diese Page verändert hat — das ist die Verbindung zwischen Buffer Pool und Transaction Log. Wenn SQL Server nach einem Absturz eine Page wiederherstellt, vergleicht er diese LSN mit dem Log, um festzustellen, ob die Page aktuell oder veraltet ist.

Das Row Offset Array wächst von hinten nach vorne. Neue Zeilen landen im Datenbereich von vorne nach hinten; neue Offset-Einträge kommen vom Ende der Page. Wenn vorn und hinten sich treffen — sprich: die Page ist voll — braucht SQL Server einen Page Split. Page Splits sind teuer: Eine neue Page muss alloziert, Daten umkopiert und Indizes aktualisiert werden. Hohe Fragmentierung entsteht durch viele Page Splits.

Extents: Acht Pages als Allokationseinheit

Einzelne Pages werden nicht einzeln vom Betriebssystem angefordert — das wäre zu ineffizient. SQL Server gruppiert Pages in Extents: Ein Extent ist immer genau acht aufeinanderfolgende Pages (8 × 8 KB = 64 KB). Extents sind die Mindesteinheit für Speicherallokation auf Dateiebene.

SQL Server kennt zwei Arten von Extents: Uniform Extents und Mixed Extents. Ein Uniform Extent gehört vollständig einem einzelnen Objekt — alle acht Pages sind für dieselbe Tabelle oder denselben Index reserviert. Ein Mixed Extent hingegen kann Pages für bis zu acht verschiedene Objekte enthalten. Das klingt nach einem Detail, ist aber für Performance-Diagnosen relevant: TempDB-Contention entsteht oft durch Streit um Mixed Extents — genau das analysieren wir in Kapitel 13.

 

Extent-Typ

Zugehörigkeit

Wann genutzt

Performance-Aspekt

Uniform Extent

Gehört einem Objekt vollständig

Ab der 9. Page eines Objekts

Weniger Verwaltungsaufwand, weniger Contention

Mixed Extent

Bis zu 8 Objekte teilen sich die Pages

Für die ersten 8 Pages eines neuen Objekts

Höherer Verwaltungsaufwand, potenzielle Contention

Tab. 4.2: Uniform vs. Mixed Extents

 

IAM, GAM und SGAM: Die Allokations-Buchhaltung

SQL Server führt Buch über alle allozierten Extents mithilfe spezieller System-Pages:

  • IAM (Index Allocation Map): Jedes Objekt (Tabelle, Index) hat eine oder mehrere IAM-Pages, die alle Extents auflisten, die diesem Objekt gehören. Wenn du sys.dm_db_database_page_allocations ausführst, liest SQL Server intern die IAM-Pages.
  • GAM (Global Allocation Map): Eine GAM-Page pro Dateigruppe zeigt an, welche Extents frei sind (Bit = 1) und welche belegt sind (Bit = 0). Eine GAM-Page deckt 64.000 Extents ab — das entspricht etwa 4 GB Dateidaten.
  • SGAM (Shared Global Allocation Map): Zeigt an, welche Extents Mixed Extents sind und noch freie Pages haben. Neue kleine Objekte suchen hier nach Platz.
  • Warum ist das praxisrelevant? Weil bei hoher TempDB-Aktivität — vielen parallelen Sessions, die gleichzeitig temporäre Tabellen anlegen — alle Sessions auf dieselben GAM- und SGAM-Pages zugreifen müssen. Das erzeugt PAGELATCH-Waits auf genau diesen System-Pages: PFS_PAGE, GAM_PAGE, SGAM_PAGE. Das Symptom: viele parallele Sessions verlangsamen sich gegenseitig, obwohl eigentlich genug Ressourcen vorhanden sind. Lösung: TempDB mit mehreren Datendateien (Kapitel 13).

    Page-Nutzung diagnostizieren: sys.dm_db_database_page_allocations

    Ab SQL Server 2012 gibt es eine DMF, die dir erlaubt, die Allokation von Pages auf Objekt-Ebene zu sehen — ohne DBCC-Befehle oder undokumentierte Syscalls. Sehr praktisch für die Diagnose von Fragmentierung oder Objekt-Größen:

    -- Page-Allokation für eine Tabelle abfragen
    -- Zeigt: welche Pages gehören zu welchem Objekt, wie fragmentiert ist es?
    -- Achtung: Auf großen Datenbanken kann das viele Zeilen zurückgeben — am besten auf konkrete Objekte filtern
    SELECT
        object_name(object_id)   AS tabelle,
        index_id,
        allocation_unit_type_desc,
        page_type_desc,
        page_free_space_percent,  -- Wie voll ist die Page?
        is_allocated,
        is_mixed_page_allocation  -- Mixed Extent?
    FROM sys.dm_db_database_page_allocations(
        DB_ID(),           -- aktuelle Datenbank
        OBJECT_ID('dbo.Bestellungen'),  -- nur diese Tabelle
        NULL,              -- alle Index-IDs
        NULL,              -- alle Partition-IDs
        'DETAILED'         -- Details anfordern (kostet mehr Zeit)
    )
    ORDER BY page_type_desc, page_free_space_percent;

     

    -- Schneller Überblick: Wie viele Pages nutzt jede Tabelle?
    SELECT
        OBJECT_NAME(i.object_id) AS tabelle,
        i.name                   AS index_name,
        SUM(a.total_pages)       AS seiten_gesamt,
        SUM(a.total_pages) * 8   AS kb_gesamt
    FROM sys.indexes i
    JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
    JOIN sys.allocation_units a ON p.partition_id = a.container_id
    WHERE OBJECT_NAME(i.object_id) IS NOT NULL
    GROUP BY i.object_id, i.name
    ORDER BY seiten_gesamt DESC;

    Diese Abfrage ist besonders nützlich, wenn du verstehen willst, warum eine Tabelle so viel Speicherplatz belegt — oder warum ein Index trotz geringer Datenmenge viele Pages beansprucht. Hohe page_free_space_percent-Werte in Kombination mit vielen Pages deuten auf starke Fragmentierung hin: viele halbvolle Pages, viele Page Splits in der Vergangenheit. Das ist ein starkes Signal, dass der betreffende Index gewartet werden sollte — mehr dazu in Kapitel 17.

    Der Buffer Pool: SQL Servers Herz aus RAM

    Der Buffer Pool ist der wichtigste Speicherbereich von SQL Server. Hier cached SQL Server alle Datenpages — sowohl unveränderte (clean) als auch modifizierte (dirty). Das Ziel ist simpel: Je mehr Daten im RAM liegen, desto seltener muss SQL Server auf Disk zugreifen. Und Disk-Zugriffe sind teuer. Selbst auf moderner NVMe-SSD kostet ein zufälliger Lesezugriff 50–150 Mikrosekunden; im RAM sind es Nanosekunden — Faktor 1.000.

    Der Buffer Pool ist NUMA-aware: Er hält für jeden NUMA-Node einen eigenen Speicherbereich vor, damit Threads, die auf Node 0 laufen, bevorzugt auf Pages aus dem RAM von Node 0 zugreifen. Wenn du auf einem Server mit mehreren NUMA-Nodes arbeitest, ist das keine Kleinigkeit — Remote-NUMA-Zugriffe können die Latenzen vervielfachen. Mehr dazu in Kapitel 1 (NUMA-Topologie) und Kapitel 11 (Memory Management).

    Page-Lebenszyklus: Einlesen, Modifizieren, Schreiben

    Eine Page durchläuft im Buffer Pool einen definierten Lebenszyklus. Verstehe diesen Zyklus, und du verstehst gleichzeitig Checkpoint-Verhalten, Dirty-Page-Mengen und IO-Muster auf deinem Server.

     

    Abb. 4.2: Buffer Pool Page-Lifecycle: Einlesen, Dirty werden, Checkpoint und Lazy Writer

    Der Zyklus im Überblick: SQL Server liest eine Page von Disk in den Buffer Pool (Physical Read). Dort wird die Page gecached und verbleibt so lange im RAM wie möglich. Wenn eine Transaktion die Page modifiziert, wird sie als "dirty" markiert — sie enthält jetzt Daten, die noch nicht auf Disk persistiert sind. Irgendwann schreibt entweder der Lazy Writer oder ein Checkpoint die Dirty Page auf Disk (Clean Write). Danach ist die Page wieder clean.

     

    Hintergrund: LRU-2 Seitenersetzung

    Wenn der Buffer Pool voll ist und neue Pages eingelesen werden müssen, braucht SQL Server eine Strategie, welche alten Pages verdrängt werden. SQL Server verwendet einen LRU-2-Algorithmus (Least Recently Used, zweifacher Zugriff).

    Im Gegensatz zu einfachem LRU werden Pages nur dann als "hot" eingestuft, wenn sie mindestens zweimal zugegriffen wurden. Eine Page die einmal gelesen und nie wieder gebraucht wird (klassischer Full Table Scan!) verdrängt nicht unnötig wertvolle Pages aus dem Cache.

    Das erklärt, warum ein großer Table Scan den Buffer Pool "kalt" schießen kann — er liest massenhaft Pages einmalig ein und verdrängt damit Pages, auf die regelmäßig zugegriffen wird. Dieses Phänomen heißt "Buffer Pool Pollution".

     

    PLE — Page Life Expectancy: Der Pulsschlag des Buffer Pools

    Die Page Life Expectancy (PLE) misst, wie lange eine Page im Durchschnitt im Buffer Pool verbleibt — in Sekunden. Sie ist einer der wichtigsten Indikatoren für den RAM-Gesundheitszustand deines SQL Servers.

    Die traditionelle Faustregel lautet: PLE > 300 Sekunden ist akzeptabel. Diese Zahl stammt aus einer Zeit, als SQL Server typischerweise auf Servern mit 4–8 GB RAM lief. Heute, mit 128 oder 256 GB RAM, sollte die PLE deutlich höher sein — 1.000 bis 4.000 Sekunden sind auf gut ausgestatteten Servern normal. Bei Musterwerk GmbH (Instanz MWSQL01), einem typisch ausgestatteten Produktionsserver mit 64 GB RAM, liegt die PLE konstant bei etwa 2.800 Sekunden — ein gesunder Wert, der zeigt, dass der Buffer Pool die wichtigsten Working-Sets hält.

    Wichtiger als die absolute Zahl ist der Trend: Wenn die PLE innerhalb von Stunden von 3.000 auf 200 Sekunden fällt, läuft etwas Konkretes schief — ein neuer Job, eine schlecht optimierte Abfrage, oder ein Batchprozess der den Buffer Pool leert. Die Baseline ist entscheidend — mehr dazu in Kapitel 9.

    -- PLE pro NUMA-Node abfragen
    -- Wichtig: Auf NUMA-Systemen gibt es einen Wert pro Node!
    -- Nur den Gesamtwert (NUMA-Node 999) zu schauen führt in die Irre,
    -- wenn ein einzelner Node unter Druck steht
    SELECT
        object_name,
        counter_name,
        instance_name,
        cntr_value AS ple_sekunden
    FROM sys.dm_os_performance_counters
    WHERE counter_name = 'Page life expectancy'
      AND object_name LIKE '%Buffer Manager%'
      -- OR object_name LIKE '%Buffer Node%'  -- für NUMA-Nodes einzeln
    ORDER BY instance_name;

    Auf einem System mit vier NUMA-Nodes kann die PLE auf Node 0 bei 4.000 Sekunden stehen, während Node 2 nur 150 Sekunden erreicht — ein klares Indiz für ungleichmäßige Speichernutzung oder ein NUMA-Konfigurationsproblem. Kapitel 11 geht auf genau dieses Szenario ein.

    Lazy Writer und Checkpoint: Wer schreibt wann auf Disk?

    SQL Server nutzt zwei Mechanismen, um Dirty Pages auf Disk zu schreiben. Beide haben unterschiedliche Trigger und Prioritäten:

     

    Mechanismus

    Trigger

    Ziel

    IO-Typ

    Lazy Writer

    Buffer Pool läuft voll (wenig freie Pages)

    Speicher freigeben

    Schreibt Dirty Pages aus; verworfen, wenn clean

    Automatic Checkpoint

    Log-Größe oder Timeout (Recovery Interval)

    Crash-Recovery-Zeit begrenzen

    Schreibt alle Dirty Pages seit letztem Checkpoint

    Indirect Checkpoint

    Dirty Page Count überschreitet Ziel (= 0,9 × Ziel-Recovery-Zeit)

    Gleichmäßiges IO, keine Spitzen

    Kontinuierlich, vorhersehbar

    Manual Checkpoint

    CHECKPOINT T-SQL-Befehl

    Manuell, z.B. vor Maintenance

    On demand

    Tab. 4.3: Checkpoint-Mechanismen im Vergleich

     

    Automatic Checkpoint war lange der Standard. Das Problem: Wenn die Recovery-Interval-Grenze erreicht wird, startet ein Checkpoint und schreibt u.U. riesige Mengen an Dirty Pages auf einen Schlag auf Disk — ein IO-Spike, der andere Operationen verlangsamt. Ab SQL Server 2012 ist Indirect Checkpoint der empfohlene Weg, weil er IO gleichmäßig verteilt.

     

    Tipp: Indirect Checkpoint aktivieren

    Ab SQL Server 2016 ist Indirect Checkpoint für neue Datenbanken Standard (Target Recovery Time = 60 Sekunden). Für ältere Datenbanken oder, wenn das Verhalten des Auto-Checkpoints bekannt problematisch ist, empfiehlt sich:

    ALTER DATABASE [MeineDB] SET TARGET_RECOVERY_TIME = 60 SECONDS;

    Das hält Dirty-Page-Counts niedrig und IO-Muster gleichmäßig. Ausnahme: TEMPDB — dort ist Indirect Checkpoint seit SQL 2016 immer aktiv, egal was du einstellst.

     

    Buffer Pool Extensions (BPE): Wenn RAM nicht reicht

    Buffer Pool Extensions erlauben es, eine schnelle SSD als Erweiterung des Buffer Pools zu nutzen. Clean Pages, die aus dem RAM-Buffer-Pool verdrängt werden, landen dann auf der SSD statt vollständig verworfen zu werden — ein zweistufiger Cache.

    In der Praxis ist BPE selten die richtige Antwort. Wenn der Buffer Pool unter Druck steht, ist die echte Lösung mehr RAM. BPE kann sinnvoll sein, wenn du NVMe-SSDs hast und der Server kurzfristig nicht aufgerüstet werden kann — aber auf herkömmlichen SATA-SSDs ist der Vorteil oft marginal, während der Verwaltungsaufwand steigt. In virtualisierten Umgebungen (Kapitel 3) ist BPE meist kontraproduktiv.

    Der Lock Manager: Gleichzeitigkeit und Konsistenz

    Mehrere Benutzer greifen gleichzeitig auf dieselben Daten zu. Das klingt nach einem Problem — und ohne den Lock Manager wäre es auch eines. Der Lock Manager ist die Instanz, die dafür sorgt, dass keine zwei Transaktionen sich gegenseitig in die Quere kommen, ohne es zu merken. Er ist einfach ausgedrückt der Verkehrspolizist der Datenkonsistenz.

    Locks sind temporäre Ressourcen, die SQL Server während einer Transaktion hält und am Ende wieder freigibt. Der Lock Manager koordiniert, welche Locks kompatibel sind, wer warten muss, und wann eine Situation eskaliert oder zum Deadlock wird. Das vollständige Bild zu Blocking und Deadlocks findest du in Kapitel 14 — hier legen wir das konzeptionelle Fundament.

    Lock-Granularität: Von Zeile bis zur Datenbank

    SQL Server kann Locks auf verschiedenen Granularitätsebenen vergeben. Grober Granulat bedeutet weniger Verwaltungsaufwand, aber mehr Konflikte. Feiner Granulat bedeutet mehr Parallelität, aber mehr Overhead für den Lock Manager.

     

    Granularität

    Beschreibung

    Wann sinnvoll

    Overhead

    RID (Row ID)

    Einzelne Zeile in einem Heap (ohne Clustered Index)

    Sehr gezielter Einzel-Zeilen-Zugriff

    Hoch (viele Lock-Einträge)

    KEY

    Einzelner Schlüsselwert in einem Index

    Standard für Index-Abfragen

    Mittel

    PAGE

    Gesamte 8-KB-Datenseite (alle Zeilen)

    Bei Bereichsabfragen, häufig durch Eskalation

    Niedrig

    EXTENT

    Acht aufeinanderfolgende Pages

    Nur intern bei Allokation

    Sehr niedrig

    TABLE (HoBT)

    Gesamte Tabelle

    Lock Escalation, kleine Tabellen, DDL

    Minimal

    DATABASE

    Gesamte Datenbank

    Backup, Restore, Offline-Operationen

    Minimal

    Tab. 4.4: Lock-Granularitäten in SQL Server

     

    Lock-Typen: Was darf gleichzeitig, was nicht?

    Nicht jeder Lock ist gleich. SQL Server kennt verschiedene Lock-Modi, die unterschiedlich kompatibel miteinander sind. Das Grundprinzip: Lesende Operationen (Shared Locks) sind untereinander kompatibel — viele Leser gleichzeitig sind kein Problem. Schreibende Operationen (Exclusive Locks) sind mit niemandem kompatibel.

     

    Lock-Typ

    Abkürzung

    Beschreibung

    Typischer Auslöser

    Shared

    S

    Für Lesezugriffe — mehrere S-Locks gleichzeitig möglich

    SELECT

    Exclusive

    X

    Für Schreibzugriffe — kein anderer Lock gleichzeitig

    INSERT, UPDATE, DELETE

    Update

    U

    Übergang vor Exclusive — verhindert Deadlock-Muster

    UPDATE (vor dem eigentlichen Write)

    Intent Shared

    IS

    Ankündigung: "Ich werde Shared Locks auf untergeordneter Ebene nehmen"

    SELECT mit feinerer Granularität

    Intent Exclusive

    IX

    Ankündigung: "Ich werde Exclusive Locks auf untergeordneter Ebene nehmen"

    DML auf Tabellenebene

    Shared + Intent Exclusive

    SIX

    Tabelle gesperrt für Lesen; einzelne Rows exklusiv

    Selten, z.B. bei Cursor-Operationen

    Tab. 4.5: Lock-Typen und ihre Bedeutung

     

    Intent Locks klingen zunächst verwirrend, sind aber ein elegantes Konzept: Bevor SQL Server einen feingranularen Lock (z.B. auf Row-Ebene) hält, setzt er auf allen übergeordneten Ebenen (Page, Table) einen Intent Lock. Das erlaubt dem Lock Manager, sehr schnell zu prüfen, ob eine grobe Operation (z.B. ein Table Lock) mit laufenden feinen Operationen kompatibel ist — ohne jede einzelne Row-Lock zu prüfen.

    Lock-Kompatibilitätsmatrix

    Diese Matrix zeigt, welche Lock-Typen gleichzeitig auf derselben Ressource gehalten werden können ( = kompatibel, = blockiert):

     

    Vorhanden ↓ / Angefordert →

    IS

    S

    U

    IX

    SIX

    X

    IS (Intent Shared)

    S (Shared)

    U (Update)

    IX (Intent Exclusive)

    SIX (Shared+Intent Excl.)

    X (Exclusive)

    Tab. 4.6: Lock-Kompatibilitätsmatrix

     

    Lock Escalation: Wenn viele kleine Locks zu einem großen werden

    Lock-Granularität hat ihren Preis: Hält eine Transaktion sehr viele feine Locks (z.B. Tausende Row-Locks), verbraucht das Speicher und Verwaltungsaufwand. SQL Server greift dann zu einem drastischen Mittel: Lock Escalation. Alle feinen Locks werden durch einen einzigen Table Lock ersetzt. Weniger Overhead — aber deutlich mehr Blockierung anderer Transaktionen.

    Lock Escalation wird ausgelöst, wenn eine Transaktion auf einer einzelnen Tabelle mehr als 5.000 Lock-Einheiten hält. Das ist keine konfigurierbare Schwelle (es sei denn, du deaktivierst Escalation per ALTER TABLE), sondern ein fester interner Wert. Das Problem: Wenn eine Massenoperation (z.B. ein Batch-Update von 100.000 Zeilen) Lock Escalation auslöst, ist die gesamte Tabelle für alle anderen Schreiber und Leser (ohne READ_COMMITTED_SNAPSHOT) gesperrt, bis die Transaktion abgeschlossen ist.

     

    Tipp: Lock Escalation vermeiden

    Zwei Strategien gegen unerwünschte Lock Escalation:

    1. Batching: Große Operationen in kleinere Batches aufteilen, z.B. UPDATE in Tranchen von 1.000 Zeilen. So wird die Eskalationsschwelle nie erreicht.

    2. RCSI aktivieren: Read Committed Snapshot Isolation eliminiert Shared Locks bei Lesern vollständig — Leser blockieren Schreiber nicht mehr und umgekehrt. Kapitel 14 und 29 behandeln das im Detail.

    ALTER TABLE dbo.Bestellungen SET (LOCK_ESCALATION = DISABLE); — Vorsicht: Nur, wenn du weißt, was du tust!

     

    Aktive Locks diagnostizieren: sys.dm_tran_locks

    sys.dm_tran_locks gibt dir einen Echtzeit-Überblick über alle aktuell gehaltenen Locks auf dem Server. Das ist die erste Anlaufstelle, wenn du Blocking-Probleme vermutest oder verstehen willst, wer was blockiert.

    -- Aktive Locks und Blocking-Ketten anzeigen
    -- Zeigt: wer wartet, wer blockiert, welche Ressource, welcher Lock-Typ
    SELECT
        tl.request_session_id       AS session_id,
        tl.resource_type,           -- ROW, PAGE, OBJECT, DATABASE...
        tl.resource_database_id,
        DB_NAME(tl.resource_database_id) AS datenbank,
        tl.resource_description,    -- Konkrete Ressource (z.B. Page-ID)
        tl.request_mode,            -- S, X, U, IS, IX...
        tl.request_status,          -- GRANT = gehalten, WAIT = wartend
        tl.request_owner_type,      -- TRANSACTION, SESSION...
        er.blocking_session_id,     -- Wer blockiert diese Session?
        er.wait_type,               -- Was ist der aktuelle Wait?
        er.wait_time / 1000.0 AS wartezeit_sekunden
    FROM sys.dm_tran_locks tl
    LEFT JOIN sys.dm_exec_requests er
        ON tl.request_session_id = er.session_id
    WHERE tl.request_status = 'WAIT'       -- Nur wartende Anfragen
       OR er.blocking_session_id IS NOT NULL -- Oder blockierte Sessions
    ORDER BY er.wait_time DESC;

     

    -- Zusammenfassung: Wie viele Locks pro Typ?
    SELECT
        resource_type,
        request_mode,
        request_status,
        COUNT(*) AS anzahl
    FROM sys.dm_tran_locks
    GROUP BY resource_type, request_mode, request_status
    ORDER BY anzahl DESC;

    Wir gehen in Kapitel 14 tief in Blocking und Deadlocks ein — mit vollständigen Diagnose-Workflows und dem Deadlock-Graphen. Hier reicht das Grundverständnis: Lock Manager koordiniert Parallelität, Lock Escalation kann Probleme verursachen, und sys.dm_tran_locks ist dein erster Blick in die aktuelle Locking-Situation.

    SQLOS: SQL Servers eigenes Betriebssystem

    SQL Server hat kein normales Verhältnis zum Windows-Scheduler. Während andere Anwendungen brav darauf warten, dass Windows ihnen CPU-Zeit zuteilt, hat SQL Server ein eigenes Scheduling-System eingebaut: den SQLOS (SQL Server Operating System). Kein Marketing-Begriff, sondern eine technische Realität.

    Der Grund ist historisch und praktisch: SQL Server braucht extrem feingranulare Kontrolle über seine Worker Threads, weit mehr als ein generischer OS-Scheduler bieten kann. Und er braucht NUMA-Awareness, die Windows nicht automatisch liefert.

    Scheduler, Worker Threads und Tasks

    Das SQLOS-Modell hat drei Ebenen: Schedulers, Worker Threads und Tasks.

     

    Konzept

    Beschreibung

    Anzahl / Limit

    Scheduler

    Ein Scheduler pro logische CPU. Er verwaltet Worker Threads, die auf dieser CPU laufen.

    1 pro logische CPU (Standard)

    Worker Thread

    Ein Thread aus dem Thread-Pool, der Tasks ausführt. Wartet kooperativ, wenn er blockiert ist.

    max worker threads konfigurierbar

    Task

    Kleinste Arbeitseinheit — eine konkrete Aufgabe (z.B. Parallel-Worker für eine Abfrage)

    Beliebig viele pro Request

    Tab. 4.7: SQLOS-Abstraktionsebenen

     

    Jeder Scheduler verwaltet eine Queue von Worker Threads, die darauf warten, Tasks auszuführen. Ein Worker Thread nimmt sich einen Task, führt ihn aus — und gibt den Scheduler freiwillig wieder frei, wenn er auf etwas warten muss (IO, Lock, Signal). Das ist kooperatives Scheduling.

    Kooperatives Scheduling: Warum SQL Server nicht präemptiv ist

    Windows-Threads werden präemptiv gescheduled: Das OS kann einen Thread jederzeit unterbrechen und einem anderen die CPU geben. SQL Server macht das anders. Worker Threads laufen kooperativ: Sie geben die CPU freiwillig ab, wenn sie warten müssen. Das erlaubt SQL Server, Context-Switches extrem effizient zu verwalten und vermeidet, dass Worker Threads an ungünstigen Stellen unterbrochen werden.

    Das hat eine wichtige Konsequenz: Wenn ein Worker Thread nicht kooperiert — weil er z.B. in einer langen berechnungsintensiven Operation steckt oder in einem externen COM-Aufruf hängt — blockiert er seinen Scheduler. SQL Server erkennt das (nach einigen Sekunden) und meldet es als "Non-Yielding Scheduler"-Fehler. Das ist einer der wenigen Fälle, wo SQL Server tatsächlich unstabil werden kann.

     

    Warnung: Non-Yielding Scheduler

    Taucht im SQL Server Error Log "Non-Yielding Scheduler" auf, ist das ein ernstes Warnsignal. Es bedeutet, dass ein Worker Thread den Scheduler für zu lange Zeit blockiert.

    Typische Ursachen: Schlecht geschriebener CLR-Code, externe Calls via Linked Server, bestimmte interne Operationen bei extremer CPU-Auslastung. In seltenen Fällen deutet es auf Bugs in SQL Server selbst hin.

    Der Server kann in dieser Situation hängen oder abstürzen. Sofort ins Error Log schauen und die Ursache identifizieren — vor allem welche Datenbank und welches Query betroffen war.

     

    NUMA-Awareness des SQLOS

    Jeder Scheduler in SQL Server ist einem NUMA-Node zugeordnet. Der SQLOS versucht, Threads so zu schedulen, dass sie auf demselben Node laufen wie die Daten, auf die sie zugreifen — minimale Remote-Speicherzugriffe, maximale Performance.

    Wenn NUMA falsch konfiguriert ist (z.B. Soft-NUMA statt echter NUMA-Topologie, falsche Affinity-Masks), kann SQL Server den Vorteil von NUMA-Awareness verlieren. Das haben wir in Kapitel 1 ausführlich besprochen und in Kapitel 11 schauen wir uns an, wie man das mit DMVs diagnostiziert.

    Resource Governor: Wer bekommt was, und wer geht leer aus

    Der Resource Governor ist SQL Servers eingebauter Traffic-Cop für CPU und Memory. Ohne ihn gilt das Gesetz des Dschungels: Wer am lautesten schreit (sprich: wer die teuerste Abfrage hat), bekommt die meisten Ressourcen. Der Resource Governor ändert das — er erlaubt, verschiedenen Workloads definierte Ressourcen-Budgets zuzuweisen und durchzusetzen.

    Klingt nach einem Feature für große Rechenzentren? Ist es auch. Aber es gibt kleinere Szenarien, in denen der Resource Governor echten Wert liefert — und die kennen zu nicht, kann teuer werden.

    Architektur: Resource Pools und Workload Groups

    Der Resource Governor hat zwei Konfigurationsebenen: Resource Pools und Workload Groups.

    Ein Resource Pool definiert die Ressourcengrenzen auf Systemebene: minimale und maximale CPU-Prozente, minimaler und maximaler Arbeitsspeicher. SQL Server liefert zwei eingebaute Pools: default (für alles, was nicht explizit zugewiesen wurde) und internal (für interne SQL Server-Prozesse — nicht anfassbar).

    Innerhalb eines Pools können mehrere Workload Groups existieren. Eine Workload Group fügt eine weitere Feingranularität hinzu: Innerhalb des Pool-Budgets kann sie zusätzlich eigene Grenzen für gleichzeitige Anfragen, maximale Abfragekosten (aus Sicht des Optimizers) oder Memory Grants definieren. Jede eingehende Session landet — basierend auf einer Classifier Function — in genau einer Workload Group.

     

    Konfigurationselement

    Beschreibung

    Typische Einstellungen

    Resource Pool

    Systemweite CPU/RAM-Grenzen

    MIN_CPU_PERCENT, MAX_CPU_PERCENT, MIN_MEMORY_PERCENT, MAX_MEMORY_PERCENT

    Workload Group

    Grenzen innerhalb eines Pools

    MAX_DOP, REQUEST_MAX_CPU_TIME_SEC, REQUEST_MAX_MEMORY_GRANT_PERCENT

    Classifier Function

    T-SQL-Funktion, die jede neue Session einer Workload Group zuordnet

    Basierend auf Login, Host, App-Name, Tageszeit…

    Tab. 4.8: Resource Governor Komponenten

     

    Wann setzt man den Resource Governor ein?

    Der Resource Governor ist kein Allheilmittel — und er kostet selbst Overhead. Aber in bestimmten Situationen ist er unverzichtbar:

  • Gemischte Workloads auf einer Instanz: OLTP und Reporting laufen auf demselben Server. Ohne Steuerung frisst der nächste große Report den Buffer Pool leer und killt die OLTP-Antwortzeiten. Mit Resource Governor bekommt der Report-Pool ein CPU-Maximum (z.B. 40%) und ein Memory-Limit (z.B. 20%).
  • Ad-hoc-Abfragen aus Self-Service-Tools: Power BI Direct Query, Excel-Verbindungen, ad-hoc SQL von Entwicklern. Ohne Limits kann eine schlecht geschriebene Abfrage den Server lahmlegen. Mit Resource Governor: MAX_DOP = 2, REQUEST_MAX_CPU_TIME_SEC = 30 für diese Workload Group.
  • Maintenance Jobs: Index-Rebuilds und Statistik-Updates können CPU-intensiv sein. In einen eigenen Pool mit niedrigem CPU-Anteil stecken, damit sie nachts nicht mit regulärem Betrieb konkurrieren.
  • Multi-Tenant-Szenarien: Mehrere Kunden auf einer Instanz — jeder bekommt seinen eigenen Pool. Begrenzt den "noisy neighbor"-Effekt.
  • Für Memory Grants ist der Resource Governor besonders relevant — er kann begrenzen, wie viel RAM eine einzelne Abfrage für Sort- und Hash-Operationen anfordern darf. Das verhindert, dass ein einzelner schlecht optimierter Query den kompletten verfügbaren Arbeitsspeicher für Memory Grants blockiert und alle anderen Abfragen in die Warteschlange zwingt. Kapitel 12 (Memory Grants und Spills) baut auf diesem Konzept auf.

    -- Einfaches Resource Governor Setup: Reporting vs. OLTP trennen

     

    -- Schritt 1: Resource Pools anlegen
    CREATE RESOURCE POOL ReportingPool
        WITH (MIN_CPU_PERCENT = 0,    -- kein Minimum — Report wartet, wenn nötig
              MAX_CPU_PERCENT = 40,   -- höchstens 40% CPU für Reports
              MIN_MEMORY_PERCENT = 0,
              MAX_MEMORY_PERCENT = 30);  -- höchstens 30% Memory Grants

     

    CREATE RESOURCE POOL OLTPPool
        WITH (MIN_CPU_PERCENT = 30,   -- mindestens 30% CPU garantiert für OLTP
              MAX_CPU_PERCENT = 100,
              MIN_MEMORY_PERCENT = 0,
              MAX_MEMORY_PERCENT = 80);

     

    -- Schritt 2: Workload Groups anlegen
    CREATE WORKLOAD GROUP ReportingGroup
        WITH (MAX_DOP = 4,                        -- maximal 4 Kerne pro Report
              REQUEST_MAX_CPU_TIME_SEC = 120)      -- Report wird nach 2 Min. abgebrochen
        USING ReportingPool;

     

    CREATE WORKLOAD GROUP OLTPGroup
        USING OLTPPool;

     

    -- Schritt 3: Classifier Function erstellen
    -- Einfaches Beispiel: bestimmten Login in Report-Pool leiten
    CREATE FUNCTION dbo.ResourceClassifier()
    RETURNS SYSNAME WITH SCHEMABINDING
    AS BEGIN
        -- Reporting-Logins in die Reporting-Gruppe leiten
        IF SUSER_NAME() IN ('ReportUser', 'PowerBIService')
            RETURN 'ReportingGroup';
        RETURN 'OLTPGroup';  -- Standard: OLTP-Gruppe
    END;

     

    -- Schritt 4: Resource Governor aktivieren
    ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = dbo.ResourceClassifier);
    ALTER RESOURCE GOVERNOR RECONFIGURE;

    Einen vollständigen Exkurs zum Resource Governor und wie er Memory Grants begrenzt findest du in Kapitel 12. Dort zeigen wir auch, wie man mit dem Resource Governor gezielt gegen "Memory Grant Waits" (RESOURCE_SEMAPHORE) vorgeht — einem der häufigsten Probleme auf Servern mit gemischten Workloads.

    Query Processing: Von SQL-Text zum Ergebnisset

    Eine SQL-Abfrage legt eine lange Strecke zurück, bevor das erste Ergebnisset zurückkommt. Dieser Weg ist nicht zufällig — er folgt einer festen Pipeline, und jede Stufe kann Probleme verursachen, die sich als Performance-Problem manifestieren.

     

    Phase

    Aufgabe

    Mögliche Probleme

    Parse

    SQL-Text → Query Tree. Syntaxprüfung, Tokenisierung.

    Syntaxfehler (selten Performance-relevant)

    Algebrize

    Objekte auflösen: Tabellen, Indizes, Spalten. Permissions prüfen.

    Compilation-Lock bei Schemaänderungen

    Optimize

    Kostenbasierte Planauswahl. Statistiken lesen, Alternativen bewerten.

    Schlechte Statistiken → schlechter Plan → CPU-Explosion

    Execute

    Plan ausführen. Pages anfordern, Locks halten, Results zurückliefern.

    IO-Waits, Lock-Waits, Memory Grants

    Tab. 4.9: Query Processing Pipeline

     

    Plan Cache: Das Gedächtnis des Optimizers

    Der Query Optimizer ist teuer. Eine Abfrage zu optimieren kann hunderte von Millisekunden dauern — für komplexe Queries sogar Sekunden. Deshalb cached SQL Server fertige Ausführungspläne im Plan Cache (auch Procedure Cache genannt). Kommt dieselbe Abfrage wieder, wird der gecachte Plan wiederverwendet — Kompilierungszeit: null.

    Das klingt wie eine gute Sache, und meistens ist es das auch. Aber der Plan Cache ist gleichzeitig eine der häufigsten Problemquellen in SQL Server-Umgebungen, die wir ab Kapitel 18 ausführlich behandeln.

    Parametrische vs. Ad-hoc-Abfragen und Single-Use Plans

    SQL Server unterscheidet zwischen parametrischen und Ad-hoc-Abfragen. Eine parametrische Abfrage verwendet Parameter (z.B. stored procedures oder explizite sp_executesql-Calls) — der Plan wird mit den spezifischen Parameterwerten kompiliert und gecached. Bei identischer Query-Struktur mit anderen Parametern wird der vorhandene Plan wiederverwendet (oder ein neuer kompiliert, falls er nicht passt).

    Ad-hoc-Abfragen — also direkte SQL-Strings ohne Parametrisierung — werden oft als einmalige Pläne gecached. Jede Variante des Query-Strings erzeugt einen eigenen Cache-Eintrag. Das Ergebnis: ein Plan Cache voller Single-Use Plans, die RAM belegen und nie wieder genutzt werden.

    -- Single-Use Plans im Plan Cache identifizieren
    -- Viele davon sind ein Indiz für fehlende Parametrisierung
    -- (typisches Trendforge-Problem — wir sehen das in Kapitel 34)
    SELECT
        usecounts,                    -- Wie oft wurde dieser Plan genutzt?
        size_in_bytes / 1024 AS kb,   -- Speicherverbrauch
        objtype,                      -- Proc, Adhoc, Prepared...
        text
    FROM sys.dm_exec_cached_plans AS cp
    CROSS APPLY sys.dm_exec_sql_text(plan_handle) AS st
    WHERE usecounts = 1              -- Nur einmal genutzt = Single-Use
      AND objtype = 'Adhoc'          -- Ad-hoc-Abfragen
    ORDER BY size_in_bytes DESC       -- Größte zuerst
    OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY;

    Wenn dieser Query Hunderte oder Tausende Einträge zurückgibt, hast du ein Parametrisierungsproblem. Die Lösung ist entweder "Optimize for Ad-hoc Workloads" (Kapitel 5) oder die Applikation auf Parametrisierung umzustellen. Letzteres ist die bessere Lösung, ersteres der schnelle Notfalleingriff.

    Plan Cache Invalidierung: Wann werden Pläne verworfen?

    Ein gecachter Plan ist kein ewiger Freund. SQL Server verwirft Pläne unter verschiedenen Bedingungen — manchmal sinnvoll, manchmal unnötig und kostspielig:

  • Statistik-Updates: Wenn SQL Server Statistiken aktualisiert (automatisch oder manuell), werden alle Pläne, die auf diese Statistiken angewiesen sind, aus dem Cache entfernt und bei der nächsten Ausführung neu kompiliert. Das ist einer der wichtigsten Auslöser für Recompilationen — und in Kapitel 16 sehen wir, wie man das genau misst.
  • Schema-Änderungen: ALTER TABLE, CREATE INDEX, DROP INDEX — jede strukturelle Änderung invalidiert betroffene Pläne sofort.
  • sp_recompile: Manuelle Invalidierung eines einzelnen Objekts. Nützlich nach Index-Änderungen oder, wenn ein bekannter schlechter Plan aus dem Cache entfernt werden muss.
  • DBCC FREEPROCCACHE: Nuklearer Option — verwirft alle Pläne aus dem gesamten Plan Cache. Nur im Notfall und auf Produktionssystemen mit äußerster Vorsicht zu verwenden.
  • Speicherdruck: Wenn der Gesamtspeicher knapp wird, verdrängt SQL Server Plan-Cache-Einträge. Große Pläne fliegen zuerst raus. Das bedeutet: Auf Servern mit zu wenig RAM steigen die Recompilation-Raten — CPU-Last wächst nicht, weil die Queries teurer werden, sondern, weil sie ständig neu kompiliert werden.
  • Der Zusammenhang zwischen Plan Cache Invalidierung und CPU-Last ist wichtig: Hohe Kompilierungsraten belasten die CPU, auch, wenn die eigentlichen Abfragen günstig sind. Auf Servern mit hoher Compile-Rate (messbar über den Performance Counter "SQL Compilations/sec") kann CPU-Last entstehen, die nichts mit der Abfrage-Komplexität zu tun hat. Wir messen das in Kapitel 9 (Wait Statistics) und analysieren es in Kapitel 16 und 18 im Detail.

    Toxic Plans: Wenn der Cache zum Feind wird

    Manchmal ist ein gecachter Plan nicht gut — er ist schlecht. Sehr schlecht. Das passiert, wenn Parameter Sniffing einen Plan mit untypischen Parameterwerten kompiliert hat, der für alle anderen Aufrufe ineffizient ist. Der Plan sitzt im Cache und wird wiederverwendet, obwohl er jedes Mal Schaden anrichtet. Solche Pläne heißen gelegentlich "Toxic Plans".

    -- Teuerste Pläne im Cache nach durchschnittlicher CPU-Zeit
    -- Toxic Plans erkennt man oft an sehr hoher durchschnittlicher CPU
    -- bei gleichzeitig vielen Ausführungen
    SELECT TOP 20
        qs.execution_count,
        qs.total_worker_time / qs.execution_count AS avg_cpu_mikrosekunden,
        qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
        qs.total_elapsed_time / qs.execution_count AS avg_dauer_mikrosekunden,
        SUBSTRING(st.text, 1, 200)  AS query_text,
        qp.query_plan               -- Ausführungsplan als XML
    FROM sys.dm_exec_query_stats qs
    CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
    CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
    ORDER BY avg_cpu_mikrosekunden DESC;  -- Teuerste pro Ausführung zuerst

     

    -- Plan direkt aus dem Cache entfernen (gezielt, nicht alles löschen!)
    -- Beim nächsten Aufruf wird ein neuer Plan kompiliert
    DBCC FREEPROCCACHE(plan_handle);  -- plan_handle aus obiger Abfrage nehmen

    Ein gezieltes DBCC FREEPROCCACHE mit spezifischem plan_handle ist chirurgisch — es entfernt nur diesen einen Plan, nicht den gesamten Cache. Das ist die bevorzugte Methode gegenüber dem "alles löschen"-Ansatz, der vorübergehend zu massiven Recompilationen auf dem gesamten Server führt. Kapitel 18 zeigt vollständige Strategien gegen Parameter Sniffing und Toxic Plans.

    Log Buffer und Commit-Verarbeitung: Wie Transaktionen wirklich enden

    Jeder kennt das COMMIT-Statement. Wenige wissen, was danach passiert. Und noch weniger wissen, dass genau dieser Ablauf einer der häufigsten Performance-Engpässe auf produktiven SQL Servern ist. Der Log Buffer ist der Schlüssel — er sitzt zwischen deiner Transaktion und der Disk, und seine Geschwindigkeit bestimmt deine Commit-Latenz.

    Der Log Buffer im RAM

    Write-Ahead Logging bedeutet: Bevor eine Daten-Page auf Disk geschrieben wird, muss der zugehörige Log-Record sicher auf Disk sein. Aber SQL Server schreibt nicht jeden einzelnen Log-Record sofort auf Disk — das wäre viel zu ineffizient. Stattdessen puffert SQL Server Log-Records im sogenannten Log Buffer im RAM: einem ringförmigen Speicherbereich, der per Default bis zu 60 KB groß ist, aber dynamisch wachsen kann.

    Log-Records werden kontinuierlich in diesen Buffer geschrieben. Der Buffer wird auf Disk geflusht — diesen Vorgang nennt man Log Hardening — in folgenden Situationen:

  • COMMIT einer Transaktion: Der wichtigste Trigger. Beim COMMIT muss sichergestellt sein, dass alle Log-Records dieser Transaktion auf Disk sind — sonst wäre die Durability-Garantie verletzt.
  • Buffer ist voll (60 KB Standard): Wenn der Log Buffer voll ist, wird er automatisch geleert, unabhängig von Commits.
  • Checkpoint: Checkpoint schreibt nicht nur Daten-Pages, sondern stellt sicher, dass auch alle Log-Records bis zum aktuellen LSN auf Disk sind.
  • Log-Backup: Beim Start eines Log-Backups wird der aktuelle Buffer State geflusht.
  • WRITELOG-Wait: Der Fingerabdruck langsamer Commit-Operationen

    Jedes Mal, wenn SQL Server auf den Flush des Log Buffers wartet — also, wenn ein COMMIT nicht sofort zurückkehrt, weil der Log-Record noch nicht auf Disk ist — entsteht ein WRITELOG-Wait. Das ist einer der häufigsten Wait Types auf transaktionsintensiven Systemen, und er sagt dir sehr direkt: "Deine Log-Disk ist zu langsam für deine Commit-Rate."

    -- WRITELOG-Waits aktuell messen
    -- Zeigt wie viel Zeit SQL Server seit dem letzten Restart auf Log-Flushes gewartet hat
    SELECT
        wait_type,
        waiting_tasks_count,
        wait_time_ms,
        -- Durchschnittliche Wartezeit pro Wait-Event:
        wait_time_ms / NULLIF(waiting_tasks_count, 0) AS avg_wait_ms,
        max_wait_time_ms
    FROM sys.dm_os_wait_stats
    WHERE wait_type = 'WRITELOG'
    ORDER BY wait_time_ms DESC;

     

    -- Commit-Latenz live messen (über kurze Zeiträume samplen):
    DECLARE @t1 DATETIME2 = SYSDATETIME();
    BEGIN TRANSACTION;
        -- Minimale Änderung — nur um einen Commit zu messen
        UPDATE dbo.PingTabelle SET Zeitstempel = SYSDATETIME() WHERE Id = 1;
    COMMIT;
    SELECT DATEDIFF(MICROSECOND, @t1, SYSDATETIME()) AS commit_dauer_mikrosekunden;
    -- Zielwert auf NVMe: < 100 µs. Auf SATA-SSD: 200–500 µs.
    -- Auf Sparfuchs-Hardware (Spindel): typischerweise 5.000–15.000 µs.

    Die Commit-Latenz ist eine der direktesten Messgrößen für die Log-Disk-Qualität. Auf moderner NVMe-SSD sollte ein einzelner Commit unter 100 Mikrosekunden dauern. Auf SATA-SSD sind 200–500 Mikrosekunden realistisch. Auf einer mechanischen Festplatte — wie bei Sparfuchs & Partner, wo Transaction Log, Daten, TempDB und Betriebssystem auf einer einzigen Spindel liegen — sind 5.000 bis 15.000 Mikrosekunden keine Seltenheit. Das bedeutet: Pro Transaktion verliert man bis zu 15 Millisekunden allein durch den Commit. Bei einer Applikation die 200 Transaktionen pro Sekunde abfeuert, ist das Rechenaufgabe fürs Kopfrechnen.

    Delayed Durability: Wenn Konsistenz gegen Performance getauscht wird

    Ab SQL Server 2014 gibt es Delayed Durability — eine Option, die den synchronen Log-Flush beim COMMIT auflöst. Commits kehren zurück, bevor der Log-Record physisch auf Disk ist. Stattdessen werden Log-Records gebatcht und verzögert auf Disk geschrieben.

    Das reduziert WRITELOG-Waits massiv. Der Preis: Im Falle eines Absturzes gehen die letzten committed Transaktionen verloren — bis zu 60 KB Log-Daten, also je nach Transaktionsgröße Dutzende bis Hunderte Transaktionen. Delayed Durability ist für Szenarien gedacht, in denen Datenverlust in diesem Umfang akzeptabel ist — zum Beispiel für Logging-Tabellen oder Session-Tracking. Für Finanztransaktionen oder Bestellungen ist es keine Option.

     

    Tipp: Instant File Initialization — die vergessene Optimierung

    Verwandt mit dem Log-Thema, aber für Datendateien: Instant File Initialization (IFI) erlaubt SQL Server, neue Datendatei-Allokationen sofort zu nutzen, ohne die Bytes vorher auf null zu setzen.

    Für Log-Dateien gilt IFI nicht — diese müssen immer initialisiert werden, aus Sicherheitsgründen. Für Datendateien aber kann IFI Autogrowth-Operationen von Sekunden auf Millisekunden reduzieren.

    Wie du IFI aktivierst und was es bringt, erklärt Kapitel 5 (Serverkonfiguration) im Detail. Spoiler: Es ist eine der einfachsten Optimierungen mit dem größten Effekt auf Autogrowth-Performance.

     

    Transaction Log Internals: Das WAL-Prinzip

    Das Transaction Log ist nicht optional. Es ist das Fundament der ACID-Garantien von SQL Server — ohne es gäbe es keine Durability, keine Crash Recovery, keine vollständigen Backups. Wer das Transaction Log missachtet (oder, wie in manchen Umgebungen gesehen, nie ein Log-Backup macht — wir reden über dich, Sparfuchs), zahlt irgendwann einen sehr hohen Preis.

    Write-Ahead Logging (WAL)

    SQL Server folgt dem Write-Ahead Logging-Prinzip: Bevor eine modifizierte Datenseite auf Disk geschrieben wird, muss der zugehörige Log-Eintrag auf Disk sein. Immer. Ausnahmslos.

    Das klingt nach Mehraufwand — und das ist es auch, aber aus gutem Grund: Wenn SQL Server abstürzt, kann er aus dem Transaction Log genau rekonstruieren, welche Transaktionen committed waren und welche nicht. Ohne WAL wäre nach einem Absturz unklar, in welchem Zustand die Datenpages sind.

     

    Definition: Log Sequence Number (LSN)

    Jeder Log-Eintrag bekommt eine Log Sequence Number (LSN) — eine monoton steigende Nummer, die jeden einzelnen Log-Record eindeutig identifiziert.

    LSNs sind das Rückgrat von Backup und Recovery, Replikation, Always On und Change Data Capture. Wenn du je einen "log sequence number out of order"-Fehler gesehen hast, weißt du, dass LSN-Konsistenz kein Detail ist.

    Format: (VLF-Sequenznummer):(Offset in VLF):(Slot-Nummer) — z.B. 0000:001a3f8b:0001

     

    Log-Dateistruktur: VLFs und ihre Tücken

    Die Log-Datei einer SQL-Server-Datenbank ist intern in Virtual Log Files (VLFs) unterteilt. VLFs sind die kleinsten Einheiten, die SQL Server für Log-Truncation und Backup verwendet. Zu viele VLFs verlangsamen den Startup und die Recovery. Zu wenige bedeuten, dass die Log-Datei häufig wächst und unnötige IO-Spikes erzeugt.

    -- VLF-Anzahl der aktuellen Datenbank prüfen
    -- Ziel: unter 50 VLFs für normale Datenbanken
    -- > 1.000 VLFs = ernstes Problem (Sparfuchs: 48.312 VLFs!)
    DBCC LOGINFO;  -- jede Zeile = ein VLF

     

    -- Alternativ: Anzahl zählen
    SELECT COUNT(*) AS vlf_anzahl
    FROM sys.dm_db_log_info(DB_ID());  -- ab SQL Server 2016

    Sparfuchs & Partner hatte auf ihrer Produktionsdatenbank 48.312 VLFs — das Ergebnis von 847 Autogrowth-Events à 1 MB über die Laufzeit des Servers. Jedes Autogrowth erzeugt neue VLFs, und bei kleinen Autogrowth-Werten sind das viele kleine VLFs. Die Recovery nach einem Neustart dauerte dadurch fast 20 Minuten. Wir sehen diesen Fall in Kapitel 33 im Detail.

    In Kapitel 6 (Datenbankeinstellungen) zeigen wir, wie man VLFs durch korrektes Log-Sizing und sinnvolle Autogrowth-Werte von Anfang an vermeidet — und wie man bei einer Datenbank mit zu vielen VLFs aufräumt.

    Log Truncation: Wann und warum

    Log Truncation bedeutet nicht, dass die Log-Datei kleiner wird — es bedeutet, dass der Speicherplatz der Log-Datei wiederverwendet werden kann. Committed und gebackupte Transaktionen werden als nicht mehr benötigt markiert; die entsprechenden VLFs können überschrieben werden.

    Log Truncation passiert automatisch nach einem Log-Backup (bei Full und Bulk-Logged Recovery Model) oder bei jedem Checkpoint (Simple Recovery Model). Was Log Truncation verhindert: eine aktive Transaktion, eine offene Replikations-Subscription, ein aktives AG, oder das Fehlen von Log-Backups. Das Ergebnis ist ein wachsendes Transaction Log — in extremen Fällen bis zur vollständigen Disk-Auslastung.

     

    Recovery Model

    Log-Truncation-Trigger

    Backup-Anforderung

    Typischer Einsatz

    Simple

    Automatisch bei Checkpoint

    Kein Log-Backup nötig

    Entwicklung, kleine DBs, kein RPO-Bedarf

    Full

    Nach Log-Backup

    Log-Backups zwingend

    Produktionsdatenbanken, RPO < 1h

    Bulk-Logged

    Nach Log-Backup (Bulk-Ops minimal geloggt)

    Log-Backups zwingend

    Massendaten-Loads in Produktion

    Tab. 4.10: Recovery Models und Log-Truncation

     

    Parallelismus: Wenn SQL Server mehrere CPUs gleichzeitig nutzt

    SQL Server kann eine einzelne Abfrage auf mehrere Worker Threads verteilen — Intra-Query Parallelism. Das klingt immer nach einer guten Idee, ist es aber nicht automatisch. Parallelismus erzeugt Koordinationsaufwand, belegt mehrere Kerne für eine einzelne Abfrage, und kann andere Abfragen hungern lassen.

    Der Query Optimizer entscheidet, ob eine Abfrage parallel ausgeführt wird, basierend auf den geschätzten Kosten und dem eingestellten Cost Threshold for Parallelism. Wenn der Optimizer die Kosten unter dem Threshold schätzt, läuft die Abfrage seriell. Wenn drüber — und MAXDOP > 1 — kann ein paralleler Plan gewählt werden.

    Exchange Operators: Das Rückgrat des Parallelismus

    In einem parallelen Ausführungsplan tauchen Exchange Operators auf (auch Distribute Streams, Gather Streams oder Repartition Streams genannt). Sie sind die Schnittstelle zwischen parallelen und seriellen Teilen des Plans — und sie sind teuer.

    Exchange Operators müssen Daten zwischen Worker Threads koordinieren, was Synchronisation und Pufferung erfordert. CXPACKET-Waits (und seit SQL 2017: CXCONSUMER-Waits) entstehen genau hier: Wenn ein Thread auf einen anderen warten muss, bevor er weitermachen kann. Wir schauen uns das in Kapitel 9 (Wait Statistics) und Kapitel 15 (Ausführungspläne) genauer an.

     

    Tipp: Parallelismus ist ein Werkzeug, keine Allheilmittel

    Wenn du CXPACKET-Waits siehst, bedeutet das nicht automatisch, dass du MAXDOP auf 1 setzen musst. Manchmal sind parallele Abfragen tatsächlich schneller — und CXPACKET ist nur der Preis dafür.

    Die Frage ist immer: Was ist die Gesamtlaufzeit der Abfrage? Und was kostet Parallelismus im Vergleich zu serieller Ausführung? Signal Wait-Anteil ist entscheidend: Hohe Signal Waits = CPU-Hunger, nicht Parallelismus-Problem.

    MAXDOP und Cost Threshold gemeinsam konfigurieren — Kapitel 5 zeigt die Formeln.

     

    Diagnose: Architektur-Probleme erkennen und beheben

     

    Warnung: Symptome: Architektur-bedingte Performance-Probleme

    • Hohe PAGEIOLATCH-Waits (SQL Server wartet auf Disk): Buffer Pool zu klein, zu wenig RAM

    • PLE unter 300 Sekunden (oder stark sinkend): Speicherdruck, Buffer Pool wird nicht warm

    • Hohe RESOURCE_SEMAPHORE-Waits: Memory Grants werden nicht gewährt → Kapitel 12

    • SOS_SCHEDULER_YIELD-Waits: CPU-Sättigung, Scheduler überlastet → Kapitel 9

    • Non-Yielding Scheduler im Error Log: Worker Thread blockiert Scheduler dauerhaft

    • Viele Single-Use Plans im Cache (hohe Compile-Rate): fehlende Parametrisierung

    • Hohe WRITELOG-Waits: Log-Disk zu langsam für die Commit-Rate der Applikation

    • Blocking-Ketten (LCK_M_X-Waits): Lock Escalation oder lang laufende Transaktionen

     

     

    Tipp: So misst du das

    — Buffer Pool Auslastung und PLE (pro NUMA-Node!)

    SELECT object_name, counter_name, instance_name, cntr_value

    FROM sys.dm_os_performance_counters

    WHERE counter_name IN ('Page life expectancy', 'Page reads/sec', 'Page writes/sec')

    AND object_name LIKE '%Buffer%';

     

    — Scheduler-Auslastung: Wie viele Tasks warten auf einen Scheduler?

    SELECT scheduler_id, current_tasks_count, runnable_tasks_count,

    work_queue_count, pending_disk_io_count

    FROM sys.dm_os_schedulers

    WHERE status = 'VISIBLE ONLINE'

    ORDER BY runnable_tasks_count DESC;

     

    — Plan Cache Übersicht: Wie viel RAM, wie viele Single-Use-Plans?

    SELECT objtype, COUNT(*) AS anzahl,

    SUM(size_in_bytes) / 1048576 AS gesamt_mb,

    SUM(CASE WHEN usecounts = 1 THEN 1 ELSE 0 END) AS single_use

    FROM sys.dm_exec_cached_plans

    GROUP BY objtype

    ORDER BY gesamt_mb DESC;

     

     

    Hintergrund: Typische Fehlinterpretationen

    • "Hohe Page Reads/sec = Problem": Nicht unbedingt. Hohe Read-Rates sind normal beim Start, wenn der Buffer Pool noch kalt ist. Erst, wenn sie dauerhaft hoch sind UND die PLE sinkt, ist das ein Problem.

    • "Dirty Pages = Problem": Dirty Pages im Buffer Pool sind vollkommen normal — sie zeigen schreibintensive Workloads. Problematisch werden sie erst, wenn Checkpoint-IO andere Operationen blockiert.

    • "Viele Worker Threads = Problem": Nicht per se. Worker Threads, die im Leerlauf warten, verbrauchen wenig Ressourcen. Relevant ist, wenn max worker threads erreicht wird — dann werden neue Verbindungen abgewiesen.

    • "Plan Cache voll = Problem": Plan Cache nutzt verfügbaren RAM sinnvoll. Problematisch ist nur, wenn Single-Use Plans den Cache dominieren und anderen Daten keinen Platz lassen.

    • "CXPACKET überall = MAXDOP auf 1 setzen": Das ist die häufigste Fehlreaktion auf Parallelismus-Waits. CXPACKET zeigt Parallelismus — nicht ob er schlecht ist. Die richtige Antwort ist Analyse, nicht Panikreaktion.

     

     

    Tipp: Erste Gegenmaßnahmen

    • PLE sinkt stark → Baseline prüfen, Abfragen mit hohem logical reads identifizieren (sys.dm_exec_query_stats), RAM-Aufrüstung planen

    • Viele Single-Use Plans → "Optimize for Ad-hoc Workloads" aktivieren (sofort, ohne Neustart): sp_configure 'optimize for ad hoc workloads', 1; RECONFIGURE;

    • Hohe runnable_tasks_count im Scheduler → CPU-Auslastung prüfen, teuerste Abfragen identifizieren → Kapitel 9 und 15

    • Non-Yielding Scheduler → Error Log sichern, betroffene Abfrage/Datenbank identifizieren, ggf. Service neu starten (letzter Ausweg)

    • Hohe WRITELOG-Waits → Commit-Latenz messen, Log-Disk isolieren falls möglich, Delayed Durability evaluieren, wenn Datenverlust akzeptabel

    • Blocking-Ketten → sys.dm_tran_locks prüfen, Lock Escalation untersuchen, ggf. Transaktionen kürzen oder RCSI evaluieren

     

    Zusammenfassung

    SQL Server ist keine Black Box — es ist ein System aus klar definierten Schichten, die alle zusammenarbeiten. Die Relational Engine nimmt SQL entgegen und erzeugt Ausführungspläne. Die Storage Engine liefert Daten, verwaltet Locks und garantiert Transaktionskonsistenz durch Write-Ahead Logging. Der Buffer Pool ist der Dreh- und Angelpunkt: Hier landen alle Datenpages, und seine Gesundheit lässt sich direkt an der PLE ablesen. Der SQLOS koordiniert alles — mit kooperativem Scheduling und NUMA-Awareness.

    Neu hinzugekommen in diesem Kapitel: Das Verständnis der physischen Datenstruktur (Pages, Extents, IAM/GAM/SGAM) gibt dir den Hintergrund für Fragmentierungs- und TempDB-Probleme. Der Lock Manager erklärt, warum Gleichzeitigkeit manchmal schmerzt — und was Lock Escalation dabei anrichtet. Der Log Buffer und die Commit-Verarbeitung erklären, warum deine Log-Disk so wichtig ist und was WRITELOG-Waits bedeuten. Der Resource Governor zeigt, wie du Workloads voneinander trennst, bevor sie sich gegenseitig zerstören.

    Das Wichtigste in Kürze:

  • Pages (8 KB) und Extents (64 KB) sind die physischen Bausteine — GAM/SGAM/IAM verwalten die Allokation
  • Buffer Pool ist der wichtigste Speicherbereich — PLE ist sein Pulsschlag; NUMA-Node-weise messen
  • Dirty Pages werden durch Lazy Writer und Checkpoint auf Disk geschrieben — WAL zuerst, immer
  • Der Lock Manager koordiniert Gleichzeitigkeit — Lock Escalation kann Parallelität dramatisch einschränken
  • SQLOS scheduliert kooperativ, NUMA-aware — Non-Yielding Scheduler ist ein ernstes Warnsignal
  • Query Processing folgt einer festen Pipeline: Parse → Algebrize → Optimize → Execute
  • Plan Cache ist wertvoll — Single-Use Plans sind sein größter Feind, Plan Invalidierung ist CPU-Last
  • Log Buffer und Commit-Latenz bestimmen die Transaktionsgeschwindigkeit — WRITELOG-Wait misst das
  • Transaction Log ist nicht optional — VLF-Anzahl, Autogrowth und Recovery Model müssen stimmen
  • Parallelismus ist ein Werkzeug — nicht immer die Antwort auf "es ist langsam"
  • Resource Governor trennt Workloads — sinnvoll bei gemischten OLTP- und Reporting-Lasten
  • Ausblick auf Kapitel 5

    Jetzt wo du weißt, wie SQL Server intern funktioniert, ist es Zeit, diese Maschine richtig einzustellen. Kapitel 5 widmet sich der Serverkonfiguration — mit besonderem Augenmerk auf Max Server Memory, MAXDOP und Cost Threshold for Parallelism. Und natürlich auf die Einstellungen, die du besser nie anfasst — aber die erfahrungsgemäß trotzdem immer irgendwo aktiviert sind. Priority Boost zum Beispiel. Die Konfigurierung, die Sparfuchs bis heute aktiv hat. SQL 2008 hat sie als deprecated eingestuft. Manchmal braucht es halt eine Weile.

     

    Kapitel 5