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.

Datenbankdesign & Datentypen: – 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 ]

Datenbankdesign & Datentypen:

Performance fängt beim Schema an — nicht beim Index

Schlechtes Design ist kein Index-Problem

Es gibt eine verbreitete Vorstellung in der SQL Server-Welt: Performance-Probleme löst man mit Indizes. Index fehlt? Index hinzufügen. Immer noch langsam? Noch ein Index. Query Store zeigt hohe CPU? Index optimieren. Das ist nicht falsch — aber es ist nur die halbe Wahrheit.

Die andere Hälfte ist das, was du nicht mehr ändern kannst, wenn die Anwendung erst einmal in Produktion ist: Das Schema. Falsche Datentypen, ungünstige Primary Keys, verfehlte Normalisierungsentscheidungen — das sind strukturelle Schulden, die kein Index der Welt vollständig zurückzahlen kann. In diesem Kapitel schauen wir auf die Designentscheidungen, die Performance-Probleme verursachen, bevor der erste Index gesetzt wird.

Das ist Teil IV — wir reden jetzt als Entwickler, nicht als DBA. Du hast Einfluss auf das Schema. Nutze ihn.

 

Hinweis: Querverweise

Implicit Conversions durch falsche Datentypen zerstören Index Seeks — wir zeigen das im Detail in Kapitel 21 (SARGability). Die Konsequenzen von GUID als Clustering Key für die Index-Strategie behandelt Kapitel 17. Trendforge Digital in Kapitel 34 ist das Lehrstück für applikationsseitige Designfehler auf einem sonst tadellos konfigurierten Server.

 

Datentypen: Kleine Entscheidungen mit großer Wirkung

 

Datentyp

Speicher

Performance-Aspekte

Empfehlung

INT

4 Byte

Ideal für FK/PK — schnelle Joins, kompakte Indizes

Standard für Integer-IDs

BIGINT

8 Byte

Doppelter Speicher vs. INT — nur, wenn > 2,1 Mrd. Werte

Nur, wenn INT zu klein

SMALLINT

2 Byte

Gut für Status-Spalten, Lookup-Codes

Bei bekannt kleinen Werten

TINYINT

1 Byte

0-255 — ideal für Flags, Ratings, Status

Für kleine Domänen

VARCHAR(n)

1 Byte/Zeichen

Kein Unicode-Overhead — bevorzugen, wenn nur ASCII

Standard für Text ohne Unicode

NVARCHAR(n)

2 Byte/Zeichen

Doppelter Speicher — nur, wenn Unicode wirklich nötig

Nur bei echtem Unicode-Bedarf

DATETIME

8 Byte

3 ms Präzision — veraltet, Konversionsprobleme

DATETIME2 bevorzugen

DATETIME2

6-8 Byte

100 ns Präzision, ANSI-kompatibel, besser für Indizes

Standard für Zeitstempel

DATE

3 Byte

Nur Datum ohne Zeit — sehr kompakt

Wenn Zeit irrelevant

DECIMAL(p,s)

5-17 Byte

Präzise, kein Rundungsfehler — langsamer als INT

Für Geldbeträge, Messwerte

FLOAT/REAL

4-8 Byte

Schnell, aber Rundungsfehler — nie für Geld!

Nur für wissenschaftliche Werte

UNIQUEIDENTIFIER

16 Byte

Viermal größer als INT — Fragmentierung bei NEWID()

Nur mit NEWSEQUENTIALID()

Tabelle 20.1: SQL Server Datentypen und ihre Performance-Eigenschaften

 

VARCHAR vs. NVARCHAR: Die Unicode-Frage

NVARCHAR speichert jeden Zeichen in zwei Bytes — auch, wenn es nur ein ASCII-"A" ist. Bei einer Spalte Name VARCHAR(100) vs. NVARCHAR(100) mit einer Million Zeilen bedeutet das 100 MB vs. 200 MB Index-Größe. Größere Indizes = weniger Einträge pro Page = mehr IO = schlechtere PLE.

-- Prüfen ob NVARCHAR-Spalten Unicode wirklich benötigen
-- Wenn alle Werte ins ASCII-Alphabet passen: VARCHAR reicht
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH,
       COLLATION_NAME
FROM   INFORMATION_SCHEMA.COLUMNS
WHERE  TABLE_NAME = 'Kunden'
  AND  DATA_TYPE IN ('nvarchar', 'nchar');

 

-- Prüfen ob tatsächlich Unicode-Werte vorhanden sind:
SELECT COUNT(*) AS zeilen_mit_unicode
FROM   dbo.Kunden
WHERE  Name != CAST(Name AS VARCHAR(MAX));
-- Ergebnis 0: Unicode war unnötig, VARCHAR würde reichen

Die Empfehlung: Wenn die Anwendung internationalisiert sein soll oder könnte, nimm von Anfang an NVARCHAR. Nachträgliche Migration von VARCHAR zu NVARCHAR auf großen Tabellen ist aufwändig. Aber, wenn du weißt, dass es sich um interne Codes, technische IDs oder ASCII-konforme Bezeichner handelt: VARCHAR spart Platz und IO.

 

Warnung: Die tückischste VARCHAR/NVARCHAR-Falle

Wenn deine Tabellenspalte VARCHAR ist und der ORM oder die Applikation einen NVARCHAR-Parameter schickt (was .NET standardmäßig tut), passiert eine Implicit Conversion: SQL Server konvertiert die gesamte Spalte auf NVARCHAR für den Vergleich — und ignoriert dabei den Index. Das ist kein Edge Case, das passiert täglich. Kapitel 21 zeigt genau dieses Muster.

 

DATETIME vs. DATETIME2: Nimm DATETIME2

DATETIME hat eine Präzision von 3,33 ms und speichert Zeiten intern als "Millisekunden seit 1900-01-01". DATETIME2 hat eine Präzision von 100 Nanosekunden, ist ANSI/ISO-konform, kompatibel mit allen Spracheinstellungen und speichert effizient in 6-8 Byte abhängig von der gewählten Präzision. Es gibt keinen einzigen Grund im Jahr 2024 noch DATETIME zu verwenden.

-- DATETIME2 mit variabler Präzision:
DATETIME2(0)  -- Sekunden-Präzision, 6 Byte
DATETIME2(3)  -- Millisekunden, 7 Byte (wie DATETIME, aber ANSI)
DATETIME2(7)  -- 100ns-Präzision, 8 Byte (Standard)

 

-- Konversion von DATETIME zu DATETIME2 im Vergleich:
SELECT CAST(GETDATE() AS DATETIME2(3)) AS moderne_zeit,
       GETDATE()                       AS alte_zeit;

GUID vs. INT als Primary Key: Die Performance-Falle der Verteilung

GUIDs (UNIQUEIDENTIFIER) als Primary Keys sind in vielen modernen Applikationen der Standard — weil sie global eindeutig sind, ohne Koordination mit der Datenbank generiert werden können und gut für verteilte Systeme passen. All das stimmt. Aber der Performance-Preis ist real.

Warum NEWID() den Clustered Index fragmentiert

Ein Clustered Index ordnet die physischen Daten nach dem Index-Key. Wenn der Primary Key ein INT ist, werden neue Zeilen immer am Ende eingefügt — sequentiell, effizient, keine Fragmentierung. Wenn der Primary Key ein GUID mit NEWID() ist, ist jeder neue Wert zufällig über den gesamten Key-Space verteilt. Das bedeutet: Jede neue Zeile wird irgendwo mitten in den Clustered Index eingefügt — Page Split, Fragmentierung, erhöhter IO.

-- Schlechter Ansatz: NEWID() = vollständig zufällige GUIDs
-- Führt zu massiver Fragmentierung des Clustered Index
CREATE TABLE dbo.Bestellungen_Schlecht (
    BestellID    UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY,
    KundeID      INT,
    Betrag       DECIMAL(10,2)
);

 

-- Besser: NEWSEQUENTIALID() = sequentielle GUIDs
-- Neue Werte werden am Ende eingefügt — wie INT, aber global eindeutig
-- Einschränkung: Nur als DEFAULT, nicht manuell generierbar
CREATE TABLE dbo.Bestellungen_Besser (
    BestellID    UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
    KundeID      INT,
    Betrag       DECIMAL(10,2)
);

 

-- Noch besser: INT mit IDENTITY als Clustered Index,
-- GUID als UNIQUE-Spalte für externe Referenzen
CREATE TABLE dbo.Bestellungen_Optimal (
    BestellID    INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
    BestellGUID  UNIQUEIDENTIFIER DEFAULT NEWID() UNIQUE NONCLUSTERED,
    KundeID      INT,
    Betrag       DECIMAL(10,2)
);
-- Der Clustered Index ist sequentiell (INT)
-- Der GUID ist verfügbar für externe APIs, aber nicht der Clustering Key

 

Praxisbeispiel: Trendforge Digital: GUIDs als Clustering Keys

Trendforge Digital (TFSQL01) verwendete in ihrem Entity Framework-basierten Datenmodell GUIDs mit NEWID() als Primary Keys auf allen 47 Tabellen. Fragmentierungsgrad nach 3 Monaten: durchschnittlich 78%. Die täglichen Index-Rebuilds dauerten 4,5 Stunden — und zerstörten die Nacht-Backups. Nach Migration auf NEWSEQUENTIALID() sank die Fragmentierung auf unter 10% und die Rebuild-Zeit auf 40 Minuten. Kapitel 34 beschreibt den Migrationspfad.

 

Normalisierung vs. Denormalisierung: Das READ/WRITE-Verhältnis entscheidet

Normalisierung reduziert Redundanz und schützt Datenintegrität. Denormalisierung erhöht Redundanz, um Abfragen zu vereinfachen. Beide haben ihren Platz — aber die falsche Entscheidung für das falsche Workload-Profil kostet.

Die Faustregel: Je mehr Reads vs. Writes, desto eher lohnt Denormalisierung. Ein Report der 100.000 Mal am Tag läuft und immer dieselben JOINs über 5 Tabellen macht? Candidate für eine denormalisierte Reporting-Tabelle. Eine Transaktionstabelle die 50.000 INSERTs pro Minute bekommt? Bleib normalisiert.

 

Szenario

Empfehlung

Begründung

OLTP: viele kleine Writes

Normalisiert (3NF)

Weniger Seiten bei INSERTs, Locking auf kleinen Zeilen

Reports über viele Joins

Denormalisiert / Materialized

Joins kosten bei großen Datenmengen IO und CPU

Read-Heavy, seltene Updates

Denormalisiert vertretbar

Redundanz ist akzeptabel, wenn Updates selten

Hohe Schreiblast, Konsistenz kritisch

Normalisiert

Redundante Daten = mehr Locks bei Updates

Data Warehouse / Analytics

Star Schema / Columnar

Andere Denkweise: Fakten + Dimensionen, kein 3NF

Tabelle 20.2: Normalisierung vs. Denormalisierung nach Workload-Profil

 

NULLs: Unterschätzte Performance-Kosten

NULL ist kein Wert, sondern das Fehlen eines Wertes. SQL Server muss das extra speichern: In der Zeile gibt es ein Null-Bitmap das für jede nullable Spalte ein Bit reserviert. Das ist effizient, aber es hat Konsequenzen.

  • NOT NULL Spalten sind kompakter — SQL Server muss kein Null-Bitmap prüfen
  • Indizes können NULL enthalten — aber IS NULL Abfragen nutzen Indizes oft schlechter als Gleichheitsvergleiche
  • COUNT(*) und COUNT(spalte) verhalten sich bei NULL-Werten verschieden — ein klassischer Entwickler-Fehler
  • NULL-Vergleiche: WHERE Spalte = NULL funktioniert nie — immer WHERE Spalte IS NULL
  • -- Spalten mit hohem NULL-Anteil finden
    -- Viele NULLs = möglicherweise schlechtes Design (Sparse Columns als Alternative)
    SELECT
        COLUMN_NAME,
        IS_NULLABLE,
        -- Null-Anteil schätzen (dynamisches SQL nötig für echten Check)
        DATA_TYPE
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_NAME = 'MeineTabelle'
      AND IS_NULLABLE = 'YES'
    ORDER BY COLUMN_NAME;

     

    -- Bei sehr vielen NULLs: Sparse Column erwägen
    -- Sparse Columns sparen Speicher, wenn >80% der Werte NULL sind
    ALTER TABLE dbo.Kunden
        ADD BesonderePraeferenzen NVARCHAR(500) SPARSE NULL;

    Tabellen-Kompression: Weniger IO, mehr CPU — der Trade-off

    SQL Server bietet zwei Kompressionsstufen für Tabellen und Indizes: Row Compression und Page Compression. Beide reduzieren die Datenmenge auf der Disk und im Buffer Pool — was IO spart und PLE verbessert. Dafür kostet das Komprimieren und Dekomprimieren CPU.

     

    Kompressionstyp

    Ersparnis

    CPU-Overhead

    Wann sinnvoll

    ROW

    10-40%

    Gering (~5%)

    Immer, wenn Speicherersparnis erwünscht

    PAGE

    30-80%

    Mittel (~10-20%)

    Read-Heavy, große Tabellen, freie CPU

    COLUMNSTORE

    80-95%

    Hoch beim Schreiben

    Analytics, Data Warehouse, Read-Only

    Keine

    0%

    0%

    Write-Heavy OLTP mit CPU-Engpass

    Tabelle 20.3: Kompressionsstufen im Vergleich

     

    -- Row Compression auf eine große Tabelle anwenden
    -- ONLINE = keine Sperrzeit, aber länger dauernder Rebuild
    ALTER TABLE dbo.Bestellungen
    REBUILD PARTITION = ALL
    WITH (DATA_COMPRESSION = ROW, ONLINE = ON);

     

    -- Kompressionsersparnis vorab schätzen (kein echter Umbau):
    EXEC sp_estimate_data_compression_savings
        @schema_name = 'dbo',
        @object_name = 'Bestellungen',
        @index_id    = NULL,     -- alle Indizes
        @partition_number = NULL, -- alle Partitionen
        @data_compression = 'ROW';

     

    Tipp: Kompression — zuerst messen, dann entscheiden

    sp_estimate_data_compression_savings ist kostenlos und braucht keine Änderungen. Führe ihn zuerst aus und prüfe: Wenn PAGE-Kompression nur 8% spart aber 20% CPU-Overhead kostet — lass es. Wenn sie 65% spart und du CPU-Headroom hast — tu es. Ohne Messung ist es Raten.

     

    Zusammenfassung

    Datenbankdesign ist keine akademische Übung — es ist der Fundament über dem alle Performance-Optimierungen aufgebaut werden. Ein solides Schema macht Indizes effektiver, Abfragen schneller und den DBA glücklicher.

    Die wichtigsten Entscheidungen:

  • VARCHAR statt NVARCHAR, wenn kein echtes Unicode nötig — spart 50% Indexspeicher
  • DATETIME2 statt DATETIME — kein Grund mehr für DATETIME in neuen Projekten
  • INT als Clustering Key statt GUID — oder zumindest NEWSEQUENTIALID() statt NEWID()
  • NOT NULL wo immer möglich — kompaktere Zeilen, saubereres Datenmodell
  • Kompression evaluieren mit sp_estimate_data_compression_savings, bevor du entscheidest
  • Normalisierung vs. Denormalisierung nach Read/Write-Verhältnis entscheiden, nicht nach Lehrbuch
  • Implicit Conversions — das stille Gift das entsteht, wenn Applikation und Datenbankschema verschiedene Datentypen verwenden — sind das Thema von Kapitel 21. Dort werden wir sehen wie ein einziger falscher Datentyp einen perfekten Index komplett nutzlos macht.

     

     

    Hinweis: Ausblick auf Kapitel 21

    Kapitel 21 behandelt SARGability — die Eigenschaft einer Abfragebedingung, einen Index nutzen zu können. Du wirst sehen, dass SARGability-Killer (Funktionen auf Spalten, LIKE mit führendem Wildcard, Implicit Conversions) die häufigste Ursache für "Index vorhanden, aber ignoriert" sind. Trendforge macht das bei 12 von 23 Abfragen systematisch falsch.

     

     

    Abb. 1: Implicit Conversion: Typkonflikte und ihre Kosten

     

    Kapitel 21