Zum Inhalt springen

12. Normalisierung

Zu Zen-Modus wechseln

Normalisierung — Tabellen entwerfen, die sich nicht wehren

Abschnitt betitelt „Normalisierung — Tabellen entwerfen, die sich nicht wehren“

Bisher haben wir gelernt, wie man Tabellen erstellt, wie man sie abfragt und wie man sie mit Constraints schützt. Aber eine Frage haben wir stillschweigend übersprungen: Welche Spalten gehören überhaupt in welche Tabelle?

Sie können dieselbe Information auf viele verschiedene Arten speichern. Manche Entwürfe funktionieren am ersten Tag einwandfrei und fallen drei Monate später leise auseinander. Normalisierung ist die Menge an Regeln, die jene Entwürfe, die gut altern, von jenen trennt, die es nicht tun. Die Regeln haben formale Namen und mathematische Grundlagen, aber die zugrunde liegende Idee ist einfach: Jede unabhängige Tatsache gehört an genau einen Ort.

Beginnen wir damit, uns anzusehen, wie es schiefgeht.

Stellen Sie sich vor, unsere Schule möchte festhalten, wer welches Fach belegt, wer es unterrichtet und welche Note der Schüler bekommen hat. Jemand (vielleicht in Eile) entscheidet sich, alles in eine einzige große Tabelle zu packen. Sie sieht harmlos genug aus:

student_idstudent_namestudent_phoneclass_nameclass_roomcourse_idcourse_titleteacher_nameteacher_kuerzelgrade
1Anna Gruber0664-111, 0676-2223AHITMR204101DatabasesMr. MaurhartMAO2
1Anna Gruber0664-111, 0676-2223AHITMR204102NetworkingMs. KenderKEN1
2Ben Hofer0699-3333AHITMR204101DatabasesMr. MaurhartMAO3
3Cara Wimmer0660-4444AHITMR110101DatabasseMr. MaurhartMAO2

Auf den ersten Blick beantwortet diese Tabelle jede Frage, die wir haben. Aber bei genauerem Hinsehen fällt sie bereits auseinander:

  • Sie wiederholt sich. Die Klasse 3AHITM und ihr Raum “R204” erscheinen dreimal. “Mr. Maurhart” und sein Kürzel “MAO” erscheinen dreimal. All das wird einmal pro Einschreibung gespeichert, obwohl nichts davon mit irgendeiner einzelnen Note zu tun hat.
  • Sie widerspricht sich. Zeile 4 sagt, der Kurs heiße “Databasse”. Ist das ein Tippfehler oder ein anderer Kurs? Die Tabelle kann es Ihnen nicht sagen, weil die Wahrheit über viele Zeilen verstreut ist, statt an genau einem Ort gespeichert zu sein.
  • Sie mischt mehrere Dinge in eine Zelle. student_phone enthält zwei durch ein Komma getrennte Nummern. Versuchen Sie nun, eine Abfrage zu schreiben, die alle findet, die unter einer 0676-Nummer erreichbar sind - das gelingt nicht sauber, weil die Datenbank eine lange Zeichenkette sieht, nicht zwei Telefonnummern.

Diese eine Tabelle versucht, gleichzeitig Schülerliste, Klassenverzeichnis, Kurskatalog und Notenbuch zu sein. Immer wenn Sie unzusammenhängende Tatsachen zwingen, in derselben Zeile zu leben, erzeugen Sie die Probleme, die wir uns als Nächstes ansehen.

Die Probleme, die aus schlecht entworfenen Tabellen entstehen, haben einen Namen: Anomalien. Eine Anomalie ist etwas, das schiefgeht, wenn Sie versuchen, Daten einzufügen, zu aktualisieren oder zu löschen. Es gibt drei klassische Arten, und es lohnt sich, sie beim Namen zu kennen, weil sie immer wieder auftauchen.

  • Einfüge-Anomalie — Sie können eine Information nicht hinzufügen, weil eine unzusammenhängende Information fehlt.
  • Änderungs-Anomalie — um eine Tatsache zu ändern, müssen Sie sie in vielen Zeilen ändern, und wenn Sie eine übersehen, werden die Daten inkonsistent.
  • Lösch-Anomalie — das Löschen einer Sache zerstört versehentlich eine andere Sache, die Sie behalten wollten.

Sehen wir uns jede davon in unserer kaputten Tabelle an, mit jeweils zwei Beispielen.

Sie möchten eine Tatsache hinzufügen, aber die Tabelle zwingt Sie, auch Tatsachen zu kennen, die nichts damit zu tun haben.

  • Beispiel 1 — Ein neuer Kurs ohne Schüler. Die Schule fügt ein neues Fach “Cyber Security” hinzu, unterrichtet von Frau Schmidt. Sie möchten es jetzt erfassen, bevor irgendeine Klasse ihm zugewiesen wurde. Aber jede Zeile braucht eine student_id und eine grade. Mit dieser Tabelle können Sie schlicht keinen Kurs speichern, der keine Schüler hat — obwohl der Kurs eindeutig existiert.

  • Beispiel 2 — Ein neuer Schüler, der noch keiner Klasse zugeordnet ist. Ein Schüler wechselt mitten im Jahr herein. Die Verwaltung hat noch nicht entschieden, zu welcher Klasse er gehört, also gibt es keinen class_name und keinen class_room zum Eintragen. Es gibt auch noch keine course_id zum Ausfüllen. Mit dieser Tabelle kann der Schüler nicht in der Datenbank existieren, bis auch jede andere Spalte bekannt ist.

Eine einzige Tatsache ist in vielen Zeilen gespeichert. Um sie zu ändern, müssen Sie jede Kopie finden und korrigieren.

  • Beispiel 1 — Die Klasse zieht in einen anderen Raum um. 3AHITM wird wegen einer Renovierung von R204 nach R307 verlegt. Ihr Raum steht in zwei verschiedenen Zeilen (einmal für Anna, einmal für Ben). Sie müssen beide aktualisieren. Vergessen Sie eine, und die Datenbank behauptet nun, 3AHITM belege zwei Räume gleichzeitig.

  • Beispiel 2 — Den Kurstitel korrigieren. Sie bemerken, dass “Databasse” ein Tippfehler ist, und möchten ihn zu “Databases” korrigieren. Aber der Kurstitel ist über mehrere Zeilen dupliziert. Korrigieren Sie eine und übersehen eine andere, ist der Titel weiterhin inkonsistent.

Sie löschen eine Sache, und eine unzusammenhängende Sache verschwindet mit ihr.

  • Beispiel 1 — Ein Schüler belegt ein Fach ab. Cara ist die einzige Schülerin, die in einem bestimmten Kurs eingeschrieben ist. Sie belegt ihn ab, also löschen Sie ihre Zeile. Mit diesem einen Löschvorgang ist auch alles Wissen über diesen Kurs und seine Lehrkraft verschwunden — obwohl der Kurs nächstes Semester weiterläuft.

  • Beispiel 2 — Aufräumen am Jahresende. Sie löschen die Zeilen der Schüler, die ihren Abschluss gemacht haben. Wenn eine Lehrkraft immer nur neben diesen Schülern erschienen ist, verschwinden Name und Kürzel dieser Lehrkraft leise aus der Datenbank — obwohl die Lehrkraft weiterhin im Kollegium ist.

Normalisierung ist der Prozess, Ihre Spalten in gut gewählte Tabellen zu organisieren, sodass jede Tatsache nur einmal gespeichert wird und die obigen Anomalien nicht auftreten können.

Wir tun dies in Stufen, die Normalformen (NF) genannt werden. Jede Normalform ist eine kleine Checkliste. Sie beginnen mit einer unordentlichen Tabelle, wenden die Regel für die Erste Normalform (1NF) an, dann die Zweite Normalform (2NF), dann die Dritte Normalform (3NF). Die Formen bauen aufeinander auf: Eine Tabelle kann nicht in 2NF sein, wenn sie nicht bereits in 1NF ist, und so weiter.

Es gibt auch höhere Formen (BCNF, 4NF, 5NF), aber für nahezu jeden realen Datenbankentwurf ist das Erreichen der 3NF das Ziel. In der Praxis bedeutet “die Datenbank ist normalisiert” meist “sie ist in 3NF”.

Wir werfen Ihnen nicht alle Definitionen im Voraus an den Kopf. Stattdessen führen wir jede Definition genau dann ein, wenn wir sie brauchen — so haben die Erfinder dieser Regeln über sie nachgedacht, und so ist es viel leichter zu merken.

Die allererste Regel ist die einfachste:

1NF: Jede Zelle enthält einen einzelnen, atomaren Wert, und es gibt keine sich wiederholenden Gruppen. Außerdem muss jede Zeile eindeutig identifizierbar sein (die Tabelle braucht also einen Primärschlüssel).

“Atomar” bedeutet einfach kann für die Art, wie Sie ihn verwenden, nicht sinnvoll weiter aufgeteilt werden. Eine Telefonnummer ist atomar. Eine Liste aus zwei Telefonnummern, in eine Zelle gequetscht, ist es nicht.

Zwei Dinge in unserer Tabelle verletzen die 1NF:

  1. student_phone enthält 0664-111, 0676-222 — zwei Werte in einer Zelle.
  2. Es gibt noch keinen richtigen Primärschlüssel, der garantiert, dass jede Zeile eindeutig ist.

Um das Mehrwert-Problem zu beheben, ziehen wir die Telefonnummern in ihre eigenen Zeilen heraus, in eine eigene Tabelle. Eine Telefonnummer pro Zeile:

CREATE TABLE student_phones (
student_id INTEGER NOT NULL,
phone VARCHAR(30) NOT NULL,
PRIMARY KEY (student_id, phone)
);

Jetzt kann ein Schüler eine, zwei oder zehn Telefonnummern haben — jede ist eine saubere, separate Zeile, die Sie abfragen und filtern können:

-- Jeden Schüler finden, der unter einer 0676-Nummer erreichbar ist. Jetzt einfach:
SELECT student_id
FROM student_phones
WHERE phone LIKE '0676%';

Für die restlichen Daten packen wir sie in eine große Tabelle, die einen echten Primärschlüssel hat. Da eine einzelne student_id nicht eindeutig ist (Anna erscheint zweimal) und eine einzelne course_id ebenfalls nicht, ist das, was eine Zeile eindeutig identifiziert, die Kombination aus beiden:

CREATE TABLE enrollments (
student_id INTEGER NOT NULL,
student_name VARCHAR(100) NOT NULL,
class_name VARCHAR(10) NOT NULL,
class_room VARCHAR(10) NOT NULL,
course_id INTEGER NOT NULL,
course_title VARCHAR(100) NOT NULL,
teacher_name VARCHAR(100) NOT NULL,
teacher_kuerzel VARCHAR(10) NOT NULL,
grade SMALLINT,
PRIMARY KEY (student_id, course_id)
);

Diese Tabelle ist nun in 1NF: Jede Zelle ist atomar, und (student_id, course_id) identifiziert jede Zeile eindeutig. Gut — aber sie ist immer noch voller Wiederholungen. “Mr. Maurhart”, “MAO” und “R204” erscheinen weiterhin mehrfach. Die 1NF hat die Daten ordentlich gemacht, nicht redundanzfrei. Das ist die Aufgabe des nächsten Schritts.

Zweite Normalform (2NF) — Vom ganzen Schlüssel abhängen

Abschnitt betitelt „Zweite Normalform (2NF) — Vom ganzen Schlüssel abhängen“

Um über die 2NF zu sprechen, brauchen wir eine neue Idee: die funktionale Abhängigkeit.

Nun die Regel:

2NF: Die Tabelle ist in 1NF, und jede Nicht-Schlüssel-Spalte hängt vom ganzen Primärschlüssel ab — nicht nur von einem Teil davon.

Diese Regel greift nur, wenn der Primärschlüssel aus mehr als einer Spalte besteht, wie unser (student_id, course_id). Prüfen wir jede Nicht-Schlüssel-Spalte und fragen “wovon hängt sie wirklich ab?”:

  • student_name, class_name, class_room → hängen nur von student_id ab. (Der Kurs hat nichts damit zu tun, in welcher Klasse ein Schüler ist.)
  • course_title, teacher_name, teacher_kuerzel → hängen nur von course_id ab.
  • grade → hängt von beiden ab, student_id und course_id. (Eine Note ist das Ergebnis eines Schülers in einem bestimmten Kurs.)

Die Spalten, die nur von einem Teil des Schlüssels abhängen, nennt man partielle Abhängigkeiten, und genau die verbietet die 2NF. Die Lösung besteht darin, jedem “Teil” seine eigene Tabelle zu geben:

-- Tatsachen, die nur vom Schüler abhängen:
CREATE TABLE students (
student_id SERIAL PRIMARY KEY,
student_name VARCHAR(100) NOT NULL,
class_name VARCHAR(10) NOT NULL,
class_room VARCHAR(10) NOT NULL
);
-- Tatsachen, die nur vom Kurs abhängen:
CREATE TABLE courses (
course_id SERIAL PRIMARY KEY,
course_title VARCHAR(100) NOT NULL,
teacher_name VARCHAR(100) NOT NULL,
teacher_kuerzel VARCHAR(10) NOT NULL
);
-- Tatsachen, die von BEIDEN abhängen (die Einschreibung selbst):
CREATE TABLE enrollments (
student_id INTEGER NOT NULL REFERENCES students(student_id),
course_id INTEGER NOT NULL REFERENCES courses(course_id),
grade SMALLINT,
PRIMARY KEY (student_id, course_id)
);

Sehen Sie, was wir gerade gewonnen haben. Ein Kurs existiert nun als eine Zeile in courses, völlig unabhängig davon, ob ihn ein Schüler belegt. Das beseitigt die Einfüge-Anomalie (wir können einen Kurs hinzufügen, bevor wir ihn einer Klasse zuweisen) und die Lösch-Anomalie (das Entfernen des letzten Schülers löscht den Kurs nicht mehr). Und “Mr. Maurhart” ist genau einmal geschrieben — pro Kurs, nicht pro Einschreibung.

Aber wir sind noch nicht fertig. Sehen Sie sich jetzt beide Zwischentabellen an:

student_idstudent_nameclass_nameclass_room
1Anna Gruber3AHITMR204
2Ben Hofer3AHITMR204
3Cara Wimmer4AHITMR110
course_idcourse_titleteacher_nameteacher_kuerzel
101DatabasesMr. MaurhartMAO
102NetworkingMs. KenderKEN

Wenn 3AHITM dreißig Schüler hat, erscheint “R204” dreißigmal in students. Wenn Mr. Maurhart fünf Kurse unterrichtet, erscheint “MAO” fünfmal in courses. Die Wiederholung ist zurück, nur in anderen Tabellen. Warum? Weil class_room nicht wirklich vom Schüler abhängt — es hängt von der Klasse ab. Und teacher_kuerzel hängt nicht vom Kurs ab — es hängt von der Lehrkraft ab. Genau das löst die 3NF.

Dritte Normalform (3NF) — Keine Umwege über andere Spalten

Abschnitt betitelt „Dritte Normalform (3NF) — Keine Umwege über andere Spalten“

Eine weitere Definition, und es ist die, über die die meisten Schüler stolpern — machen wir sie also zuerst konkret.

Die Regel:

3NF: Die Tabelle ist in 2NF, und keine Nicht-Schlüssel-Spalte hängt von einer anderen Nicht-Schlüssel-Spalte ab. Jede Nicht-Schlüssel-Spalte muss vom Schlüssel abhängen, vom ganzen Schlüssel und von nichts als dem Schlüssel.

Beide obigen Tabellen verletzen die 3NF. Die Lösung ist in beiden Fällen dieselbe — geben Sie der “mittleren” Entität ihre eigene Tabelle:

-- Die Klasse bekommt ihre eigene Tabelle; der Raum gehört zur Klasse, nicht zum Schüler:
CREATE TABLE classes (
class_name VARCHAR(10) PRIMARY KEY,
class_room VARCHAR(10) NOT NULL
);
CREATE TABLE students (
student_id SERIAL PRIMARY KEY,
student_name VARCHAR(100) NOT NULL,
class_name VARCHAR(10) NOT NULL REFERENCES classes(class_name)
);
-- Die Lehrkraft bekommt ihre eigene Tabelle; das Kürzel gehört zur Lehrkraft, nicht zum Kurs:
CREATE TABLE teachers (
teacher_id SERIAL PRIMARY KEY,
teacher_name VARCHAR(100) NOT NULL,
teacher_kuerzel VARCHAR(10) NOT NULL UNIQUE
);
CREATE TABLE courses (
course_id SERIAL PRIMARY KEY,
course_title VARCHAR(100) NOT NULL,
teacher_id INTEGER NOT NULL REFERENCES teachers(teacher_id)
);

Jetzt wird der Raum von 3AHITM an genau einem Ort gespeichert: in einer Zeile in classes. Wenn die Klasse in einen anderen Raum umzieht, ändern Sie einen einzigen Wert, und jeder Schüler in 3AHITM ist sofort korrekt. Und Mr. Maurharts Kürzel lebt in einer Zeile in teachers — ändern Sie es einmal, und jeder Kurs, den er unterrichtet, spiegelt es wider. Beide Änderungs-Anomalien sind verschwunden.

Hier ist der vollständige Entwurf nach Erreichen der 3NF. Fünf kleine Tabellen, jede über genau eine klare Sache:

  1. classes — welche Klassen existieren und wo sie sitzen. (class_name, class_room)

  2. students — wer die Schüler sind und zu welcher Klasse sie gehören. (student_id, student_name, class_name)

  3. teachers — wer die Lehrkräfte sind und ihr offizielles Kürzel. (teacher_id, teacher_name, teacher_kuerzel)

  4. courses — welche Fächer existieren und wer sie unterrichtet. (course_id, course_title, teacher_id)

  5. enrollments — welcher Schüler welchen Kurs belegt und die resultierende Note. (student_id, course_id, grade)

Um die ursprüngliche breite Ansicht zurückzubekommen, verbinden Sie die Teile bei Bedarf einfach mit JOIN:

SELECT s.student_name,
s.class_name,
cl.class_room,
c.course_title,
t.teacher_name,
t.teacher_kuerzel,
e.grade
FROM enrollments e
JOIN students s ON s.student_id = e.student_id
JOIN classes cl ON cl.class_name = s.class_name
JOIN courses c ON c.course_id = e.course_id
JOIN teachers t ON t.teacher_id = c.teacher_id;

Die Daten werden ohne Redundanz gespeichert, und die bequeme “Alles-in-einer-Zeile”-Ansicht ist nur eine Abfrage entfernt. Das ist der ganze Handel, den die Normalisierung anbietet: sauber speichern, bei Bedarf zusammensetzen.

Sie werden von BCNF (einer strengeren Version der 3NF), 4NF und 5NF hören. Sie befassen sich mit selteneren Situationen mit überlappenden Schlüsselkandidaten und komplexen Viele-zu-viele-Beziehungen. Es ist gut, von ihnen gehört zu haben, aber im alltäglichen Datenbankentwurf ändern sie fast nie etwas an dem, was Sie nach Erreichen der 3NF tun würden. Für diesen Kurs — und für die meiste professionelle Arbeit — ist die 3NF Ihr Ziel.

Ja. Und das ist der Teil, den viele Lehrbücher zu erwähnen vergessen.

Jedes Mal, wenn Sie eine Tabelle aufteilen, um eine höhere Normalform zu erreichen, erzeugen Sie eine weitere Tabelle, die zur Abfragezeit wieder zusammengeführt werden muss. Ein perfekt normalisierter Entwurf kann eine einzelne logische “Sache” über sechs oder sieben Tabellen verteilen. Sie zurückzulesen bedeutet dann einen Sechs-Tabellen-JOIN — und Joins kosten Zeit. Auf einem stark ausgelasteten System mit Millionen von Zeilen sind diese Kosten real.

Normalisierung optimiert für das Schreiben (keine duplizierten Tatsachen, keine Anomalien). Aber viele Systeme verbringen den Großteil ihres Lebens mit Lesen. Es gibt Situationen, in denen das bewusste Beibehalten einer gewissen Redundanz das System schneller und einfacher macht:

  • Berichte und Analysen. Ein Dashboard, das den Umsatz des letzten Monats aufsummiert, möchte nicht für jedes Diagramm zehn Tabellen verbinden. Data Warehouses verwenden oft absichtlich denormalisierte “Sternschemata”.
  • Teure, wiederholte Joins. Wenn 95 % Ihrer Abfragen immer dieselben fünf Tabellen verbinden, lohnt es sich manchmal, eine vorab zusammengeführte Kopie zu speichern.
  • Werte, die billig zu kopieren sind und sich nie ändern. Den Ländernamen neben seinem Code zu speichern, mag technisch redundant sein, kann aber bei nahezu keinem Risiko einen Join sparen.

Das bewusste Zurücktreten von der vollständigen Normalisierung nennt man Denormalisierung. Das Schlüsselwort ist bewusst: Sie denormalisieren, nachdem Sie den normalisierten Entwurf verstanden haben und einen gemessenen Grund zum Abweichen haben — niemals, weil Sie zu faul waren, die Tabelle von vornherein aufzuteilen.

NormalformDie Frage, die sie beantwortetDie Lösung
1NFEnthält jede Zelle einen einzelnen atomaren Wert, und ist jede Zeile eindeutig identifizierbar?Mehrwertige Zellen in separate Zeilen aufteilen; einen Primärschlüssel definieren.
2NFHängt jede Nicht-Schlüssel-Spalte vom ganzen Primärschlüssel ab?Spalten, die nur von einem Teil eines zusammengesetzten Schlüssels abhängen, in ihre eigene Tabelle verschieben.
3NFHängt irgendeine Nicht-Schlüssel-Spalte von einer anderen Nicht-Schlüssel-Spalte ab?Die “Umweg”-Spalten (transitive Abhängigkeiten) in ihre eigene Tabelle verschieben.

Die großen Ideen, die Sie mitnehmen sollten:

  • Anomalien (Einfügen, Ändern, Löschen) sind die Symptome; redundante, verwobene Daten sind die Krankheit.
  • Normalisierung heilt sie, indem jede Tatsache genau einmal gespeichert wird, eine Tabelle pro “Sache”.
  • 3NF ist das praktische Ziel für nahezu alle Entwürfe.
  • Denormalisierung ist eine bewusste, abgewogene Ausnahme für leselastige Systeme — keine Ausrede, das Nachdenken zu überspringen.

Entwerfen Sie zuerst sauber. Optimieren Sie später, mit Absicht und mit Zahlen in der Hand.

Lernergebnisse: Was Sie nach diesem Kapitel können sollten

Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“

Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:

  • Nennen: die Einfüge-, Änderungs- und Löschanomalien sowie die Normalformen 1NF–3NF nennen.
  • Erklären: erklären, welches Problem jede Normalform löst (Atomarität, volle funktionale Abhängigkeit, Transitivität).
  • Anwenden: eine gegebene Tabelle schrittweise in die 1NF, 2NF und 3NF überführen.
  • Analysieren: funktionale Abhängigkeiten und Redundanzen in einem Schema analysieren.
  • Beurteilen: abwägen, wann eine Denormalisierung gerechtfertigt ist.