Messreihe 1 · 10 Mio. Zeilen
Collation & Implizite Typkonvertierung
Wenn .NET / Entity Framework einen Parameter als Unicode (nvarchar) schickt, die Spalte in der Datenbank jedoch varchar ist:
Passend
3 Reads · 0 ms CPU
nvarchar
27.469 Reads · 3.011 ms CPU
Die Ursache: Gemäß SQL Server Data Type Precedence hat nvarchar einen höheren Vorrang als varchar. SQL Server muss daher nicht den Parameter umwandeln, sondern jede einzelne Tabellenzeile per interner Funktion konvertieren (Plan-Warnung: CONVERT_IMPLICIT). Ein Seek ist unmöglich; die CPU brennt 2,4 Sekunden lang.
Messreihe 2 · 10 Mio. Zeilen
Mengenübergabe (IN-Listen) & Plan-Cache-Bloat
Wie übergibt man 2.000 IDs aus C# an die Datenbank? Der Vergleich von dynamischen Strings vs. Table-Valued Parameters (TVP):
TVP
222 ms · 0 ms Compile
IN-Liste
589 ms · 185 ms Compile
Die Ursache: Dynamisch zusammengebaute IN (1, 2, 3...)-Strings fluten den Plan-Cache mit Einmal-Plänen. Bei 50.000 Aufrufen belegte die Ad-hoc-Variante 781 MB Arbeitsspeicher im Plan-Cache, während die parametrisierte TVP-Lösung mit 0,1 MB auskam.
Messreihe 3 · 1 Mio. Kind- / 50.000 Elternzeilen
Text-Schlüssel vs. Technischer int-Key (Surrogate)
Ein natürlicher 3-teiliger Textschlüssel (56 Zeichen) gegenüber einem schlanken int-Surrogate-Key:
int-Key
65 MB · 471 ms Verbund
Text-Key
287 MB · 1.142 ms Verbund
Die Ursache: Der Text-Primärschlüssel wiederholt sich in jedem Fremdschlüssel und in jedem Nonclustered Index der Kindtabellen. Tabellen und Indizes werden 4,4-mal größer und belasten den Buffer Pool dauerhaft.
Messreihe 4 · 10 Mio. Zeilen
SARGability: Führender Platzhalter (LIKE '%berg')
Endungssuche auf Firmennamen. Standard-Scan vs. berechnete Spalte mit REVERSE():
REVERSE
43 Reads · 6,3 ms
LIKE '%...'
49.022 Reads · 3.890 ms
Die Ursache: LIKE '%berg' kann keinen B-Tree nutzen. Mit einer berechneten Spalte FirmaRev AS REVERSE(Firma) und einem Index darauf wird aus der Endungssuche eine blitzschnelle Präfixsuche LIKE 'greb%': ×617 schneller.
Messreihe 5 · 1 Mio. Zeilen
Datenbankgrenzen: Collation-Falle
Abfragen über Datenbankgrenzen hinweg:
Gleiche Collation
66 Reads · Index Seek
Collation-Mismatch
7.882 Reads (Faktor ×119)
Die Ursache: Weicht die Sortierung der Remote-Datenbank ab, muss SQL Server alle Daten lokal ziehen und Zeile für Zeile vergleichen. Index-Seeks auf der Gegenseite werden unmöglich.
Messreihe 6 · 1 Mio. Zeilen
Normalisierung (3NF) & Datenqualität
Klartext-Spalten in der Haupttabelle vs. normalisierte Nachschlagetabelle (z. B. StadtId):
Normalisiert
33 ms · Widerspruch: 4,3 ms
Klartext
1.518 ms · Widerspruch: 521 ms
Die Ursache: Eine Suche auf einer kleinen Nachschlagetabelle (5.000 Städte) kostet fast nichts, bevor über die kompakte int-Spalte gejoint wird – 46,4-mal schneller, und das Modell verhindert abweichende Schreibweisen derselben Stadt.
Messreihe 7 · 10 Mio. Kunden, 40 Mio. Positionen
Prozedur gegen View: wo gefiltert wird
Gesucht ist der Umsatz der 100 Kunden einer Stadt, mit passenden Indizes. Die Logik steckt einmal in einer View, einmal in einer Prozedur ohne Parameter, deren Ergebnis erst außen gefiltert wird (INSERT … EXEC):
View, WHERE außen
52 ms · 3.408 Reads
Mehranweisungs-Funktion
53.095 ms (Faktor ×1.029)
Prozedur, außen gefiltert
134.172 ms · 44.110.987 Reads (Faktor ×2.600)
Die Ursache: Die View wird in die Abfrage eingesetzt, der Filter auf die Stadt wandert nach innen und nutzt den Index. Die Prozedur berechnet dagegen alle 10 Mio. Kunden, erst danach wird gefiltert. Das liegt nicht an der Prozedur an sich: Mit Parameter braucht sie 54 ms, so viel wie die View. Gemessen sind Client-Zeiten bei kaltem Cache.
Messreihe 8 · 100 Mio. Buchungen
Einen Monat entfernen: DELETE gegen Partition leeren
Dieselben Buchungen über 120 Monate, einmal nach Buchungsnummer gespeichert, einmal nach Datum, einmal nach Monat partitioniert. Entfernt wird ein Monat (833.333 Zeilen):
Partition leeren
8,5 ms (TRUNCATE … WITH (PARTITIONS))
DELETE, nach Datum
14.082 ms (Faktor ×1.657)
DELETE, ohne Datumsindex
67.298 ms (Faktor ×7.917)
Die Ursache: DELETE schreibt jede Zeile ins Protokoll und pflegt jeden Index mit. Das Leeren einer Partition gibt nur deren Seiten frei. Voraussetzung: Der Monat liegt genau in einer Partition, und alle Indizes sind gleich partitioniert. Gemessen wurde nur die Löschanweisung, in einer Transaktion, die danach zurückgerollt wurde.
Messreihe 9 · 100 Mio. Buchungen
Partitionierung macht Abfragen nicht automatisch schneller
Dieselben drei Tabellen wie in Messreihe 8, diesmal zwei Abfragen: die Summe eines Monats und die Summe eines Kontos über alle Jahre:
Monat, partitioniert
244 ms (ohne Datumsindex: 11.077 ms)
Konto, nicht partitioniert
45 ms
Konto, partitioniert
285 ms
Die Ursache: Für die Monatssumme genügt ein Index nach Datum; die Partitionierung war in dieser Messung sogar etwas langsamer. Die Kontoabfrage kennt den Monat nicht und muss in allen 120 Partitionen suchen. Partitionierung ist ein Werkzeug für die Datenpflege (Messreihe 8), kein Ersatz für den passenden Index.
Messreihe 10 · 100 Mio. Kunden, 400 Mio. Positionen
Join über drei Tabellen bei 100 Millionen Kunden
Kunde, Bestellung und Position; gesucht sind die 400 Positionen der Kunden einer Stadt. Ohne Index auf den Fremdschlüsseln gegen Indizes auf Stadt und Fremdschlüsseln (mit INCLUDE):
Mit Indizes
66 ms · 3.144 Reads
Nur Primärschlüssel
41.982 ms · 6.843.764 Reads (Faktor ×632)
Die Ursache: Ohne Index muss SQL Server alle 400 Mio. Positionen lesen, um 400 zu finden – 2.177-mal so viele Seiten. Drei Unterabfragen je Kunde statt des Joins: 50.737 ms gegen 232 ms (Faktor ×219). Erzwungene Nested Loops ohne Index wurden bei dieser Größe nicht gemessen.