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.

Lernziel

Nach dieser Lektion können Sie

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_id → kurse.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

Primärreferenzen

Geprüfte SQL-Referenzen

Die fachlichen Aussagen dieser Lektion werden gegen Normkontext und zuständige Primärdokumentationen der jeweiligen Datenbanksysteme geprüft. Erklärungen, Beispiele, Übungen, Quizfragen und Lösungen sind eigenständige SemaTrain-Inhalte.

Lektionsprofil: Relationales Modell und DDL

Kursbasis: Gemeinsamer SQL-Kern mit explizit gekennzeichneten Varianten für SQLite 3.53.4, PostgreSQL 18 und MySQL 8.4 LTS.

Quellenprüfung: 2026-09-18

Einordnung: Die Modellierungsgrundlagen werden systemübergreifend vermittelt. Datentypen, Schlüsselgenerierung und einzelne DDL-Details können je Produkt abweichen.

ISO/IEC 9075-1:2023 Information technology — Database languages SQL — Part 1: Framework

Quellentyp: Norm-/Standardreferenz

Projekt / Referenzstelle: ISO/IEC 9075

Ersteller / Maintainer: ISO/IEC JTC 1/SC 32

Versionsbezug: ISO/IEC 9075-1:2023

Aussagebereich: SQL-Framework, Begriffe und Standardkontext.

Offizielle Primärreferenz

SQLite CREATE TABLE

Quellentyp: Projekt-Primärdokumentation

Projekt / Referenzstelle: SQLite

Ersteller / Maintainer: SQLite Developers

Versionsbezug: SQLite 3.53.4

Aussagebereich: CREATE TABLE, PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK und DEFAULT.

Offizielle Primärreferenz

PostgreSQL 18 CREATE TABLE

Quellentyp: Projekt-Primärdokumentation

Projekt / Referenzstelle: PostgreSQL

Ersteller / Maintainer: PostgreSQL Global Development Group

Versionsbezug: PostgreSQL 18

Aussagebereich: CREATE TABLE, Spalten, Constraints und Schlüssel.

Offizielle Primärreferenz

MySQL 8.4 CREATE TABLE Statement

Quellentyp: Hersteller-Primärdokumentation

Projekt / Referenzstelle: MySQL

Ersteller / Maintainer: Oracle Corporation

Versionsbezug: MySQL 8.4 LTS

Aussagebereich: CREATE TABLE, Schlüssel, Constraints und MySQL-spezifische Optionen.

Offizielle Primärreferenz

SQL Quellen und Methodik

Quiz: Relationales Modell & Schlüssel

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

Einfachauswahl – genau eine Antwort ist richtig.

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

Einfachauswahl – genau eine Antwort ist richtig.

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

Einfachauswahl – genau eine Antwort ist richtig.

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

Einfachauswahl – genau eine Antwort ist richtig.

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
-- ==========================================================

-- SQLite: Fremdschlüsselprüfung pro Verbindung mit PRAGMA foreign_keys = ON aktivieren.
-- 1) FK-Test: Termin existiert nicht -> muss bei aktiver FK-Prüfung 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.