Von der Theorie zur Praxis¶
Nachdem wir die theoretischen Grundlagen des relationalen Modells, die verschiedenen Beziehungstypen und den Normalisierungsprozess kennengelernt haben, wenden wir uns nun der praktischen Umsetzung zu. In diesem Abschnitt zeigen wir, wie die theoretischen Konzepte in SQL-Anweisungen umgesetzt werden und welche Best Practices es für das Datenbankdesign gibt.
Die Umsetzung eines gut strukturierten Datenbankschemas ist entscheidend für den langfristigen Erfolg einer Anwendung. Ein durchdachtes Schema erleichtert die Wartung, verbessert die Performance und ermöglicht flexible Erweiterungen.
Implementierung von Beziehungen in SQL¶
1:1 Beziehungen¶
Eine 1:1-Beziehung wird typischerweise durch einen Fremdschlüssel in einer der beiden Tabellen mit einem UNIQUE-Constraint implementiert.
Beispiel: Person und Mitgliedskarte¶
-- Tabelle für Personen
CREATE TABLE Person (
PersonID UUID PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Vorname VARCHAR(100) NOT NULL,
Geburtsdatum DATE,
Email VARCHAR(100)
);
-- Tabelle für Mitgliedskarten mit 1:1-Beziehung zu Person
CREATE TABLE Mitgliedskarte (
KartenID UUID PRIMARY KEY,
Ausstellungsdatum DATE NOT NULL,
Ablaufdatum DATE NOT NULL,
PersonID UUID UNIQUE REFERENCES Person(PersonID)
);
Der UNIQUE
-Constraint für den Fremdschlüssel PersonID
stellt sicher, dass jede Person höchstens eine Mitgliedskarte haben kann.
Designüberlegungen für 1:1 Beziehungen:¶
Fremdschlüsselplatzierung: Überlegen Sie, in welcher Tabelle der Fremdschlüssel platziert werden sollte:
- Wenn eine Seite optional ist (z.B. nicht jede Person hat eine Mitgliedskarte), platzieren Sie den Fremdschlüssel in dieser Tabelle.
- Wenn eine Entität häufiger abgefragt wird, halten Sie diese Tabelle möglichst schlank.
Zusammenführung: In manchen Fällen kann es sinnvoll sein, die beiden Tabellen zu einer einzigen Tabelle zusammenzuführen, besonders wenn alle Attribute für jeden Datensatz relevant sind und es keine optionalen Beziehungen gibt.
1:n Beziehungen¶
Eine 1:n-Beziehung wird durch einen Fremdschlüssel in der “Viele”-Tabelle implementiert, der auf den Primärschlüssel der “Eins”-Tabelle verweist.
Beispiel: Abteilung und Mitarbeiter¶
-- Tabelle für Abteilungen
CREATE TABLE Abteilung (
AbteilungID UUID PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Standort VARCHAR(100),
Leiter VARCHAR(100)
);
-- Tabelle für Mitarbeiter mit 1:n-Beziehung zu Abteilung
CREATE TABLE Mitarbeiter (
MitarbeiterID UUID PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Vorname VARCHAR(100) NOT NULL,
Position VARCHAR(100),
Einstellungsdatum DATE,
AbteilungID UUID REFERENCES Abteilung(AbteilungID)
);
Hier kann eine Abteilung mehrere Mitarbeiter haben, aber jeder Mitarbeiter gehört zu genau einer Abteilung (oder zu keiner, wenn AbteilungID
NULL sein darf).
Designüberlegungen für 1:n Beziehungen:¶
Optionalität: Überlegen Sie, ob die Beziehung obligatorisch oder optional sein soll:
-- Obligatorisch: Jeder Mitarbeiter muss einer Abteilung zugeordnet sein AbteilungID UUID NOT NULL REFERENCES Abteilung(AbteilungID) -- Optional: Ein Mitarbeiter kann keiner Abteilung zugeordnet sein AbteilungID UUID REFERENCES Abteilung(AbteilungID)
Löschweitergabe: Definieren Sie, was passieren soll, wenn der referenzierte Datensatz gelöscht wird:
-- Löschen verhindern, wenn noch Mitarbeiter in der Abteilung sind AbteilungID UUID REFERENCES Abteilung(AbteilungID) ON DELETE RESTRICT -- Mitarbeiter automatisch löschen, wenn die Abteilung gelöscht wird AbteilungID UUID REFERENCES Abteilung(AbteilungID) ON DELETE CASCADE -- Mitarbeiter keiner Abteilung zuordnen, wenn die Abteilung gelöscht wird AbteilungID UUID REFERENCES Abteilung(AbteilungID) ON DELETE SET NULL
n:m Beziehungen¶
Eine n:m-Beziehung wird durch eine Zwischentabelle (Junction Table) implementiert, die Fremdschlüssel zu beiden beteiligten Tabellen enthält.
Beispiel: Studenten und Kurse¶
-- Tabelle für Studenten
CREATE TABLE Student (
StudentID UUID PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Vorname VARCHAR(100) NOT NULL,
Matrikelnummer VARCHAR(10) NOT NULL UNIQUE
);
-- Tabelle für Kurse
CREATE TABLE Kurs (
KursID UUID PRIMARY KEY,
Bezeichnung VARCHAR(100) NOT NULL,
Credits INTEGER,
Dozent VARCHAR(100)
);
-- Zwischentabelle für n:m-Beziehung zwischen Studenten und Kursen
CREATE TABLE Einschreibung (
StudentID UUID REFERENCES Student(StudentID),
KursID UUID REFERENCES Kurs(KursID),
Einschreibedatum DATE NOT NULL,
Note DECIMAL(3,1),
PRIMARY KEY (StudentID, KursID)
);
Die Zwischentabelle Einschreibung
hat einen zusammengesetzten Primärschlüssel aus beiden Fremdschlüsseln und kann zusätzliche Attribute enthalten, die spezifisch für die Beziehung sind (hier: Einschreibedatum und Note).
Designüberlegungen für n:m Beziehungen:¶
Benennung der Zwischentabelle: Wählen Sie einen Namen, der die Beziehung klar beschreibt (z.B. “Einschreibung”, “Bestellung_Produkt”, “Mitarbeiter_Projekt”).
Beziehungsspezifische Attribute: Identifizieren Sie Attribute, die zur Beziehung selbst gehören, nicht zu den einzelnen Entitäten:
- Zeitliche Informationen (wann wurde die Beziehung hergestellt?)
- Quantitative Informationen (wie viele, wie oft?)
- Qualitative Informationen (wie gut, welche Art?)
Zusammengesetzter Primärschlüssel vs. Surrogatschlüssel: Sie haben zwei Optionen:
-- Option 1: Zusammengesetzter Primärschlüssel PRIMARY KEY (StudentID, KursID) -- Option 2: Surrogatschlüssel (eindeutige ID) EinschreibungID UUID PRIMARY KEY, StudentID UUID REFERENCES Student(StudentID), KursID UUID REFERENCES Kurs(KursID), UNIQUE (StudentID, KursID) -- Stellt sicher, dass die Kombination einzigartig bleibt
Option 2 ist besonders nützlich, wenn:
- Die Primärschlüssel der beteiligten Tabellen sehr gross oder komplex sind
- Die Zwischentabelle oft als Referenz in anderen Tabellen verwendet wird
- Die Datenmodellierungswerkzeuge oder ORMs besser mit einfachen Primärschlüsseln umgehen können
Selbstreferenzierende Beziehungen¶
Eine selbstreferenzierende Beziehung liegt vor, wenn eine Tabelle eine Beziehung zu sich selbst hat.
Beispiel: Mitarbeiter und Vorgesetzte¶
CREATE TABLE Mitarbeiter (
MitarbeiterID UUID PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Vorname VARCHAR(100) NOT NULL,
Position VARCHAR(100),
VorgesetzterID UUID REFERENCES Mitarbeiter(MitarbeiterID)
);
In diesem Beispiel verweist der Fremdschlüssel VorgesetzterID
auf den Primärschlüssel derselben Tabelle und modelliert eine 1:n-Beziehung (ein Vorgesetzter kann mehrere Mitarbeiter haben, aber jeder Mitarbeiter hat höchstens einen Vorgesetzten).
Designüberlegungen für selbstreferenzierende Beziehungen:¶
Zyklen vermeiden: Achten Sie darauf, keine zyklischen Abhängigkeiten zu ermöglichen (z.B. A ist Vorgesetzter von B, B ist Vorgesetzter von C, C ist Vorgesetzter von A).
Tiefenbegrenzung: Überlegen Sie, ob es eine maximale Tiefe der Hierarchie geben soll.
Wurzelknoten: Definieren Sie, wie Wurzelknoten erkennbar sind (z.B.
VorgesetzterID IS NULL
).
Indexierung für Beziehungen¶
Eine gute Indexierungsstrategie ist entscheidend für die Performance von Beziehungsabfragen. Dabei sollten insbesondere Fremdschlüssel-Spalten indexiert werden.
-- Index für den Fremdschlüssel in einer 1:n-Beziehung
CREATE INDEX idx_mitarbeiter_abteilung ON Mitarbeiter(AbteilungID);
-- Indizes für die Fremdschlüssel in einer n:m-Beziehung
CREATE INDEX idx_einschreibung_student ON Einschreibung(StudentID);
CREATE INDEX idx_einschreibung_kurs ON Einschreibung(KursID);
PostgreSQL erstellt automatisch einen Index für Primärschlüssel und UNIQUE-Constraints, aber nicht für Fremdschlüssel. Daher ist es wichtig, explizite Indizes für Fremdschlüsselspalten zu erstellen, besonders wenn diese häufig in JOIN-Operationen oder WHERE-Klauseln verwendet werden.
Naming Conventions und Best Practices¶
Benennungskonventionen¶
Konsistente Benennungskonventionen machen das Datenbankschema leichter verständlich und wartbar:
Tabellennamen:
- Verwenden Sie Singular (z.B. “Person” statt “Personen”)
- Verwenden Sie PascalCase oder snake_case konsequent
Spaltennamen:
- Verwenden Sie eindeutige, beschreibende Namen
- Für Primärschlüssel:
ID
als Suffix oder Präfix (z.B. “PersonID” oder “id_person”) - Für Fremdschlüssel: Name der referenzierten Tabelle plus “ID” (z.B. “AbteilungID”)
Indizes und Constraints:
- Indizes:
idx_tabelle_spalte
(z.B. “idx_person_name”) - Primärschlüssel:
pk_tabelle
(z.B. “pk_person”) - Fremdschlüssel:
fk_tabelle_reftabelle
(z.B. “fk_mitarbeiter_abteilung”) - Unique-Constraints:
uq_tabelle_spalte
(z.B. “uq_person_email”)
- Indizes:
Schema-Design-Best-Practices¶
Normalisierung bis zur 3NF als Ausgangspunkt:
- Beginnen Sie mit einer vollständigen Normalisierung bis zur 3NF
- Denormalisieren Sie nur, wenn es einen klaren Performance-Vorteil gibt
Sorgfältige Auswahl von Primärschlüsseln:
- Bevorzugen Sie natürliche Schlüssel, wenn sie stabil, einzigartig und kompakt sind
- Verwenden Sie andernfalls Surrogatschlüssel wie UUIDs oder Sequences
Datenintegrität durch Constraints sicherstellen:
- NOT NULL für erforderliche Felder
- UNIQUE für eindeutige Werte
- CHECK für Wertebereichsprüfungen
- FOREIGN KEY mit passenden ON DELETE/UPDATE-Aktionen
Konsistente Datentypen:
- Verwenden Sie den engsten passenden Datentyp (z.B. CHAR(4) für Schweizer PLZ)
- Verwenden Sie denselben Datentyp für Werte, die verglichen werden sollen
Kommentare für Dokumentation:
- Dokumentieren Sie Tabellen und Spalten direkt im Schema
COMMENT ON TABLE Person IS 'Enthält alle Personen des Systems'; COMMENT ON COLUMN Person.Email IS 'Primäre Kontakt-E-Mail-Adresse';
Praktisches Beispiel: Vereinsdatenbank¶
Im Folgenden sehen wir ein vollständiges Beispiel für das Schema einer Vereinsdatenbank, das die besprochenen Konzepte und Best Practices umsetzt:
-- Statustypen für Vereinsmitglieder
CREATE TABLE Status (
StatusID UUID PRIMARY KEY,
Bezeichnung VARCHAR(50) NOT NULL UNIQUE,
Beitrag NUMERIC(10,2) NOT NULL,
CONSTRAINT chk_status_beitrag CHECK (Beitrag >= 0)
);
-- Vereinsmitglieder
CREATE TABLE Person (
PersonID UUID PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Vorname VARCHAR(100) NOT NULL,
Strasse VARCHAR(100) NOT NULL,
PLZ CHAR(4) NOT NULL,
Ort VARCHAR(100) NOT NULL,
Telefon VARCHAR(20),
Email VARCHAR(100),
Geburtsdatum DATE,
Eintrittsdatum DATE NOT NULL,
Austrittsdatum DATE,
StatusID UUID NOT NULL REFERENCES Status(StatusID),
MentorID UUID REFERENCES Person(PersonID),
CONSTRAINT chk_person_austritt CHECK (Austrittsdatum IS NULL OR Austrittsdatum > Eintrittsdatum)
);
-- Index für die 1:n-Beziehung Person-Status
CREATE INDEX idx_person_status ON Person(StatusID);
-- Index für die selbstreferenzierende Beziehung (Mentor)
CREATE INDEX idx_person_mentor ON Person(MentorID);
-- Vereinsfunktionen
CREATE TABLE Funktion (
FunktionID UUID PRIMARY KEY,
Bezeichnung VARCHAR(100) NOT NULL UNIQUE,
Beschreibung TEXT
);
-- n:m-Beziehung zwischen Personen und Funktionen
CREATE TABLE Funktionsbesetzung (
PersonID UUID REFERENCES Person(PersonID),
FunktionID UUID REFERENCES Funktion(FunktionID),
Von DATE NOT NULL,
Bis DATE,
PRIMARY KEY (PersonID, FunktionID, Von),
CONSTRAINT chk_funktionsbesetzung_zeitraum CHECK (Bis IS NULL OR Bis > Von)
);
-- Indizes für die n:m-Beziehung
CREATE INDEX idx_funktionsbesetzung_person ON Funktionsbesetzung(PersonID);
CREATE INDEX idx_funktionsbesetzung_funktion ON Funktionsbesetzung(FunktionID);
-- Vereinsveranstaltungen
CREATE TABLE Anlass (
AnlassID UUID PRIMARY KEY,
Bezeichnung VARCHAR(100) NOT NULL,
Beschreibung TEXT,
Datum DATE NOT NULL,
Ort VARCHAR(100) NOT NULL,
MaxTeilnehmer INTEGER,
CONSTRAINT chk_anlass_maxteilnehmer CHECK (MaxTeilnehmer IS NULL OR MaxTeilnehmer > 0)
);
-- n:m-Beziehung zwischen Personen und Anlässen
CREATE TABLE Teilnahme (
PersonID UUID REFERENCES Person(PersonID),
AnlassID UUID REFERENCES Anlass(AnlassID),
Anmeldedatum DATE NOT NULL DEFAULT CURRENT_DATE,
Status VARCHAR(20) NOT NULL DEFAULT 'angemeldet',
Bemerkung TEXT,
PRIMARY KEY (PersonID, AnlassID),
CONSTRAINT chk_teilnahme_status CHECK (Status IN ('angemeldet', 'abgemeldet', 'teilgenommen', 'nicht erschienen'))
);
-- Indizes für die n:m-Beziehung
CREATE INDEX idx_teilnahme_person ON Teilnahme(PersonID);
CREATE INDEX idx_teilnahme_anlass ON Teilnahme(AnlassID);
-- Tabellen-Kommentare
COMMENT ON TABLE Status IS 'Enthält die möglichen Mitgliedsstatus mit zugehörigen Beiträgen';
COMMENT ON TABLE Person IS 'Speichert alle Mitglieder und ehemaligen Mitglieder des Vereins';
COMMENT ON TABLE Funktion IS 'Definiert die möglichen Funktionen/Rollen im Verein';
COMMENT ON TABLE Funktionsbesetzung IS 'Verknüpft Personen mit ihren Funktionen, inkl. Zeitraum';
COMMENT ON TABLE Anlass IS 'Enthält alle Veranstaltungen des Vereins';
COMMENT ON TABLE Teilnahme IS 'Speichert die Teilnahme von Personen an Anlässen';
Dieses Schema demonstriert:
- 1:n-Beziehungen (Person zu Status)
- n:m-Beziehungen (Person zu Funktion, Person zu Anlass)
- Selbstreferenzierende Beziehung (Person zu Mentor)
- Konsistente Benennungskonventionen
- Integrität durch Constraints (NOT NULL, UNIQUE, CHECK)
- Korrekte Indexierung
- Dokumentation durch Kommentare
Fazit¶
Die praktische Umsetzung von Beziehungen und Normalisierung in SQL erfordert sowohl ein solides theoretisches Verständnis als auch praktisches Wissen über die spezifischen Features des verwendeten DBMS.
Ein gut gestaltetes Datenbankschema, das die Prinzipien des relationalen Modells respektiert und Best Practices folgt:
- Vermeidet Redundanzen und Anomalien
- Stellt die Datenintegrität sicher
- Bietet eine solide Grundlage für effiziente Abfragen
- Ermöglicht einfache Wartung und Erweiterung
Die Konzepte, die wir in diesem Kapitel behandelt haben – Relationen, Beziehungstypen, Normalisierung und praktische Implementierung – bilden das Fundament für professionelles Datenbankdesign. Mit diesem Wissen können Sie robuste, effiziente und langlebige Datenbankschemata entwickeln.
In den folgenden Kapiteln werden wir auf diesem Fundament aufbauen und fortgeschrittene Abfragetechniken wie JOINs, Unterabfragen und Aggregationsfunktionen behandeln, die Ihnen ermöglichen, die in Ihrem Schema gespeicherten Daten vollständig zu nutzen.