Grundsätze des Datenbankdesigns – SQL Server

SQL Server Normalisierung Datentypen Primärschlüssel Fremdschlüssel Naming Conventions

Grundsätze des
Datenbankdesigns

Entscheidungen beim Datenbankdesign wirken Jahre oder Jahrzehnte. Dieser Artikel beschreibt die wichtigsten Überlegungen – von Normalisierung über Schlüsselstrategie bis zu SQL-Server-spezifischen Besonderheiten.

Ein Datenbankschema ist kein Entwurf, der sich später leicht ändern lässt. Tabellennamen, Schlüsselstrategien und Datentypen, die in der ersten Produktivversion gesetzt werden, begleiten ein System oft über viele Jahre. Migrations­skripte, Anwendungscode und Berichte hängen an diesen frühen Entscheidungen.

Die folgenden Abschnitte behandeln die grundlegenden Überlegungen, die vor dem Anlegen der ersten Tabelle abgeschlossen sein sollten – ergänzt um SQL-Server-spezifische Empfehlungen und konkrete T-SQL-Beispiele.

01
Datenstruktur

Normalisierung – und wann man sie bricht

Normalisierung ist das systematische Verfahren, Redundanzen aus einem Datenbankschema zu entfernen und Abhängigkeiten klar zu strukturieren. Das Ziel: Jede Information wird an genau einer Stelle gespeichert. Das verhindert Inkonsistenzen bei Änderungen und vereinfacht die Pflege.

1NF
Atomare Werte, keine wiederholenden Gruppen, eindeutige Zeilen
2NF
Jedes Nicht-Schlüssel-Attribut hängt vom gesamten Primärschlüssel ab
3NF
Kein Nicht-Schlüssel-Attribut hängt von einem anderen Nicht-Schlüssel-Attribut ab
BCNF
Jede funktionale Abhängigkeit wird durch einen Superschlüssel bestimmt

Für operative Datenbanken (OLTP) ist die dritte Normalform (3NF) in der Regel das sinnvolle Ziel. BCNF und höhere Normalformen sind theoretisch elegant, erzwingen aber oft so viele Tabellen, dass die Abfragestruktur darunter leidet.

Typischer Fehler: mehrwertige Attribute in einer Spalte

Verletzt 1NF – Liste in einer Spalte
-- Telefonnummern als komma-separierter String 
CREATE TABLE Kunden 
( 
KundeID INT, 
Name NVARCHAR(100), 
Telefon NVARCHAR(200) -- '030-123,089-456' 
);
Korrekt – eigene Tabelle für Telefonnummern
CREATE TABLE Kunden 
( 
KundeID INT NOT NULL, 
Name NVARCHAR(100) NOT NULL 
); 
CREATE TABLE KundenTelefon 
( 
TelefonID INT NOT NULL, 
KundeID INT NOT NULL, 
Nummer NVARCHAR(30) NOT NULL, 
Typ NVARCHAR(20) -- 'Mobil', 'Büro' 
);

Kontrollierte Denormalisierung

Für analytische Abfragen (OLAP) oder Reporting-Datenbanken ist Denormalisierung oft bewusst sinnvoll: Vorberechnete Aggregatspalten, redundante Schlüsselspalten oder flache Wide-Tables reduzieren die Anzahl der JOINs und beschleunigen Lesezugriffe erheblich.

Faustregel: Normalisierung schützt die Datenintegrität – Denormalisierung verbessert die Leseperformance. Beide Ziele schließen sich aus. Die Entscheidung sollte bewusst und dokumentiert getroffen werden, nicht durch Bequemlichkeit entstehen.

02
Primär- und Ersatzschlüssel

Schlüsselstrategie

Der Primärschlüssel ist die Grundlage jeder Tabelle. Er identifiziert jede Zeile eindeutig, bestimmt die physische Speicherreihenfolge (Clustered Index) und dient als Ankerpunkt für alle Fremdschlüsselbeziehungen. Die Wahl der Schlüsselstrategie beeinflusst Performance, Wartbarkeit und Integrationsfähigkeit nachhaltig.

Natürliche vs. Ersatzschlüssel

AnsatzVorteileNachteileEmpfehlung
Natürlicher Schlüssel Fachlich verständlich, kein Join für Anzeige nötig Ändert sich mit der Realität; oft breit (Text); externe Abhängigkeit Mit Bedacht
INT IDENTITY Schmal (4 Byte), sequential, schnelle Joins, kein externes System Kein fachlicher Aussagewert; nicht geeignet für verteilte Systeme Standard für OLTP
BIGINT IDENTITY Wie INT, aber für sehr große Tabellen (> 2 Mrd. Zeilen) 8 Byte – größerer Index-Footprint Bei Bedarf
GUID (NEWID) Weltweit eindeutig; ideal für Replikation und verteilte Systeme 16 Byte; zufällig → starke Index-Fragmentierung Nicht als Clustered Key
GUID (NEWSEQUENTIALID) Wie GUID, aber sequenziell → deutlich weniger Fragmentierung 16 Byte; nur als DEFAULT nutzbar, nicht manuell setzbar Wenn GUID nötig
GUID als Clustered Index: Ein zufälliger GUID als Clustered Index verursacht bei jedem INSERT eine Seitenteilung (Page Split), weil der neue Wert statistisch gesehen immer in der Mitte des Index landet – nicht am Ende. Das Ergebnis: bis zu 90 % Fragmentierung und deutlich erhöhter I/O. NEWSEQUENTIALID() oder ein separater Clustered Key aus INT IDENTITY sind die Lösung.

Zusammengesetzte Primärschlüssel

Für reine Verknüpfungstabellen (Many-to-Many) ist ein zusammengesetzter PK aus den beiden Fremdschlüsseln oft die richtige Wahl – er erzwingt die Eindeutigkeit der Kombination ohne zusätzliche Spalte und der Index ist bereits der Clustered Index.

T-SQL – Verknüpfungstabelle mit zusammengesetztem PK
-- Many-to-Many: Bestellung ↔ Produkt 
CREATE TABLE BestellungProdukt 
( 
BestellungID INT NOT NULL, 
ProduktID INT NOT NULL, 
Menge INT NOT NULL, 
Einzelpreis DECIMAL(10,2) NOT NULL, 
CONSTRAINT PK_BestellungProdukt 
PRIMARY KEY (BestellungID, ProduktID) 
);

03
Speicher & Verarbeitung

Datentypen richtig wählen

Der Datentyp einer Spalte beeinflusst Speicherverbrauch, Indexgröße, Sortierverhalten und die Möglichkeit impliziter Typkonvertierungen bei Abfragen. Falsch gewählte Typen sind eine häufige Ursache für ungenutzte Indizes.

Zahlen

TypSpeicherWertebereichVerwendung
TINYINT1 Byte0 – 255Status-Codes, kleine Lookup-Werte
SMALLINT2 Byte–32.768 – 32.767Selten; wenn INT zu groß wäre
INT4 Byte± 2,1 Mrd.Standardtyp für IDs und Mengen
BIGINT8 Byte± 9,2 × 10¹⁸Große Sequenzen, Log-Tabellen
DECIMAL(p,s)5–17 ByteExaktGeldbeträge, Gewichte – nie FLOAT für Geld
FLOAT / REAL4–8 ByteNäherungswertNicht für Geldbeträge – Rundungsfehler möglich

Zeichenketten

varchar vs. nvarchar: nvarchar speichert Unicode (2 Byte pro Zeichen) und ist nötig, wenn mehrsprachige Inhalte oder Sonderzeichen erwartet werden. varchar speichert 1 Byte pro Zeichen (Codepage-abhängig) und reicht für rein ASCII-basierte Daten. Ein Mix beider Typen in JOINs oder Vergleichen erzwingt eine implizite Konvertierung, die Indizes unbrauchbar macht.
TypEncodingMax. LängeWann verwenden
CHAR(n)ASCII8.000 ByteFeste Länge (z.B. Ländercodes, IBAN-Prüfziffern)
VARCHAR(n)ASCII8.000 ByteVariable ASCII-Texte; eng begrenzen (nicht pauschal 255)
NCHAR(n)Unicode4.000 ZeichenFeste Unicode-Felder (selten)
NVARCHAR(n)Unicode4.000 ZeichenNamen, Freitexte, mehrsprachige Inhalte
NVARCHAR(MAX)Unicode2 GBSparsam einsetzen – verhindert Covering Indexes, hoher I/O

Datum und Uhrzeit

TypSpeicherGenauigkeitEmpfehlung
DATE3 ByteTagWenn Uhrzeit irrelevant (Geburtsdaten, Fälligkeiten)
TIME(n)3–5 Byte100 nsWenn nur Uhrzeit benötigt wird
DATETIME2(n)6–8 Byte100 nsStandard-Empfehlung – löst DATETIME ab
DATETIMEOFFSET10 Byte100 ns + ZeitzoneZeitzonenbewusste Systeme
DATETIME8 Byte3,33 msLegacy – in neuen Designs durch DATETIME2 ersetzen
SMALLDATETIME4 Byte1 MinuteLegacy – zu grob für die meisten Anwendungsfälle
DATETIME2 statt DATETIME: DATETIME2 hat eine höhere Genauigkeit, einen größeren Wertebereich (0001–9999), ist ISO 8601-konform und benötigt bei niedriger Präzision (DATETIME2(0)) nur 6 Byte statt 8. Es gibt keinen guten Grund, in neuen Tabellen noch DATETIME zu verwenden.

04
Konsistenz

Referentielle Integrität und Constraints

Referentielle Integrität stellt sicher, dass Beziehungen zwischen Tabellen konsistent bleiben: Ein Fremdschlüsselwert muss immer auf einen existierenden Primärschlüsselwert verweisen. Die Durchsetzung dieser Regel in der Datenbank selbst – nicht nur im Anwendungscode – ist ein zentrales Designprinzip.

Häufiger Fehler: Fremdschlüsselbeziehungen werden aus Bequemlichkeit oder Performancebedenken nicht als Constraints definiert – die Konsistenz wird stattdessen der Anwendung überlassen. Das führt früher oder später zu verwaisten Datensätzen (Orphan Records), die schwer zu bereinigen sind.

Fremdschlüssel mit CASCADE-Optionen

T-SQL – Fremdschlüssel mit Optionen
CREATE TABLE Bestellungen 
( 
BestellungID INT NOT NULL IDENTITY(1,1), 
KundeID INT NOT NULL, 
Bestelldatum DATETIME2(0) NOT NULL DEFAULT SYSDATETIME(), 
Status TINYINT NOT NULL DEFAULT 1, 
CONSTRAINT PK_Bestellungen 
PRIMARY KEY (BestellungID), 
CONSTRAINT FK_Bestellungen_Kunden 
FOREIGN KEY (KundeID) 
REFERENCES Kunden (KundeID) 
ON DELETE NO ACTION -- Löschen verhindern, wenn Bestellungen existieren 
ON UPDATE CASCADE -- KundeID-Änderung wird weitergegeben 
);
CASCADE-OptionVerhalten bei DELETE/UPDATE der ElterntabelleEmpfehlung
NO ACTIONFehler, wenn abhängige Zeilen existierenStandard
CASCADEAbhängige Zeilen werden automatisch gelöscht/aktualisiertMit Bedacht – Kettenreaktionen möglich
SET NULLFK-Spalte wird auf NULL gesetztNur wenn NULL fachlich sinnvoll ist
SET DEFAULTFK-Spalte erhält ihren DEFAULT-WertSelten sinnvoll

CHECK Constraints

CHECK Constraints erzwingen fachliche Regeln direkt in der Datenbank und sind zuverlässiger als Validierungslogik in der Anwendung.

T-SQL – CHECK Constraints
ALTER TABLE Bestellungen 
ADD CONSTRAINT CK_Bestellungen_Status 
CHECK (Status IN (1, 2, 3, 4)); -- 1=Offen, 2=Bestätigt, 3=Versendet, 4=Storniert 
 
ALTER TABLE Produkte 
ADD CONSTRAINT CK_Produkte_Preis 
CHECK (Preis >= 0); 
 
ALTER TABLE Bestellungen 
ADD CONSTRAINT CK_Bestellungen_Datum 
CHECK (Lieferdatum > Bestelldatum);

05
Konsistenz & Lesbarkeit

Naming Conventions

Konsistente Namensgebung ist keine ästhetische Frage, sondern eine Frage der Wartbarkeit. In einer Datenbank mit hunderten Tabellen und tausenden Spalten entscheiden Namenskonventionen darüber, ob ein neuer Entwickler in zehn Minuten oder in zwei Tagen produktiv wird.

Empfohlene Konventionen für SQL Server

ObjektKonventionBeispiel
TabellenPascalCase, SingularKunde, Bestellung, Produkt
SpaltenPascalCaseKundeID, Bestelldatum, Gesamtbetrag
PrimärschlüsselPK_TabellennamePK_Bestellung
FremdschlüsselFK_Kindtabelle_ElterntabelleFK_Bestellung_Kunde
IndizesIX_Tabelle_Spalte(n)IX_Bestellung_Datum
Unique ConstraintsUQ_Tabelle_SpalteUQ_Kunde_Email
CHECK ConstraintsCK_Tabelle_RegelCK_Bestellung_Status
Stored Proceduresusp_Verb_Objektusp_Get_KundeByID
Viewsv_Beschreibungv_BestellungAktiv
Triggertr_Tabelle_Ereignistr_Bestellung_AfterInsert
Reservierte Wörter und Sonderzeichen vermeiden: Tabellen- und Spaltennamen wie Order, Name, Date, User oder Status sind reservierte Schlüsselwörter in T-SQL. Sie erzwingen bei jedem Zugriff eckige Klammern ([Order]) und sind fehleranfällig. Eindeutige, fachlich sprechende Namen wie Bestellung oder Auftragsstatus sind immer vorzuziehen.

Schemas sinnvoll nutzen

SQL Server unterstützt Schemas als Namensraum-Konzept unterhalb der Datenbank. Statt alles in dbo zu legen, können Schemas funktionale Bereiche klar trennen – und separate Berechtigungen auf Schemaebene vergeben werden.

T-SQL – Schemas anlegen und nutzen
-- Schemas für funktionale Bereiche 
CREATE SCHEMA Sales; 
CREATE SCHEMA Inventory; 
CREATE SCHEMA HR; 
GO 
 
-- Tabellen im jeweiligen Schema 
CREATE TABLE Sales.Bestellung ( ... ); 
CREATE TABLE Sales.Bestellposition ( ... ); 
CREATE TABLE Inventory.Produkt ( ... ); 
CREATE TABLE HR.Mitarbeiter ( ... ); 
 
-- Berechtigungen auf Schemaebene 
GRANT SELECT ON SCHEMA::Sales TO ReportingUser;

06
Dreiwertiger Logik

NULL – bewusst einsetzen

NULL bedeutet in SQL nicht „leer" oder „null", sondern „unbekannt". Das hat weitreichende Konsequenzen: Vergleiche mit NULL ergeben weder TRUE noch FALSE, sondern UNKNOWN – was dazu führt, dass Zeilen aus Ergebnismengen verschwinden, die intuitiv erwartet werden.

Falle – NULL in Vergleichen
-- Findet KEINE Zeilen, auch wenn Lieferdatum NULL ist! 
SELECT * FROM Bestellungen 
WHERE Lieferdatum = NULL; 
 
-- Auch diese Bedingung greift nicht: 
SELECT * FROM Bestellungen 
WHERE Lieferdatum != NULL;
Korrekt – IS NULL / IS NOT NULL
-- Findet alle Bestellungen ohne Lieferdatum 
SELECT * FROM Bestellungen 
WHERE Lieferdatum IS NULL; 
 
-- COALESCE für Standardwert bei NULL 
SELECT 
BestellungID, 
COALESCE(Lieferdatum, 'noch nicht geliefert') 
AS LieferstatusText 
FROM Bestellungen;

Designentscheidung: NOT NULL als Standard

Spalten sollten standardmäßig NOT NULL definiert werden. NULL sollte nur dann erlaubt sein, wenn das Fehlen eines Wertes fachlich bedeutsam ist – zum Beispiel ein optionales Lieferdatum oder ein noch nicht zugewiesener Sachbearbeiter. Jede nullable Spalte erhöht die Komplexität von Abfragen und die Fehleranfälligkeit der Anwendungslogik.

ANSI_NULLS: SQL Server hat standardmäßig SET ANSI_NULLS ON, was dem ISO-Standard entspricht. In veralteten Datenbanken findet man gelegentlich ANSI_NULLS OFF, das = NULL als Vergleich zulässt. Diese Einstellung ist deprecated und sollte nicht verwendet werden.

07
Schemadesign & Abfrageeffizienz

Designentscheidungen mit Performance-Auswirkung

Einige Designentscheidungen, die auf den ersten Blick neutral oder rein strukturell erscheinen, haben direkte Auswirkungen auf die Abfrageperformance. Sie lassen sich im Nachhinein oft nur mit erheblichem Aufwand korrigieren.

Schmale Primärschlüssel – schmale Indizes

Der Clustered Index Key wird in jedem Non-Clustered Index als Zeiger mitgeführt. Ein breiter PK (z.B. NVARCHAR(50) oder ein zusammengesetzter Schlüssel aus drei Spalten) vergrößert daher jeden weiteren Index der Tabelle proportional. Ein schmaler INT oder BIGINT als PK minimiert den Index-Footprint der gesamten Tabelle.

Tabellen von Anfang an mit Indizes planen

Indizes sollten nicht nachträglich „bei Bedarf" ergänzt werden, sondern beim Schemaentwurf mitgeplant werden. Spalten, die regelmäßig in WHERE-Klauseln, JOIN ON-Bedingungen oder ORDER BY vorkommen, sind primäre Indexkandidaten. Jeder nicht vorhandene Index bei einer häufigen Abfrage kostet permanent Laufzeit – nicht nur beim ersten Auftreten.

T-SQL – Indizes beim Tabellendesign mitplanen
CREATE TABLE Sales.Bestellung 
( 
BestellungID INT NOT NULL IDENTITY(1,1), 
KundeID INT NOT NULL, 
Bestelldatum DATETIME2(0) NOT NULL DEFAULT SYSDATETIME(), 
Status TINYINT NOT NULL DEFAULT 1, 
Gesamtbetrag DECIMAL(12,2) NOT NULL, 
CONSTRAINT PK_Bestellung PRIMARY KEY (BestellungID), 
CONSTRAINT FK_Bestellung_Kunde 
FOREIGN KEY (KundeID) REFERENCES Sales.Kunde (KundeID) 
); 
GO 
 
-- Häufige Filterung nach KundeID → Index 
CREATE INDEX IX_Bestellung_KundeID 
ON Sales.Bestellung (KundeID); 
 
-- Häufige Filterung nach Datum + Status → zusammengesetzter Index 
CREATE INDEX IX_Bestellung_Datum_Status 
ON Sales.Bestellung (Bestelldatum, Status) 
INCLUDE (KundeID, Gesamtbetrag); -- Covering: kein Key Lookup für diese Abfrage

Tabellengröße und Wachstum einschätzen

Eine Tabelle, die heute 10.000 Zeilen hat, kann in fünf Jahren 50 Millionen Zeilen haben. Designentscheidungen sollten dieses Wachstum berücksichtigen: Ist INT für den PK groß genug? Werden Archivierungsstrategien (Partitionierung, historische Tabellen) benötigt? Wird eine Löschstrategie für veraltete Datensätze gebraucht?

Temporale Tabellen für Historisierung

Ab SQL Server 2016 unterstützt SQL Server System-Versioned Temporal Tables – Tabellen, die automatisch jede Änderung mit Zeitstempeln historisieren. Das ist eine sauberere Alternative zu selbst gebauten Audit-Triggern.

T-SQL – Temporale Tabelle (ab SQL Server 2016)
CREATE TABLE Sales.Produkt 
( 
ProduktID INT NOT NULL, 
Bezeichnung NVARCHAR(200) NOT NULL, 
Preis DECIMAL(10,2) NOT NULL, 
-- Systemzeitstempel – werden automatisch gepflegt 
GueltigVon DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL, 
GueltigBis DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL, 
PERIOD FOR SYSTEM_TIME (GueltigVon, GueltigBis), 
CONSTRAINT PK_Produkt PRIMARY KEY (ProduktID) 
) 
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = Sales.Produkt_History)); 
 
-- Historischen Preis zu einem bestimmten Zeitpunkt abfragen 
SELECT Bezeichnung, Preis 
FROM Sales.Produkt 
FOR SYSTEM_TIME AS OF '2024-06-01 12:00:00' 
WHERE ProduktID = 42;
Fazit: Gutes Datenbankdesign ist kein einmaliger Schritt, sondern eine Investition. Jede Stunde, die in durchdachte Normalisierung, konsistente Namensgebung, explizite Constraints und eine durchgeplante Schlüsselstrategie fließt, spart ein Vielfaches an Aufwand bei Wartung, Migration und Fehlersuche. Die beste Zeit für diese Überlegungen ist immer vor dem ersten CREATE TABLE.
MS SQL Server · Datenbankdesign · Grundsätze