SemaTrain Logo Ein Fachportal von SemaTrain

Einführung: Relationales Modell & SQL

Sie starten bei Null: Was ist das relationale Modell? Wie hängen Tabellen, Schlüssel und Beziehungen zusammen – und warum ist das die Grundlage für JOINs, Filtern und Aggregation?

Hinweis: Beispiele sind bewusst klein und didaktisch – Ziel ist das Denkmodell, nicht „perfekte“ Fachdomäne.

SQL Grundlagen Schulung – Kursbezug

Dieses Kapitel ist Teil des Lernpfads zur SQL Grundlagen Schulung. Termine & Buchung laufen über SemaTrain.de.

Modell Tabellen + Schlüssel + Beziehungen = Grundlage für alles Weitere.

Worum geht’s?

SQL-Denkmodell

Merksatz: „Datenmodell zuerst“. Gute Abfragen sind oft Folge eines sauberen Modells.

Beispiel-Modell (Schulungskontext)

Mini-Domäne: SQL-Schulung mit Terminen und Buchungen. Wir halten es bewusst klein, damit die Beziehungen klar werden.

DDL: Tabellen, PK/FK, Constraints (SQL)
-- Beispiel: Kurs-Termine und Buchungen (konsistent zu Kap. 02–04)
CREATE TABLE kurse (
  kurs_id    INTEGER PRIMARY KEY,
  kurs_name  TEXT NOT NULL
);

CREATE TABLE termine (
  termin_id     INTEGER PRIMARY KEY,
  kurs_id       INTEGER NOT NULL,
  start_datum   DATE NOT NULL,
  ort           TEXT NOT NULL,
  format        TEXT NOT NULL CHECK (format IN ('Online','Praesenz')),
  dauer_tage    INTEGER NOT NULL CHECK (dauer_tage > 0),
  preis_eur     NUMERIC NOT NULL CHECK (preis_eur >= 0),

  -- Fremdschlüssel: Termin gehört zu genau einem Kurs
  FOREIGN KEY (kurs_id) REFERENCES kurse(kurs_id)
);

CREATE TABLE buchungen (
  buchung_id INTEGER PRIMARY KEY,
  termin_id  INTEGER NOT NULL,
  name       TEXT NOT NULL,
  email      TEXT NOT NULL,
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,

  -- Fremdschlüssel: Buchung gehört zu genau einem Termin
  FOREIGN KEY (termin_id) REFERENCES termine(termin_id)
);

Beziehungen: 1:n und n:m

1:n (Kurs → Termine)

  • Ein Kurs hat viele Termine.
  • Ein Termin gehört zu genau einem Kurs.
  • Umsetzung: FK termine.kurs_idkurse.kurs_id

n:m (Teilnehmende ↔ Termine)

  • Ein Teilnehmender kann mehrere Termine buchen.
  • Ein Termin hat viele Teilnehmende.
  • Umsetzung: Zwischentabelle (z. B. buchungen)
Warum braucht man IDs?
  • Namen sind nicht eindeutig („Max Müller“).
  • Texte ändern sich (Rechtschreibfehler, Umzug).
  • IDs sind stabil und schnell verknüpfbar (Index/Join).

Constraints: Datenqualität im Modell

Constraints/Indizes (optional, aber praxisnah) (SQL)
-- Beispiel: E-Mail darf pro Termin nur einmal buchen (optional)
CREATE UNIQUE INDEX ux_buchungen_termin_email
ON buchungen(termin_id, email);

-- Beispiel: Index auf Foreign Key-Spalte für Join-Performance
CREATE INDEX ix_termine_kurs_id
ON termine(kurs_id);

Normalisierung (kurz & praxisnah)

Ziel: Redundanz reduzieren, Updates sauber halten. Für den Einstieg reicht dieses Bild:

Anti-Pattern: „Alles in eine Tabelle“
Anti-Pattern: Redundanz (SQL)
-- SCHLECHT (Beispiel): Kursname wird in jedem Termin wiederholt
CREATE TABLE termine_schlecht (
  termin_id   INTEGER PRIMARY KEY,
  kurs_name   TEXT NOT NULL,   -- Redundanz!
  start_datum DATE NOT NULL,
  ort         TEXT NOT NULL
);

-- Wenn sich kurs_name ändert, müssen viele Zeilen aktualisiert werden.

Besser: kurse + termine mit FK.

Praxisaufgabe

  1. Erstelle die Tabellen kurse, termine, buchungen aus dem Beispiel.
  2. Füge 1 Kurs, 2 Termine und 2 Buchungen ein.
  3. Überlege: Welche Spalten sollten NOT NULL sein? Wo ist CHECK sinnvoll?
  4. (Bonus) Lege einen UNIQUE-Index an, der doppelte Buchungen (Termin+E-Mail) verhindert.
Lösungsvorschlag anzeigen
Daten einfügen (Beispiel) (SQL)
INSERT INTO kurse(kurs_id, kurs_name) VALUES (1, 'SQL Grundlagen Schulung');

INSERT INTO termine(termin_id, kurs_id, start_datum, ort, format, dauer_tage, preis_eur) VALUES
(101, 1, '2026-02-23', 'Hamburg', 'Praesenz', 3, 1428.00),
(102, 1, '2026-03-09', 'remote',  'Online',   3, 1428.00);

INSERT INTO buchungen(buchung_id, termin_id, name, email) VALUES
(1001, 101, 'Max Mustermann', 'max@example.com'),
(1002, 102, 'Erika Musterfrau', 'erika@example.com');

Kurz-Takeaways

Quiz: Relationales Modell & Schlüssel

1. (LZ1) Wozu dient ein Primärschlüssel (PRIMARY KEY) typischerweise?

2. (LZ2) Was beschreibt ein Fremdschlüssel (FOREIGN KEY)?

3. (LZ3) Welche Beziehung wird meist über eine Zwischentabelle modelliert?

4. (LZ4) Wozu dienen Constraints wie NOT NULL oder CHECK?

Praxisaufgabe

Mini-Projekt: „Kursverwaltung“ – Schema, Datenqualität, Mini-Tests

Sie bauen ein kleines, sauberes Datenmodell rund um Kurse, Trainer, Termine und Buchungen. Fokus in Kapitel 01: PK/FK, Constraints, sinnvolle Datentypen und die Denkweise „erst Modell, dann Abfragen“.

Beitrag zu den Lernzielen: LZ1 (Modell), LZ2 (PK/FK), LZ3 (Beziehungen), LZ4 (Constraints).
Stufenprojekt MVP → Qualität → Performance → Mini-Reporting

Stufe A – MVP (Schema)

Stufe B – Datenqualität (Constraints)

Stufe C – Performance-Basics (Indizes)

Stufe D – Mini-Tests (Absichtlich kaputt machen)

Stufe E – Erste Abfragen (ohne JOIN)

Musterlösung (Schema + Seed + Tests + Queries)
Projektlösung: Schema → Daten → Tests → Queries (SQL)
-- ==========================================================
-- Stufe A: MVP-Schema (konsistent für Kap. 02–04)
-- ==========================================================

CREATE TABLE kurse (
  kurs_id    INTEGER PRIMARY KEY,
  kurs_name  TEXT    NOT NULL UNIQUE
);

CREATE TABLE trainer (
  trainer_id INTEGER PRIMARY KEY,
  name       TEXT NOT NULL
);

CREATE TABLE termine (
  termin_id   INTEGER PRIMARY KEY,
  kurs_id     INTEGER NOT NULL,
  trainer_id  INTEGER NULL, -- optional (z. B. noch nicht zugewiesen)
  start_datum DATE    NOT NULL,
  ort         TEXT    NOT NULL,
  format      TEXT    NOT NULL CHECK (format IN ('Online','Praesenz')),
  dauer_tage  INTEGER NOT NULL CHECK (dauer_tage > 0),
  preis_eur   NUMERIC NOT NULL CHECK (preis_eur >= 0),

  FOREIGN KEY (kurs_id) REFERENCES kurse(kurs_id),
  FOREIGN KEY (trainer_id) REFERENCES trainer(trainer_id)
);

CREATE TABLE buchungen (
  buchung_id INTEGER PRIMARY KEY,
  termin_id  INTEGER NOT NULL,
  name       TEXT    NOT NULL,
  email      TEXT    NOT NULL,
  created_at TEXT    DEFAULT CURRENT_TIMESTAMP,

  FOREIGN KEY (termin_id) REFERENCES termine(termin_id)
);

-- ==========================================================
-- Stufe B/C: Constraints + Indizes
-- ==========================================================

-- E-Mail darf pro Termin nur einmal buchen (praxisnah)
CREATE UNIQUE INDEX ux_buchungen_termin_email
ON buchungen(termin_id, email);

-- Performance: Indizes auf FK-Spalten
CREATE INDEX ix_termine_kurs_id    ON termine(kurs_id);
CREATE INDEX ix_termine_trainer_id ON termine(trainer_id);
CREATE INDEX ix_buchungen_termin_id ON buchungen(termin_id);

-- Optional: häufige Filter (Datum)
CREATE INDEX ix_termine_start_datum ON termine(start_datum);

-- ==========================================================
-- Stufe A: Seed-Daten
-- ==========================================================

INSERT INTO kurse (kurs_id, kurs_name) VALUES
  (1, 'SQL Grundlagen Schulung'),
  (2, 'Python Schulung'),
  (3, 'Java Grundlagen Schulung');

INSERT INTO trainer (trainer_id, name) VALUES
  (1, 'Mathias Ellmann'),
  (2, 'Gasttrainer');

-- trainer_id ist absichtlich einmal NULL (für LEFT JOIN in Kap. 03)
INSERT INTO termine (termin_id, kurs_id, trainer_id, start_datum, ort, format, dauer_tage, preis_eur) VALUES
  (101, 1, 1,    '2026-02-23', 'Hamburg', 'Praesenz', 3, 1428.00),
  (102, 1, NULL, '2026-03-09', 'Hamburg', 'Online',   3, 1428.00),
  (201, 2, 2,    '2026-04-06', 'remote',  'Online',   3, 1428.00);

INSERT INTO buchungen (buchung_id, termin_id, name, email) VALUES
  (1001, 101, 'Max Mustermann',   'max@example.com'),
  (1002, 101, 'Erika Musterfrau', 'erika@example.com');

-- ==========================================================
-- Stufe D: Mini-Tests (sollten FEHLSCHLAGEN) – auskommentieren
-- ==========================================================

-- 1) FK-Test: Termin existiert nicht -> muss scheitern
-- INSERT INTO buchungen (buchung_id, termin_id, name, email)
-- VALUES (9999, 999, 'Test', 'test@example.com');

-- 2) CHECK-Test: dauer_tage = 0 -> muss scheitern
-- INSERT INTO termine (termin_id, kurs_id, trainer_id, start_datum, ort, format, dauer_tage, preis_eur)
-- VALUES (999, 1, 1, '2026-05-01', 'Berlin', 'Praesenz', 0, 100.00);

-- 3) UNIQUE-Test: doppelte Buchung (termin_id + email) -> muss scheitern
-- INSERT INTO buchungen (buchung_id, termin_id, name, email)
-- VALUES (1003, 101, 'Max Mustermann', 'max@example.com');

-- ==========================================================
-- Stufe E: Erste Abfragen (ohne JOIN)
-- ==========================================================

-- (E1) Alle Kurse
SELECT kurs_id, kurs_name
FROM kurse
ORDER BY kurs_name ASC;

-- (E2) Termine im März 2026
SELECT termin_id, kurs_id, trainer_id, start_datum, ort, format, dauer_tage, preis_eur
FROM termine
WHERE start_datum BETWEEN '2026-03-01' AND '2026-03-31'
ORDER BY start_datum ASC;

-- (E3) Buchungen für Termin 101
SELECT buchung_id, termin_id, name, email, created_at
FROM buchungen
WHERE termin_id = 101
ORDER BY created_at ASC;

Ausblick: In Kapitel 02 filtern/sortieren wir die Daten mit SELECT/WHERE/ORDER BY. In Kapitel 03 (JOINs) machen wir daraus echte Reports: Termine + Kursname + Trainer.