Diplom-Biologe | Senior IT-Consultant
|
SH |
Sascha Hess xenosystems.de - IT-Consulting & Data Management |
www.xenosystems.de |
|
|
NOTFALL-KIT – SQL SERVER 2026 |
|
|
SQL Server |
|
|
Normalisierung vs. Denormalisierung |
|
|
Das richtige Datenbankdesign für OLTP, DWH und BI — fundiert entschieden |
WAS SIE IN DIESEM KIT ERHALTEN:
|
|
1 |
10 Design-Fallen Die häufigsten Fehler beim Datenbankdesign — mit Sofort-Korrekturen |
|
|
2 |
Normalformen kompakt 1NF bis BCNF — verständlich, mit Vorher-Nachher-Beispielen |
|
|
3 |
Entscheidungsrahmen Wann normalisieren, wann denormalisieren — der Entscheidungsbaum |
|
|
4 |
Star Schema & Data Vault Denormalisierungsstrategien für DWH und BI — praxisnah erklärt |
|
|
5 |
30-Tage-Redesign-Plan Bestehende Schemas systematisch analysieren und gezielt verbessern |
HAFTUNGSAUSSCHLUSS
Alle Skripte, Designempfehlungen und Methoden wurden sorgfältig erarbeitet und in realen SQL-Server-Umgebungen erprobt. Da jede Datenbankumgebung individuell ist, übernimmt der Autor keinerlei Haftung für Datenverlust, Systemausfälle oder sonstige Schäden. Testen Sie alle Schema-Änderungen zunächst in einer Nicht-Produktionsumgebung und erstellen Sie vor jedem Eingriff ein vollständiges Backup.
KEINE ERGEBNISGARANTIE
Genannte Performance-Verbesserungen sind Erfahrungswerte aus realen Projekten und keine verbindliche Zusicherung. Tatsächliche Ergebnisse hängen von Hardware, Datenvolumen, Abfragemustern und Anwendungsarchitektur ab.
VERSIONSHINWEIS
Die Inhalte beziehen sich auf SQL Server 2022 (16.x) und SQL Server 2025 (17.x), Stand März 2026. Konzepte zur Normalisierung und zu Datenbankdesign-Prinzipien sind standardbasiert (ISO/IEC 9075) und weitgehend versionsneutral. Tool-spezifische Features (Columnstore, In-Memory OLTP) können je nach Version abweichen.
URHEBERRECHT
Dieses Dokument ist für den persönlichen oder betriebsinternen Gebrauch des Käufers lizenziert. Weiterverkauf, Weitergabe an Dritte und öffentliche Veröffentlichung sind ohne schriftliche Genehmigung nicht gestattet.
MARKENRECHTE
SQL Server, Power BI und Azure Synapse sind eingetragene Marken der Microsoft Corporation. Data Vault ist eine Methodik von Dan Linstedt. Alle anderen genannten Produkt- und Unternehmensnamen sind Eigentum ihrer jeweiligen Inhaber.
Eine ausführliche Version dieses Haftungsausschlusses befindet sich am Ende dieses Dokuments.
01 Einleitung
Warum das Datenbankdesign über Performance und Wartbarkeit entscheidet
02 Die 10 Design-Fallen
Häufigste Fehler im Datenbankschema — mit Diagnose und Fix
03 Normalformen kompakt
1NF bis BCNF — verständlich, mit Vorher-Nachher-Beispielen
04 Anomalien und ihre Kosten
Insert-, Update- und Delete-Anomalien im Alltag
05 Der Entscheidungsrahmen
Wann normalisieren, wann denormalisieren — systematisch entscheiden
06 Denormalisierung gezielt einsetzen
Abgeleitete Spalten, Redundanz und Precalculation — sicher und kontrolliert
07 Star Schema & Snowflake
Das Denormalisierungsmodell für BI und Data Warehouse
08 Data Vault als Mittelweg
Normalisierte Historisierung mit denormalisierter Auswertbarkeit
09 Moderne Optimierungen
Columnstore, Partitionierung, In-Memory OLTP — wann was hilft
10 30-Tage-Redesign-Plan
Bestehende Schemas analysieren und gezielt verbessern
01
Zwei Datenbanken, dieselben Daten, dasselbe ERP-System. Die eine liefert Reports in drei Sekunden, hat keine Dateninkonsistenzen und lässt sich problemlos erweitern. Die andere braucht 45 Sekunden für denselben Report, enthält widersprüchliche Kundenadressen in drei Tabellen gleichzeitig, und jede Schemaerweiterung zieht eine Kaskade von Anpassungen nach sich.
Der Unterschied liegt fast nie in der Hardware, selten im Query-Tuning — und fast immer im Datenbankdesign.
Normalisierung und Denormalisierung sind keine akademischen Konzepte. Sie sind Designentscheidungen mit direkten, messbaren Auswirkungen auf Performance, Datenintegrität und Entwicklungskosten.
Und doch werden diese Entscheidungen in der Praxis mittelständischer IT-Projekte erschreckend selten bewusst getroffen. Schemas wachsen organisch: eine Tabelle hier, ein Extrafeld dort, eine Redundanz, weil der JOIN zu aufwendig wirkt. Nach drei Jahren hat niemand mehr einen vollständigen Überblick — und der erste Entwickler, der eine Änderung vornehmen will, braucht eine halbe Woche, um die Nebenwirkungen zu verstehen.
Das zentrale Spannungsfeld ist dabei kein Fehler — es ist eine fundamentale Eigenschaft relationaler Datenbanken:
→ Normalisierung eliminiert Redundanzen, schützt die Datenintegrität und reduziert den Speicheraufwand. Sie optimiert für Schreiboperationen und Konsistenz — ideal für transaktionale Systeme (OLTP).
→ Denormalisierung reduziert JOINs, verbessert Leseperformance und vereinfacht Abfragen auf Kosten von Redundanz und Wartungsaufwand. Sie optimiert für Leseoperationen — ideal für analytische Systeme (OLAP, BI, DWH).
Die Kunst liegt nicht darin, immer zu normalisieren oder immer zu denormalisieren. Die Kunst liegt darin, für jeden Kontext — OLTP, DWH, BI, Reporting — die richtige Strategie zu wählen und diese Entscheidung bewusst, dokumentiert und wartbar umzusetzen.
|
|
WAS SIE IN DIESEM BUCH ERWARTEN DÜRFEN ■ 10 Design-Fallen — Die häufigsten Schema-Fehler in KMU-Datenbanken mit konkreter Diagnose und Fix. ■ Normalformen kompakt — 1NF bis BCNF mit echten Vorher-Nachher-Beispielen statt abstrakter Theorie. ■ Entscheidungsrahmen — Wann normalisieren, wann denormalisieren — mit Entscheidungsbaum je Anwendungsfall. ■ Star Schema und Data Vault — Denormalisierungsstrategien für DWH und BI, mit konkreten SQL-Beispielen. ■ 30-Tage-Redesign-Plan — Bestehende Schemas systematisch analysieren, priorisieren und gezielt verbessern. |
|
|
DIE GRUNDREGEL Normalisieren Sie zuerst — vollständig bis mindestens 3NF. Denormalisieren Sie danach — gezielt, kontrolliert und dokumentiert, dort wo Messung (nicht Intuition) einen konkreten Bedarf belegt. Nie umgekehrt. |
02
Eine Spalte enthält kommaseparierte Listen: Kategorien = "Elektronik,Haushalt,Garten". Das ist keine Datenbank — das ist eine versteckte Tabelle in einer Textspalte. Kein Index, keine referenzielle Integrität, kein effizienter Filter möglich.
DIAGNOSE:
|
-- Spalten mit kommaseparierten Werten finden: SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE DATA_TYPE IN ('varchar','nvarchar','text') AND TABLE_SCHEMA = 'dbo' ORDER BY TABLE_NAME, COLUMN_NAME; -- Dann manuell prüfen: gibt es Spalten mit trennzeichengetrennten Werten? |
FIX:
|
-- VORHER: Mehrere Werte in einer Spalte (verletzt 1NF) -- Artikel: ArtikelID=1, Kategorien='Elektronik,Haushalt,Garten'
-- NACHHER: Auflösung in Relationstabelle (1NF erfüllt) CREATE TABLE Kategorien ( KategorieID INT IDENTITY PRIMARY KEY, KategorieName NVARCHAR(100) NOT NULL UNIQUE );
CREATE TABLE Artikel_Kategorien ( ArtikelID INT NOT NULL REFERENCES Artikel(ArtikelID), KategorieID INT NOT NULL REFERENCES Kategorien(KategorieID), PRIMARY KEY (ArtikelID, KategorieID) ); |
Telefon1, Telefon2, Telefon3 in einer Kundentabelle. Was wenn ein Kunde vier Nummern hat? Was wenn 90 % der Kunden nur eine Nummer haben und die anderen Spalten leer sind? Die Tabelle ist unnötig breit, schlecht erweiterbar und schwer abzufragen.
FIX:
|
-- VORHER: Wiederholende Spaltengruppen (verletzt 1NF) -- Kunden: KundeID, Name, Telefon1, Telefon2, Telefon3
-- NACHHER: Ausgelagerte Entität mit Typkennzeichen CREATE TABLE Kunden_Kontakte ( KontaktID INT IDENTITY PRIMARY KEY, KundeID INT NOT NULL REFERENCES Kunden(KundeID), KontaktTyp NVARCHAR(20) NOT NULL -- 'Mobil','Festnetz','Fax' CHECK (KontaktTyp IN ('Mobil','Festnetz','Fax','Sonstig')), Nummer NVARCHAR(30) NOT NULL, IstPrimär BIT NOT NULL DEFAULT 0 ); CREATE INDEX IX_Kunden_Kontakte_KundeID ON Kunden_Kontakte (KundeID); |
In einer Bestelltabelle: BestellID, KundeID, KundeName, KundenPLZ, KundenStadt. KundenPLZ und KundenStadt hängen von KundeID ab — nicht von BestellID. Wenn der Kunde umzieht, müssen alle Bestellzeilen aktualisiert werden — oder die Daten werden inkonsistent.
FIX:
|
-- VORHER: Transitive Abhängigkeit (verletzt 3NF) -- Bestellungen: BestellID, KundeID, KundeName, KundenPLZ, KundenStadt
-- NACHHER: Kunden-Stammdaten in eigener Tabelle CREATE TABLE Kunden ( KundeID INT IDENTITY PRIMARY KEY, KundeName NVARCHAR(100) NOT NULL, PLZ NVARCHAR(10) NOT NULL, Stadt NVARCHAR(100) NOT NULL );
CREATE TABLE Bestellungen ( BestellID INT IDENTITY PRIMARY KEY, KundeID INT NOT NULL REFERENCES Kunden(KundeID), Bestelldatum DATE NOT NULL, -- KundeName, PLZ, Stadt: NICHT hier -- kommen per JOIN ); |
NULL wird verwendet um gleichzeitig "unbekannt", "nicht zutreffend", "nicht ausgefüllt" und "gelöscht" auszudrücken. JOIN-Logik wird unvorhersehbar. Aggregationen verhalten sich überraschend. WHERE-Bedingungen liefern unerwartete Ergebnisse.
FIX:
|
-- VORHER: NULL für mehrere Bedeutungen -- Lieferdatum NULL kann bedeuten: noch nicht geliefert ODER kein Versand nötig ODER unbekannt
-- NACHHER: Explizite Statusmodellierung CREATE TABLE Bestellungen ( BestellID INT IDENTITY PRIMARY KEY, Lieferstatus NVARCHAR(20) NOT NULL DEFAULT 'Ausstehend' CHECK (Lieferstatus IN ('Ausstehend','Versendet','Geliefert','Kein Versand')), Lieferdatum DATE NULL, -- NULL nur wenn Lieferstatus = 'Ausstehend' Lieferkommentar NVARCHAR(500) NULL -- Freitext, darf NULL sein CONSTRAINT CK_Lieferdatum CHECK ( (Lieferstatus IN ('Ausstehend','Kein Versand') AND Lieferdatum IS NULL) OR (Lieferstatus IN ('Versendet','Geliefert') AND Lieferdatum IS NOT NULL) ) ); |
Kundennummer, EAN-Code, Steuernummer als Primärschlüssel. Das funktioniert — bis sich die Kundennummer ändert, der EAN-Standard aktualisiert wird oder ein Unternehmen seine Steuernummer erhält. Dann müssen Hunderte von Fremdschlüsselreferenzen kaskadierend aktualisiert werden.
FIX:
|
-- VORHER: Natürlicher Schlüssel als PK CREATE TABLE Artikel ( EAN NVARCHAR(13) PRIMARY KEY, -- Kann sich ändern! Bezeichnung NVARCHAR(200) NOT NULL );
-- NACHHER: Surrogate Key als PK, natürlicher Schlüssel als UNIQUE CREATE TABLE Artikel ( ArtikelID INT IDENTITY PRIMARY KEY, -- Surrogate Key: stabil EAN NVARCHAR(13) NOT NULL UNIQUE, -- Business Key: eindeutig Bezeichnung NVARCHAR(200) NOT NULL ); -- Fremdschlüssel referenzieren ArtikelID — nie EAN |
Bestellzeilen referenzieren ArtikelIDs, die in der Artikeltabelle nicht existieren — weil kein Fremdschlüssel definiert wurde. Datenimporte bringen Waisen-Datensätze ein. Reports geben falsche Aggregate zurück. JOINs verlieren Zeilen ohne Fehlermeldung.
FIX:
|
-- Bestehende referenzielle Integrität prüfen: -- Verwaiste Fremdschlüsselwerte finden (ohne FK-Constraint) SELECT COUNT(*) AS Waisen_Bestellzeilen FROM dbo.Bestellzeilen bz WHERE NOT EXISTS ( SELECT 1 FROM dbo.Artikel a WHERE a.ArtikelID = bz.ArtikelID );
-- FK-Constraint nachträglich hinzufügen: ALTER TABLE dbo.Bestellzeilen ADD CONSTRAINT FK_Bestellzeilen_Artikel FOREIGN KEY (ArtikelID) REFERENCES dbo.Artikel(ArtikelID); -- Bei vorhandenen Waisenzeilen: erst bereinigen, dann Constraint anlegen |
Eine Tabelle mit Spalten EntityID, AttributName, AttributWert für alle möglichen Entitäten und Attribute. Das Entity-Attribute-Value-Muster (EAV) ist verlockend flexibel — und eine Performance- und Integritäts-Katastrophe. Keine Typsicherheit, keine Fremdschlüssel, keine Indizes auf Werte, katastrophale Abfragebarkeit.
FIX:
|
-- VORHER: EAV-Anti-Pattern -- EntityAttributes: EntityID=1, Attribut='Preis', Wert='29.99' -- EntityAttributes: EntityID=1, Attribut='Gewicht', Wert='0.5'
-- NACHHER A: Typisierte Tabellen (wenn Entitäten bekannt) CREATE TABLE Artikel ( ArtikelID INT IDENTITY PRIMARY KEY, Bezeichnung NVARCHAR(200) NOT NULL, Preis DECIMAL(10,2) NOT NULL, Gewicht_kg DECIMAL(8,3) NULL );
-- NACHHER B: JSON-Spalte (wenn wirklich flexible Attribute nötig sind) -- Verfügbar ab SQL Server 2016 — typisiert, indexierbar CREATE TABLE Artikel ( ArtikelID INT IDENTITY PRIMARY KEY, Bezeichnung NVARCHAR(200) NOT NULL, Preis DECIMAL(10,2) NOT NULL, Eigenschaften NVARCHAR(MAX) NULL -- JSON: {"Gewicht":0.5,"Farbe":"Blau"} CONSTRAINT CK_Eigenschaften CHECK ( Eigenschaften IS NULL OR ISJSON(Eigenschaften) = 1 ) ); -- JSON-Index für häufig genutzte Attribute: CREATE INDEX IX_Artikel_Gewicht ON dbo.Artikel (CAST(JSON_VALUE(Eigenschaften,'$.Gewicht') AS DECIMAL(8,3))); |
Eine Tabelle mit 80 Spalten. 60 davon werden in 95 % aller Abfragen nie benötigt. SQL Server muss trotzdem alle Spalten in den Buffer Pool laden — weil das Row-Store-Format keine selektive Spaltenladung kennt. Unnötige I/O-Last, verschwendeter Speicher.
FIX:
|
-- Breite Tabelle vertikal aufteilen: häufig genutzter Kern + selten genutzte Erweiterung -- VORHER: Eine breite Tabelle mit 80 Spalten -- NACHHER: Kern-Tabelle + Erweiterungs-Tabelle (1:1-Beziehung)
CREATE TABLE Kunden_Kern ( KundeID INT IDENTITY PRIMARY KEY, KundeName NVARCHAR(100) NOT NULL, Email NVARCHAR(200) NOT NULL, PLZ NVARCHAR(10) NOT NULL, -- 5–10 häufig genutzte Stammspalten );
CREATE TABLE Kunden_Erweiterung ( KundeID INT PRIMARY KEY REFERENCES Kunden_Kern(KundeID), Geburtstag DATE NULL, Kreditlimit DECIMAL(12,2) NULL, Notizen NVARCHAR(MAX) NULL, MarketingEinwilligung BIT NULL, -- Weitere selten genutzte Spalten ); -- Abfragen auf Kern sind schlank, voller Join nur wenn nötig |
Ein Kunde ändert seine Adresse. Alle alten Bestellungen zeigen jetzt die neue Adresse. War die Lieferadresse damals richtig? Niemand weiß es mehr. Für Buchhaltung, Revision und DSGVO-Auskunft ein reales Problem.
FIX:
|
-- Slowly Changing Dimension Typ 2 (SCD2): Historisierung durch Versionierung CREATE TABLE Kunden_Historisch ( KundeHistID INT IDENTITY PRIMARY KEY, KundeID INT NOT NULL, -- Business Key, stabil KundeName NVARCHAR(100) NOT NULL, Strasse NVARCHAR(200) NOT NULL, PLZ NVARCHAR(10) NOT NULL, Stadt NVARCHAR(100) NOT NULL, GueltigVon DATE NOT NULL, GueltigBis DATE NULL, -- NULL = aktueller Datensatz IstAktuell BIT NOT NULL DEFAULT 1, CONSTRAINT UQ_Kunden_Aktuell UNIQUE (KundeID, GueltigVon) );
-- Aktuellen Datensatz lesen: SELECT * FROM Kunden_Historisch WHERE KundeID = 42 AND IstAktuell = 1;
-- Historischen Zustand zu einem Datum lesen: SELECT * FROM Kunden_Historisch WHERE KundeID = 42 AND GueltigVon <= '2024-06-15' AND (GueltigBis IS NULL OR GueltigBis > '2024-06-15'); |
Eine berechnete Spalte BestellungGesamtBetrag wird redundant in der Bestellkopftabelle gespeichert — und bei Änderungen in Bestellzeilen nicht aktualisiert. Nach drei Monaten divergieren Kopf- und Zeilensumme. Reports zeigen je nach Abfrage unterschiedliche Zahlen.
FIX:
|
-- Option A: Computed Column (SQL Server berechnet automatisch, nie inkonsistent) ALTER TABLE dbo.Bestellungen ADD GesamtBetrag_Berechnet AS ( SELECT SUM(Menge * Einzelpreis) FROM dbo.Bestellzeilen bz WHERE bz.BestellID = BestellID ); -- Achtung: Subquery in computed column ist in SQL Server nicht direkt möglich -- → stattdessen: Indexed View oder Trigger
-- Option B: Indexed View (materialisiert, automatisch aktuell, indexierbar) CREATE VIEW vw_Bestellungen_Summen WITH SCHEMABINDING AS SELECT bz.BestellID, SUM(bz.Menge * bz.Einzelpreis) AS GesamtBetrag, COUNT_BIG(*) AS Zeilenanzahl FROM dbo.Bestellzeilen bz GROUP BY bz.BestellID;
CREATE UNIQUE CLUSTERED INDEX IX_vw_Bestellungen_Summen ON vw_Bestellungen_Summen (BestellID); -- SQL Server hält die View automatisch aktuell — keine Inkonsistenz möglich |
03
Als durchgängiges Beispiel dient eine typische gewachsene Bestelltabelle aus einem KMU:
|
AUSGANGSTABELLE: Bestellungen_Roh (nicht normalisiert) ══════════════════════════════════════════════════════════════ BestellNr │ KundeID │ KundeName │ KundenPLZ │ KundenStadt │ ArtikelIDs │ ArtikelPreise │ Lieferant │ LiefAdr ──────────┼─────────┼───────────┼───────────┼─────────────┼────────────────┼────────────────┼───────────┼──────── B-001 │ K1 │ Müller AG │ 99423 │ Weimar │ A10,A12 │ 29.99,14.50 │ Liefex │ Leipzig B-002 │ K2 │ Schmidt │ 80333 │ München │ A10 │ 29.99 │ Liefex │ Berlin B-003 │ K1 │ Müller AG │ 99423 │ Weimar │ A15,A10,A20 │ 5.00,29.99,8.75│ FastShip │ Leipzig ══════════════════════════════════════════════════════════════ Probleme: Mehrere Werte in ArtikelIDs/Preise, KundenPLZ/Stadt von KundeID abhängig, nicht von BestellNr |
Anforderung: Alle Attributwerte sind atomar (unteilbar). Keine Mengen, keine Listen, keine Wiederholungsgruppen in einer Zelle.
|
-- NACH 1NF: Bestellzeilen aus der Kopftabelle herauslösen -- Jede Zeile ist atomar — keine Listen mehr
-- Tabelle 1: Bestellkopf CREATE TABLE Bestellkopf_1NF ( BestellNr NVARCHAR(10) NOT NULL, KundeID NVARCHAR(10) NOT NULL, KundeName NVARCHAR(100) NOT NULL, KundenPLZ NVARCHAR(10) NOT NULL, KundenStadt NVARCHAR(100) NOT NULL, Lieferant NVARCHAR(100) NOT NULL, LieferAdr NVARCHAR(100) NOT NULL, PRIMARY KEY (BestellNr) );
-- Tabelle 2: Bestellzeilen (eine Zeile je Artikel) CREATE TABLE Bestellzeilen_1NF ( BestellNr NVARCHAR(10) NOT NULL, ArtikelID NVARCHAR(10) NOT NULL, Einzelpreis DECIMAL(10,2) NOT NULL, PRIMARY KEY (BestellNr, ArtikelID) ); -- 1NF erfüllt: jede Zelle enthält genau einen Wert |
Anforderung: 1NF erfüllt + jedes Nicht-Schlüsselattribut ist vollständig vom gesamten Primärschlüssel abhängig. Keine partiellen Abhängigkeiten (nur relevant bei zusammengesetzten Schlüsseln).
|
PROBLEM IN Bestellzeilen_1NF: PK = (BestellNr, ArtikelID) Einzelpreis hängt von BEIDEN Schlüsselteilen ab → OK Artikelbezeichnung (falls vorhanden) hängt nur von ArtikelID ab → Verletzung! |
|
-- NACH 2NF: Artikelinformationen auslagern CREATE TABLE Artikel_2NF ( ArtikelID NVARCHAR(10) NOT NULL PRIMARY KEY, Bezeichnung NVARCHAR(200) NOT NULL, -- Weitere artikelspezifische Attribute hier );
CREATE TABLE Bestellzeilen_2NF ( BestellNr NVARCHAR(10) NOT NULL, ArtikelID NVARCHAR(10) NOT NULL REFERENCES Artikel_2NF(ArtikelID), Einzelpreis DECIMAL(10,2) NOT NULL, -- Preis zum Bestellzeitpunkt Menge INT NOT NULL DEFAULT 1, PRIMARY KEY (BestellNr, ArtikelID) ); -- 2NF erfüllt: alle Nicht-Schlüsselattribute hängen vom vollständigen PK ab |
Anforderung: 2NF erfüllt + keine transitiven Abhängigkeiten. Kein Nicht-Schlüsselattribut darf von einem anderen Nicht-Schlüsselattribut abhängen.
|
PROBLEM IN Bestellkopf_1NF: BestellNr → KundeID → KundeName, KundenPLZ, KundenStadt KundeName, PLZ, Stadt hängen von KundeID ab (nicht von BestellNr) → transitiv! Außerdem: KundenPLZ → KundenStadt (PLZ bestimmt die Stadt) → ebenfalls transitiv! |
|
-- NACH 3NF: alle transitiven Abhängigkeiten aufgelöst CREATE TABLE Staedte_3NF ( PLZ NVARCHAR(10) NOT NULL PRIMARY KEY, Stadt NVARCHAR(100) NOT NULL );
CREATE TABLE Kunden_3NF ( KundeID NVARCHAR(10) NOT NULL PRIMARY KEY, KundeName NVARCHAR(100) NOT NULL, PLZ NVARCHAR(10) NOT NULL REFERENCES Staedte_3NF(PLZ) );
CREATE TABLE Bestellkopf_3NF ( BestellNr NVARCHAR(10) NOT NULL PRIMARY KEY, KundeID NVARCHAR(10) NOT NULL REFERENCES Kunden_3NF(KundeID), Lieferant NVARCHAR(100) NOT NULL, LieferAdr NVARCHAR(100) NOT NULL -- KundeName, PLZ, Stadt: weg! kommen per JOIN aus Kunden_3NF ); -- 3NF erfüllt: keine transitiven Abhängigkeiten mehr |
Anforderung: Verschärfung der 3NF — jede Abhängigkeit muss von einem Superschlüssel ausgehen. Relevant wenn mehrere überlappende Kandidatenschlüssel existieren.
|
BEISPIEL FÜR BCNF-VERLETZUNG: Tabelle: Kurs_Raum_Zeitraum(Kurs, Raum, Zeitraum) Abhängigkeiten: (Kurs, Zeitraum) → Raum (ein Kurs hat zu einer Zeit genau einen Raum) (Raum, Zeitraum) → Kurs (ein Raum hat zu einer Zeit genau einen Kurs) Beide sind Kandidatenschlüssel → 3NF erfüllt Zusätzlich: Raum → Gebäude (Raum bestimmt das Gebäude) Gebäude hängt von Raum ab, nicht von einem Superschlüssel → BCNF-Verletzung! |
|
-- NACH BCNF: Raum-Gebäude-Abhängigkeit auslagern CREATE TABLE Räume ( RaumID INT IDENTITY PRIMARY KEY, Raum NVARCHAR(20) NOT NULL UNIQUE, Gebäude NVARCHAR(50) NOT NULL ); CREATE TABLE Kurs_Belegung ( Kurs NVARCHAR(50) NOT NULL, Zeitraum NVARCHAR(50) NOT NULL, RaumID INT NOT NULL REFERENCES Räume(RaumID), PRIMARY KEY (Kurs, Zeitraum), UNIQUE (RaumID, Zeitraum) ); |
|
|
WIE WEIT NORMALISIEREN? Für produktive OLTP-Datenbanken ist 3NF der pragmatische Zielzustand. BCNF und höhere Normalformen (4NF, 5NF) sind in akademischen Kontexten relevant — in KMU-Produktivumgebungen überwiegt meist der Aufwand den Nutzen. Fokussieren Sie auf 3NF und behandeln Sie spezifische Anomalien gezielt. |
04
Anomalien sind die direkte, sichtbare Konsequenz unnormalisierter Schemas. Sie entstehen immer dort, wo Daten redundant gespeichert sind — und machen sich im täglichen Betrieb als Inkonsistenzen, Fehler und Mehraufwand bemerkbar.
|
BEISPIELTABELLE (nicht normalisiert — 2NF verletzt): ══════════════════════════════════════════════════════════════ BestellID │ ArtikelID │ ArtikelName │ KategorieID │ KatName ──────────┼───────────┼───────────────┼─────────────┼──────── 1001 │ A10 │ Kaffeemaschine│ K3 │ Küche 1002 │ A10 │ Kaffeemaschine│ K3 │ Küche 1003 │ A15 │ USB-Kabel │ K7 │ Elektro ══════════════════════════════════════════════════════════════ |
Insert-Anomalie: Eine neue Kategorie K9 "Garten" kann nicht eingefügt werden — solange kein Artikel dieser Kategorie bestellt wurde. Die Kategorie existiert als Konzept, aber die Tabelle erlaubt keinen Eintrag ohne BestellID und ArtikelID.
Update-Anomalie: Der Name von Kategorie K3 soll von "Küche" auf "Haushalt & Küche" geändert werden. Die Änderung muss in jeder Zeile vorgenommen werden, die K3 enthält — bei 50.000 Bestellzeilen für Artikel A10 sind das 50.000 Updates. Wird auch nur eine Zeile vergessen, ist die Datenbank inkonsistent.
Delete-Anomalie: Wird Bestellung 1003 storniert und gelöscht, verschwindet auch die Information, dass Kategorie K7 "Elektro" heißt — weil diese Information nur in der gelöschten Zeile existiert.
|
Anomalie |
Direkte Kosten |
Indirekte Kosten |
|
Insert-Anomalie |
Manuelle Workarounds, Dummy-Datensätze |
Unvollständige Stammdaten, falsche Auswertungen |
|
Update-Anomalie |
Massenupdate-Aufwand, Batch-Jobs |
Inkonsistente Reports, Vertrauensverlust in Daten |
|
Delete-Anomalie |
Datenverlust beim Löschen |
Fehlende Referenzinformationen, defekte Historien |
|
-- Update-Anomalie-Risiko: Spalten mit identischen Werten je Fremdschlüssel prüfen -- (Zeigt, wo Redundanz Update-Anomalien erzeugt) SELECT ArtikelID, COUNT(DISTINCT ArtikelName) AS Namens_Varianten, COUNT(DISTINCT KategorieID) AS Kategorie_Varianten, COUNT(*) AS Zeilen_Gesamt FROM dbo.Bestellzeilen_Denorm GROUP BY ArtikelID HAVING COUNT(DISTINCT ArtikelName) > 1 -- Inkonsistente Namen! OR COUNT(DISTINCT KategorieID) > 1; -- Inkonsistente Kategorien! |
05
|
ENTSCHEIDUNGSBAUM: NORMALISIEREN ODER DENORMALISIEREN? ══════════════════════════════════════════════════════════════ Schritt 1: WAS IST DAS PRIMÄRE ZUGRIFFSMMUSTER?
→ Überwiegend SCHREIBEN (INSERT/UPDATE/DELETE): Transaktionales System, ERP, CRM, Applikationsdatenbank → NORMALISIEREN bis 3NF (OLTP-Empfehlung)
→ Überwiegend LESEN (SELECT, Reports, Dashboards): Data Warehouse, Reporting-Datenbank, BI-Layer → Weiter zu Schritt 2
Schritt 2: WIE KOMPLEX SIND DIE LESEABFRAGEN?
→ Einfache Abfragen, wenige JOINs, überschaubare Datenmenge: Normalisiertes Schema oft ausreichend — zuerst messen!
→ Viele JOINs über 5+ Tabellen, komplexe Aggregationen, große Datenmenge (>10M Zeilen): → Denormalisierung erwägen (Schritt 3)
Schritt 3: WIE HÄUFIG ÄNDERN SICH DIE DATEN?
→ Daten ändern sich häufig (täglich oder öfter): Denormalisierung mit Konsistenzmechanismus (Trigger, Indexed View) oder mäßige Denormalisierung → Star Schema mit täglichem ETL
→ Daten ändern sich selten oder werden nur hinzugefügt: Aggressive Denormalisierung möglich → Flache Tabellen, Wide Tables, Columnstore
Schritt 4: IST DATENINTEGRITÄT KRITISCH?
→ Ja (Finanzdaten, Medizin, Recht): Normalisierung bevorzugen — Integrität > Performance Denormalisierung nur mit strengen Constraints
→ Nein (Logs, Analysedaten, historische Snapshots): Denormalisierung uneingeschränkt einsetzbar ══════════════════════════════════════════════════════════════ |
|
Systemtyp |
Empfehlung |
Begründung |
|
OLTP (ERP, CRM, App) |
3NF — vollständig normalisiert |
Datenintegrität, Schreibperformance, Erweiterbarkeit |
|
Data Warehouse (DWH) |
Star Schema (denormalisiert) |
Leseperformance, BI-Tool-Kompatibilität, Einfachheit |
|
Operational Data Store (ODS) |
3NF mit ausgewählter Denorm. |
Nahe an OLTP-Quelle, leichte Auswertbarkeit |
|
Data Mart |
Star Schema oder flach |
Optimiert für spezifische Auswertungsdomäne |
|
Data Vault |
Normalisiert (Hub/Link/Sat) |
Flexibilität, Historisierung, Integration |
|
Reporting Layer |
Denormalisiert / Wide Tables |
Maximale Leseperformance, minimale JOIN-Komplexität |
|
Log-Datenbank |
Denormalisiert oder append-only |
Schreibgeschwindigkeit, keine Updates |
|
-- Vor jeder Denormalisierungsentscheidung: -- Aktuelle Abfragekosten des normalisierten Schemas messen
SET STATISTICS TIME ON; SET STATISTICS IO ON;
-- Normalisierte Abfrage (mit JOINs): SELECT b.BestellID, k.KundeName, a.Bezeichnung, bz.Menge * bz.Einzelpreis AS Umsatz FROM dbo.Bestellkopf b JOIN dbo.Kunden k ON b.KundeID = k.KundeID JOIN dbo.Bestellzeilen bz ON b.BestellID = bz.BestellID JOIN dbo.Artikel a ON bz.ArtikelID = a.ArtikelID WHERE b.Bestelldatum >= '2025-01-01';
SET STATISTICS TIME OFF; SET STATISTICS IO OFF; -- Ergebnis notieren: elapsed time, logical reads -- DANN Denormalisierung umsetzen und erneut messen -- Nur wenn Verbesserung > 20 %: Denormalisierung behalten |
06
Strategie 1 — Abgeleitete Spalten materialisieren
Statt jede Abfrage Menge * Einzelpreis zu berechnen, wird der Zeilenbetrag als Spalte gespeichert. Spart Rechenaufwand bei häufigen Aggregationen — erfordert Konsistenzsicherung.
|
-- Mit Trigger sichergestellt (Konsistenz garantiert): ALTER TABLE dbo.Bestellzeilen ADD ZeilenBetrag AS (Menge * Einzelpreis) PERSISTED; -- PERSISTED: SQL Server speichert den Wert physisch und hält ihn aktuell -- Kein Trigger nötig — SQL Server ist für Konsistenz verantwortlich
-- Für komplexere Berechnungen: Trigger CREATE OR ALTER TRIGGER trg_Bestellzeilen_ZeilenBetrag ON dbo.Bestellzeilen AFTER INSERT, UPDATE AS BEGIN UPDATE bz SET ZeilenBetrag = bz.Menge * bz.Einzelpreis FROM dbo.Bestellzeilen bz JOIN inserted i ON bz.BestellZeilID = i.BestellZeilID; END; |
Strategie 2 — Häufig genutzte JOINs vorberechnen (Denorm-Tabelle)
|
-- Denormalisierte Reporting-Tabelle: täglich per ETL befüllt CREATE TABLE dbo.Reporting_Umsatz_Flat ( -- Alle relevanten Spalten aus 4 normalisierten Tabellen: BestellID INT NOT NULL, Bestelldatum DATE NOT NULL, KundeID INT NOT NULL, KundeName NVARCHAR(100) NOT NULL, KundenRegion NVARCHAR(50) NOT NULL, ArtikelID INT NOT NULL, ArtikelName NVARCHAR(200) NOT NULL, Kategorie NVARCHAR(100) NOT NULL, Menge INT NOT NULL, Einzelpreis DECIMAL(10,2) NOT NULL, ZeilenBetrag DECIMAL(12,2) NOT NULL, -- Zeitdimensionen für BI-Aggregationen: Jahr SMALLINT NOT NULL, Quartal TINYINT NOT NULL, Monat TINYINT NOT NULL, INDEX CCI_Umsatz_Flat CLUSTERED COLUMNSTORE -- Optimal für BI-Abfragen ); -- ETL füllt täglich: alle Änderungen des Vortages -- Power BI greift auf diese Tabelle zu — nie auf OLTP-Tabellen direkt |
Strategie 3 — Lookup-Werte inline speichern (historische Korrektheit)
|
-- Preis zum Bestellzeitpunkt speichern — kein JOIN auf Preistabelle nötig -- UND historisch korrekt: Preisänderungen beeinflussen alte Bestellungen nicht CREATE TABLE dbo.Bestellzeilen ( BestellZeilID INT IDENTITY PRIMARY KEY, BestellID INT NOT NULL REFERENCES Bestellkopf(BestellID), ArtikelID INT NOT NULL REFERENCES Artikel(ArtikelID), ArtikelName NVARCHAR(200) NOT NULL, -- Snapshot zum Bestellzeitpunkt Einzelpreis DECIMAL(10,2) NOT NULL, -- Snapshot zum Bestellzeitpunkt Menge INT NOT NULL, ZeilenBetrag AS (Menge * Einzelpreis) PERSISTED ); -- Doppelspeicherung von ArtikelName ist hier KEIN Fehler — sondern Absicht: -- Historische Korrektheit ist wertvoller als Normalisierungsreinheit |
Strategie 4 — Aggregationstabellen (Summary Tables)
|
-- Vorberechnete Monatssummen: ersetzt teure Aggregationsabfragen CREATE TABLE dbo.Umsatz_Monats_Aggregat ( Jahr SMALLINT NOT NULL, Monat TINYINT NOT NULL, KundeID INT NOT NULL, ArtikelID INT NOT NULL, Umsatz_Summe DECIMAL(14,2) NOT NULL, Zeilen_Anzahl INT NOT NULL, LetzteAktualisierung DATETIME2 NOT NULL DEFAULT SYSDATETIME(), PRIMARY KEY (Jahr, Monat, KundeID, ArtikelID) ); -- Befüllung: SQL Agent Job täglich nach Geschäftsschluss -- Power BI lädt diese Tabelle in Sekunden statt Minuten |
07
Das Star Schema ist das am häufigsten eingesetzte Datenbankdesign für analytische Systeme — und das bewährteste Denormalisierungsmuster für BI und Data Warehouses.
|
STAR SCHEMA: UMSATZ-ANALYSE ══════════════════════════════════════════════════════════════ ┌─────────────────┐ │ Dim_Zeit │ │ DatumID (PK) │ │ Datum │ │ Jahr, Quartal │ │ Monat, Woche │ │ Wochentag │ │ IstFeiertag │ └────────┬────────┘ │ ┌──────────────┐ ┌──────▼──────────────┐ ┌─────────────────┐ │ Dim_Kunde │ │ Fact_Umsatz │ │ Dim_Artikel │ │ KundeID (PK) ├───►│ DatumID (FK) │◄───┤ ArtikelID (PK) │ │ KundeName │ │ KundeID (FK) │ │ ArtikelName │ │ Segment │ │ ArtikelID (FK) │ │ Kategorie │ │ Region │ │ RegionID (FK) │ │ Unterkategorie │ │ Land │ │ ── Maßzahlen ── │ │ Marke │ └──────────────┘ │ Umsatz_EUR │ │ EAN │ │ Menge │ └─────────────────┘ ┌──────────────┐ │ Deckungsbeitrag │ │ Dim_Region │ │ Kosten │ │ RegionID (PK)├───►│ │ │ Region │ └─────────────────────┘ │ Bundesland │ │ Land │ └──────────────┘ ══════════════════════════════════════════════════════════════ |
|
-- Dimensionstabellen: denormalisiert (alle Attribute flach) CREATE TABLE dbo.Dim_Kunde ( KundeID INT IDENTITY PRIMARY KEY, -- Surrogate Key KundeBizKey NVARCHAR(20) NOT NULL, -- Business Key aus Quelle KundeName NVARCHAR(100) NOT NULL, Segment NVARCHAR(50) NOT NULL, Region NVARCHAR(50) NOT NULL, Bundesland NVARCHAR(50) NOT NULL, Land NVARCHAR(50) NOT NULL DEFAULT 'Deutschland', -- SCD2-Historisierung: GueltigVon DATE NOT NULL, GueltigBis DATE NULL, IstAktuell BIT NOT NULL DEFAULT 1 );
CREATE TABLE dbo.Dim_Artikel ( ArtikelID INT IDENTITY PRIMARY KEY, ArtikelBizKey NVARCHAR(20) NOT NULL, ArtikelName NVARCHAR(200) NOT NULL, -- Bewusst denormalisiert: Kategorie, Unterkategorie, Marke -- Im OLTP wären das separate Tabellen — hier flach für JOIN-freie Abfragen Kategorie NVARCHAR(100) NOT NULL, Unterkategorie NVARCHAR(100) NULL, Marke NVARCHAR(100) NULL, EAN NVARCHAR(13) NULL );
-- Faktentabelle: nur Fremdschlüssel und Maßzahlen CREATE TABLE dbo.Fact_Umsatz ( -- Composite PK aus Fremdschlüsseln (kein Surrogate Key nötig): DatumID INT NOT NULL REFERENCES dbo.Dim_Zeit(DatumID), KundeID INT NOT NULL REFERENCES dbo.Dim_Kunde(KundeID), ArtikelID INT NOT NULL REFERENCES dbo.Dim_Artikel(ArtikelID), RegionID INT NOT NULL REFERENCES dbo.Dim_Region(RegionID), PRIMARY KEY (DatumID, KundeID, ArtikelID, RegionID), -- Additive Maßzahlen (können summiert werden): Umsatz_EUR DECIMAL(14,2) NOT NULL, Menge INT NOT NULL, Kosten_EUR DECIMAL(14,2) NOT NULL, Deckungsbeitrag_EUR DECIMAL(14,2) NOT NULL, -- Non-additive Maßzahlen (können NICHT summiert werden): Marge_Pct DECIMAL(6,3) NULL -- DB/Umsatz — nie summieren! -- Columnstore Index für BI-Performance: INDEX CCI_Fact_Umsatz CLUSTERED COLUMNSTORE ); |
Das Snowflake Schema normalisiert die Dimensionstabellen weiter. Statt Kategorie, Unterkategorie direkt in Dim_Artikel werden separate Dim_Kategorie- und Dim_Unterkategorie-Tabellen angelegt.
→ Vorteil: Weniger Redundanz in Dimensionstabellen, einfachere Stammdatenpflege.
→ Nachteil: Mehr JOINs in BI-Abfragen, schlechtere Power-BI-Kompatibilität, höhere Abfragekosten.
→ Empfehlung für KMU: Star Schema bevorzugen. Snowflake nur wenn Dimensionstabellen sehr groß (> 1 Mio. Zeilen) und Redundanz signifikant ist.
08
Data Vault ist eine Datenbankarchitektur, die die Stärken der Normalisierung (Flexibilität, Erweiterbarkeit, historische Korrektheit) mit der Auswertbarkeit des Star Schemas kombiniert.
|
DATA VAULT: GRUNDSTRUKTUR ══════════════════════════════════════════════════════════════ HUB (normalisiert — Business Keys) Hub_Kunde: HubKundeID, KundeBizKey, LoadDate, RecordSource Hub_Artikel: HubArtikelID, ArtikelBizKey, LoadDate, RecordSource
LINK (normalisiert — Beziehungen zwischen Hubs) Link_Bestellung: LinkBestellID, HubKundeID, HubArtikelID, BestellDatum, LoadDate, RecordSource
SATELLITE (historisiert — Attribute und Kontext) Sat_Kunde_Stamm: HubKundeID, LoadDate, LoadEndDate, KundeName, PLZ, Stadt, HashDiff Sat_Artikel_Preis: HubArtikelID, LoadDate, LoadEndDate, Preis, Waehrung, HashDiff
→ INFORMATION MART (denormalisiert — Star Schema für BI) Fact_Umsatz, Dim_Kunde, Dim_Artikel: per ETL aus Data Vault ══════════════════════════════════════════════════════════════ |
→ Viele Quellsysteme (ERP + CRM + externe Daten): Data Vault integriert heterogene Quellen ohne Konflikte.
→ Häufige Schemaänderungen: Neue Attribute werden als neue Satellites hinzugefügt — ohne bestehende Strukturen zu ändern.
→ Strikte Auditierbarkeit: Jeder Datensatz enthält Ladedatum und Quelle — vollständige Nachvollziehbarkeit.
→ Nicht geeignet: Kleine Umgebungen (< 5 Mio. Datensätze, 1–2 Quellsysteme) — Overhead überwiegt den Nutzen.
09
|
-- Columnstore auf normalisierter Tabelle: oft besser als Denormalisierung -- SQL Server komprimiert Spalten einzeln → hohe Kompressionsrate -- Batch Mode Processing: Aggregationen über Millionen Zeilen in Sekunden
-- Nicht-geclusterter Columnstore (NCI) — OLTP + Analyse gleichzeitig: CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_Bestellzeilen ON dbo.Bestellzeilen (ArtikelID, Menge, Einzelpreis, Bestelldatum); -- OLTP-Operationen nutzen den Row-Store → kein Konflikt -- BI-Abfragen nutzen den Columnstore → schnelle Aggregationen
-- Geclusterter Columnstore (CCI) — reine Analysetabellen: CREATE CLUSTERED COLUMNSTORE INDEX CCI_Fact_Umsatz ON dbo.Fact_Umsatz; -- Alle Daten im Columnstore-Format → maximale Kompression und Leseperformance -- INSERT/UPDATE/DELETE möglich, aber langsamer als Row-Store |
|
-- Partitionierung: ältere Daten auf günstigeren Storage auslagern -- Ohne Schema-Änderung — Abfragen laufen transparent
-- Partitionsfunktion: nach Jahr CREATE PARTITION FUNCTION pf_Jahr (DATE) AS RANGE RIGHT FOR VALUES ('2022-01-01','2023-01-01','2024-01-01','2025-01-01','2026-01-01');
-- Partitionsschema: Dateigruppen je Jahr CREATE PARTITION SCHEME ps_Jahr AS PARTITION pf_Jahr TO (FG_2021_Alt, FG_2022, FG_2023, FG_2024, FG_2025, FG_2026_Aktuell);
-- Tabelle partitioniert anlegen: CREATE TABLE dbo.Fact_Umsatz_Partitioniert ( Bestelldatum DATE NOT NULL, KundeID INT NOT NULL, ArtikelID INT NOT NULL, Umsatz_EUR DECIMAL(14,2) NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE ) ON ps_Jahr (Bestelldatum);
-- Partition Elimination: Abfragen auf 2025 lesen nur FG_2025 → viel schneller SELECT SUM(Umsatz_EUR) FROM dbo.Fact_Umsatz_Partitioniert WHERE Bestelldatum >= '2025-01-01' AND Bestelldatum < '2026-01-01'; |
|
-- In-Memory-Tabelle: bis zu 30× schneller für hochfrequente Transaktionen -- Sinnvoll für: Session-Tabellen, Warteschlangen, Echtzeit-Logging
-- Filegroup für In-Memory vorbereiten (einmalig je Datenbank): ALTER DATABASE IhreDatenbank ADD FILEGROUP FG_InMemory CONTAINS MEMORY_OPTIMIZED_DATA; ALTER DATABASE IhreDatenbank ADD FILE (NAME='InMemory', FILENAME='D:\InMemory\IhrDB_InMem') TO FILEGROUP FG_InMemory;
-- In-Memory-Tabelle (Schema nur minimal — nicht für komplexe OLTP): CREATE TABLE dbo.Auftrags_Queue ( QueueID BIGINT IDENTITY NOT NULL CONSTRAINT PK_Queue PRIMARY KEY NONCLUSTERED, ArtikelID INT NOT NULL, Menge INT NOT NULL, Eingang DATETIME2 NOT NULL DEFAULT SYSDATETIME(), Status TINYINT NOT NULL DEFAULT 0 INDEX IX_Status HASH (Status) WITH (BUCKET_COUNT = 1024) ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); -- DURABILITY = SCHEMA_ONLY: Daten nach Restart weg (für reine Puffer) -- DURABILITY = SCHEMA_AND_DATA: Daten dauerhaft (mit Write-Ahead-Log) |
10
Dieser Plan führt ein DBA-Team oder einen IT-Verantwortlichen in 30 Tagen durch eine vollständige Schema-Analyse eines bestehenden SQL-Server-Systems — von der Bestandsaufnahme über die Priorisierung bis zur ersten messbaren Verbesserung.
|
|
VOR DEM START Erstellen Sie ein vollständiges Backup aller betroffenen Datenbanken. Aktivieren Sie den Query Store auf allen analysierten Datenbanken — er ist die wichtigste Messbasis für Vorher-Nachher-Vergleiche. Vereinbaren Sie mit den Fachbereichen: Schema-Redesign kann sich auf Applikationsverhalten auswirken — Tests in einer Staging-Umgebung sind Pflicht, nicht optional. |
■ TAG 1–2: SCHEMA-INVENTUR
■ Alle Tabellen mit Zeilenanzahl, Spaltenanzahl und Größe inventarisieren
■ Tabellen ohne Primärschlüssel identifizieren — kritisch, sofort melden
■ Tabellen ohne Fremdschlüssel-Constraints identifizieren — Integritätsrisiko prüfen
■ Tabellen mit > 50 Spalten identifizieren — Kandidaten für vertikale Partitionierung
|
-- Schema-Übersicht: alle Tabellen mit Größe und Zeilenanzahl SELECT SCHEMA_NAME(t.schema_id) AS Schema_Name, t.name AS Tabelle, p.rows AS Zeilen_Anzahl, SUM(a.total_pages) * 8 / 1024 AS Groesse_MB, COUNT(c.column_id) AS Spalten_Anzahl, CASE WHEN pk.object_id IS NULL THEN 'KEIN PK!' ELSE 'OK' END AS PK_Status FROM sys.tables t JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0,1) JOIN sys.allocation_units a ON p.partition_id = a.container_id JOIN sys.columns c ON t.object_id = c.object_id LEFT JOIN ( SELECT i.object_id FROM sys.indexes i WHERE i.is_primary_key = 1 ) pk ON t.object_id = pk.object_id WHERE SCHEMA_NAME(t.schema_id) <> 'sys' GROUP BY SCHEMA_NAME(t.schema_id), t.name, p.rows, pk.object_id ORDER BY Groesse_MB DESC; |
■ TAG 3–4: ANOMALIE-ANALYSE
■ Update-Anomalie-Diagnose-Skript (Kapitel 4) auf Top-20-Tabellen ausführen
■ Spalten mit kommaseparierten Werten manuell identifizieren (Kapitel 2, Falle 01)
■ NULL-Nutzung analysieren: Spalten mit > 80 % NULL-Werten dokumentieren
■ Fehlende FK-Constraints: Referenzielle Integrität manuell prüfen
■ TAG 5–7: ABFRAGEMUSTER ANALYSIEREN
■ Top-20-Abfragen aus Query Store (DMV 1 aus Query-Kit) exportieren
■ JOINs pro Abfrage zählen: Abfragen mit > 5 JOINs sind Denormalisierungskandidaten
■ Häufig gemeinsam abgefragte Spalten identifizieren — Basis für Denorm-Entscheidung
■ Abfragen mit hohen Logical Reads bei gleichzeitig hoher Ausführungsfrequenz priorisieren
■ TAG 8–10: KRITISCHE INTEGRITÄT HERSTELLEN
■ Alle Tabellen ohne Primärschlüssel bekommen einen Surrogate-Key-PK (ArtikelID INT IDENTITY)
■ Waisen-Datensätze bereinigen: referenzierte IDs ohne gültige Eltern-Zeile löschen oder korrigieren
■ FK-Constraints für kritische Beziehungen (Bestellzeilen → Artikel, Bestellzeilen → Bestellkopf) anlegen
■ NULL-Bedeutungen dokumentieren: Für jede nullable Spalte: Was bedeutet NULL hier?
■ TAG 11–13: ERSTE NORMALFORMEN ANWENDEN
■ Kommaseparierte Spaltenwerte: Drei Kandidatentabellen zur Auflösung identifizieren und priorisieren
■ Wiederholende Spaltengruppen (Telefon1/2/3): Auflösung in separate Entität planen
■ Für den häufigsten Anomalie-Kandidaten: vollständige 3NF-Umstrukturierung in Staging-Umgebung
■ Vorher-Nachher-Messung: Abfragezeit und Logical Reads vor und nach 3NF dokumentieren
■ TAG 14: UPDATE-ANOMALIE-TEST
■ In Staging-Umgebung: Update-Anomalie provozieren (Adresse eines Kunden in OLTP-Roh-Tabelle ändern)
■ Zeigen: wie viele Zeilen müssen aktualisiert werden? Wie viele sind nach Update inkonsistent?
■ Danach 3NF-Version: Update der Adresse in einer Zeile — alle Referenzen automatisch korrekt
■ Ergebnis als Management-Summary aufbereiten: "Das ist, was uns die fehlende Normalisierung kostet"
■ TAG 15–17: REPORTING-LAYER AUFBAUEN
■ Top-5 teuersten Report-Abfragen (aus Woche 1 Analyse) identifizieren
■ Für jede: JOIN-Kette analysieren — welche Tabellen werden immer gemeinsam gelesen?
■ Flache Reporting-Tabelle (Strategie 2, Kapitel 6) für die häufigste Abfragedomäne entwerfen
■ ETL-Job erstellen: tägliche Befüllung der Reporting-Tabelle nach Geschäftsschluss
■ TAG 18–20: STAR SCHEMA FÜR BI
■ Falls Power BI im Einsatz: Star-Schema-Design (Kapitel 7) für die wichtigste Auswertungsdomäne
■ Fact-Tabelle und 3–4 Dimensionen in Staging-Umgebung anlegen und befüllen
■ Power BI auf Star Schema umstellen: Measures, Beziehungen, RLS prüfen
■ Performance-Messung: Dashboard-Ladezeit vorher vs. nachher dokumentieren
■ TAG 21: COLUMNSTORE EINFÜHREN
■ Alle reinen Analyse-/Reporting-Tabellen auf Clustered Columnstore umstellen
■ Für OLTP-Tabellen mit Analysebedarf: Non-Clustered Columnstore Index testen
■ Performance-Vergleich: identische Abfrage mit und ohne Columnstore
■ Typisches Ergebnis dokumentieren und mit Team teilen
■ TAG 22–25: HISTORISIERUNG EINRICHTEN
■ Kritische veränderliche Stammdaten identifizieren: Kunden-Adressen, Preise, Artikel-Stamm
■ SCD2-Historisierung (Kapitel 2, Falle 09) für die wichtigste Stammdatentabelle einrichten
■ SQL Server Temporal Tables als Alternative zu manueller SCD2 evaluieren
■ Temporal Table für eine Pilot-Tabelle einrichten:
|
-- SQL Server Temporal Table: automatische Historisierung ALTER TABLE dbo.Kunden ADD GueltigVon DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL, GueltigBis DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL, PERIOD FOR SYSTEM_TIME (GueltigVon, GueltigBis);
ALTER TABLE dbo.Kunden SET (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.Kunden_Historie )); -- Ab jetzt: SQL Server schreibt automatisch jede Änderung in Kunden_Historie -- Zeitreisende Abfrage: SELECT * FROM dbo.Kunden FOR SYSTEM_TIME AS OF '2024-06-15 00:00:00'; |
■ TAG 26–28: KONSISTENZMECHANISMEN ABSICHERN
■ Alle denormalisierten Spalten und Tabellen auf Konsistenz prüfen: Gibt es Trigger oder Constraints?
■ Fehlende Konsistenz-Sicherung nachziehen: PERSISTED Computed Columns oder Indexed Views
■ Dokumentation aller Denormalisierungsentscheidungen: Warum, was, Konsistenzmechanismus, Review-Termin
■ Schema-Dokumentation im Wiki aktualisieren: Lineage-Dokumente (Lineage-Kit) für neue Tabellen anlegen
■ TAG 29–30: ABSCHLUSS & MESSUNG
■ Vorher-Nachher-Vergleich: Top-5-Abfragen Baseline vs. aktuell — Verbesserungsfaktor je Abfrage
■ Schema-Qualitäts-KPIs dokumentieren: Anteil Tabellen mit PK, FK-Abdeckung, Anomalie-Score
■ Offene Punkte in Backlog priorisieren: Welche weiteren Tabellen werden als nächstes normalisiert?
■ Ergebnisse präsentieren: Zahlen, Fakten, Messbarkeit — der ROI des Schema-Redesigns ■
|
|
ERGEBNIS NACH 30 TAGEN Ihr Ergebnis nach 30 Tagen: Vollständige Schema-Inventur mit dokumentierten Normalisierungslücken, behobene kritische Integritätsprobleme (fehlende PKs, FKs), eine denormalisierte Reporting-Tabelle mit messbarer Performance-Verbesserung, ein Star-Schema-Entwurf für die wichtigste BI-Domäne, Columnstore-Indizes auf Analyse-Tabellen — und eine dokumentierte Grundlage für alle weiteren Schema-Entscheidungen. |
Die in diesem Dokument enthaltenen SQL-Skripte, Designmethoden und Architekturempfehlungen wurden nach bestem Wissen und Gewissen auf Basis langjähriger praktischer Erfahrung in der Datenbankarchitektur und -entwicklung erstellt. Dennoch kann keine Gewähr für Vollständigkeit, Richtigkeit oder universelle Anwendbarkeit übernommen werden.
Der Autor übernimmt ausdrücklich keinerlei Haftung für Schäden, die aus der Anwendung der beschriebenen Methoden und Skripte entstehen, einschließlich, aber nicht beschränkt auf: Datenverlust, Anwendungsfehler, Performance-Regression oder Systemausfälle. Schema-Änderungen an Produktionsdatenbanken sind stets mit vollständigem Backup und ausreichendem Test in einer Nicht-Produktionsumgebung vorzubereiten.
Features wie In-Memory OLTP (Hekaton), Clustered Columnstore Index, Temporal Tables, JSON-Unterstützung und PERSISTED Computed Columns sind teilweise versionsspezifisch. Prüfen Sie vor dem Einsatz die Verfügbarkeit für Ihre SQL-Server-Version. Alle Skripte wurden auf SQL Server 2022 (16.x) und SQL Server 2025 (17.x) getestet.
Die in Kapitel 3 beschriebenen Normalformen basieren auf dem relationalen Datenbankmodell nach E. F. Codd und den ISO/IEC-9075-Standards. Die in diesem Kit beschriebenen Vereinfachungen und Praxisempfehlungen weichen in Details von der akademischen Theorie ab — bewusst, um die Anwendbarkeit im KMU-Kontext zu maximieren.
Dieses Dokument und alle Inhalte sind urheberrechtlich geschützt. © 2026 Sascha Hess, xenosystems.de. Alle Rechte vorbehalten.
SQL Server, Power BI und Azure Synapse Analytics sind eingetragene Marken der Microsoft Corporation. Data Vault ist eine Methodik von Dan Linstedt. Alle anderen genannten Produkt- und Unternehmensnamen sind Eigentum ihrer jeweiligen Inhaber.
Es gilt ausschließlich deutsches Recht. Gerichtsstand für alle Streitigkeiten ist, soweit gesetzlich zulässig, Weimar, Thüringen, Deutschland.
Sascha Hess ist Diplom-Biologe und IT-Professional mit über 20 Jahren Erfahrung in der Administration und Architektur von SQL-Server-Systemen, ERP- und BI-Datenbanken. Er hat mehr als 50 Datenbankschemas analysiert, normalisiert und für analytische Workloads optimiert — von historisch gewachsenen Chaos-Schemas bis zu vollständig modellierten Data-Vault-Architekturen.
Sein Ansatz verbindet naturwissenschaftliche Präzision mit hochgradiger IT-Spezialisierung. Schwerpunkte: SQL Server Performance-Tuning, ERP-Einführungen, Business Intelligence (Power BI, DeltaMaster), Datenbankdesign, Data Governance und IT-Interim-Management.
Web: www.xenosystems.de | E-Mail: info@xenosystems.de | Standort: Weimar, Thüringen / Remote
|
Service |
Beschreibung |
|
Schema-Analyse & Redesign |
Vollständige Analyse Ihres Datenbankschemas: Normalisierungslücken, Anomalien, Integritätsprobleme, Performance-Schwachstellen — mit priorisiertem Redesign-Plan und umsetzbaren Quick Wins. Scope: 2–4 Tage. |
|
DWH-Design & Star Schema |
Entwurf und Implementierung eines Star-Schema-Data-Warehouse auf SQL Server — Faktentabellen, Dimensionen, ETL-Prozesse, Columnstore-Indizes und Power-BI-Integration. |
|
SQL Server DB Health Check |
Professioneller Audit Ihres SQL Servers — Managementreport mit Performance-Analyse, Schema-Bewertung, Indexstrategie und priorisiertem Maßnahmenplan. Scope: 3–5 Tage. |
|
Query-Tuning Session |
1-Tages-Session: DMV-Diagnose, Execution-Plan-Analyse, Rewrite-Empfehlungen und Index-Strategie für Ihre Top-Performance-Probleme. |
|
Interim IT-Management |
Übernahme der IT-Steuerung auf Zeit — Budgetplanung, Dienstleister-Management, strategische IT-Ausrichtung. |
Vollständiges Dokument
Strategische Wissens-Roadmap · Checklisten · Praxisbeispiele
49,90 €
Sichere Zahlung über PayPal · Sofort-Download nach Zahlungseingang