SQL Grundlagen Schulung – Kursbezug
Worum geht’s?
- Eine Transaktion bündelt mehrere SQL-Statements zu einer Einheit.
- COMMIT macht Änderungen dauerhaft, ROLLBACK verwirft sie.
- ACID erklärt, warum Transaktionen zuverlässig funktionieren – auch bei Fehlern oder Parallelzugriffen.
Schema-Kontext (Lernpfad)
-- kurse(kurs_id, kurs_name)
-- trainer(trainer_id, name)
-- termine(termin_id, kurs_id, trainer_id, start_datum, ort, format, dauer_tage, preis_eur)
-- (Ergänzung in diesem Kapitel)
-- termin_kapazitaet(termin_id, freie_plaetze)
-- buchungen(buchung_id, termin_id, name, email, created_at)Lehr-/Lernziele
- (LZ1) BEGIN/COMMIT/ROLLBACK sicher anwenden („alles oder nichts“).
- (LZ2) SAVEPOINT für Teil-Rollback nutzen.
- (LZ3) ACID erklären und typische Fehlerbilder erkennen (Teil-Updates, Race Conditions).
- (LZ4) Isolation-Grundidee verstehen (Lost Update, Dirty/Non-repeatable/Phantom).
ACID in 60 Sekunden
- Atomicity: Alles oder nichts.
- Consistency: Regeln/Constraints bleiben erfüllt.
- Isolation: Parallele Transaktionen stören sich nicht „unerwartet“.
- Durability: Nach COMMIT bleibt es gespeichert – auch nach Crash.
Praxisbild: „Buchung“ = Platz reservieren + Buchung schreiben. Ohne Transaktion kann eins durchgehen und das andere fehlschlagen → Inkonsistenz.
Transaktion: BEGIN / COMMIT / ROLLBACK
Wichtig (Praxis): Nach dem „Platz reservieren“-UPDATE muss geprüft werden, ob wirklich 1 Zeile geändert wurde (sonst würde man trotz 0 Plätzen trotzdem ein INSERT machen → Overbooking).
-- Beispiel: Termin buchen (alles oder nichts)
-- Voraussetzung: termin_kapazitaet(termin_id, freie_plaetze) und buchungen(...)
BEGIN;
-- 1) Platz reservieren (nur wenn noch frei)
UPDATE termin_kapazitaet
SET freie_plaetze = freie_plaetze - 1
WHERE termin_id = 2
AND freie_plaetze > 0;
-- 2) Praxis-Safety: Prüfe "affected rows" (DB/Tool abhängig)
-- Wenn 0 Zeilen geändert wurden -> KEIN Platz frei -> ROLLBACK.
-- Wenn 1 Zeile geändert wurde -> weiter mit INSERT und COMMIT.
-- 3) Buchung schreiben (nur wenn Reservierung erfolgreich war)
INSERT INTO buchungen (termin_id, name, email)
VALUES (2, 'Max Mustermann', 'max@example.com');
COMMIT; -- übernimmt beide Schritte (wenn alles ok)
-- ROLLBACK; -- Alternative: alles verwerfenAffected Rows prüfen (SQLite / MySQL / PostgreSQL)
-- SQLite: changes() liefert die Anzahl der geänderten Zeilen des letzten Statements
BEGIN;
UPDATE termin_kapazitaet
SET freie_plaetze = freie_plaetze - 1
WHERE termin_id = 2 AND freie_plaetze > 0;
-- Wenn changes() = 0 -> kein Platz -> ROLLBACK
SELECT changes() AS affected_rows;
-- (Wenn affected_rows = 1) dann:
INSERT INTO buchungen(termin_id, name, email)
VALUES (2, 'Max Mustermann', 'max@example.com');
COMMIT;
-- ----------------------------------------------------------
-- MySQL: ROW_COUNT() nach UPDATE prüfen
START TRANSACTION;
UPDATE termin_kapazitaet
SET freie_plaetze = freie_plaetze - 1
WHERE termin_id = 2 AND freie_plaetze > 0;
SELECT ROW_COUNT() AS affected_rows;
-- App/Client entscheidet:
-- affected_rows = 1 -> INSERT + COMMIT
-- affected_rows = 0 -> ROLLBACK
-- ----------------------------------------------------------
-- PostgreSQL: "UPDATE ... RETURNING" und daraus INSERT machen (robust + elegant)
BEGIN;
WITH reserved AS (
UPDATE termin_kapazitaet
SET freie_plaetze = freie_plaetze - 1
WHERE termin_id = 2 AND freie_plaetze > 0
RETURNING termin_id
)
INSERT INTO buchungen (termin_id, name, email)
SELECT termin_id, 'Max Mustermann', 'max@example.com'
FROM reserved;
-- Wenn kein Platz frei: reserved ist leer -> INSERT fügt 0 Zeilen ein -> App kann danach COMMIT/ROLLBACK entscheiden
COMMIT;Typischer Fehler: COMMIT vergessen
-- In vielen Clients gibt es einen Autocommit-Schalter.
-- Faustregel: Wenn du BEGIN siehst, brauchst du am Ende COMMIT oder ROLLBACK.
BEGIN;
UPDATE termine
SET ort = 'Hamburg'
WHERE termin_id = 42;
-- COMMIT; -- sonst ggf. nicht dauerhaft
-- ROLLBACK; -- falls es doch falsch warSAVEPOINT (Teil-Rollback)
BEGIN;
-- Platz reservieren
UPDATE termin_kapazitaet
SET freie_plaetze = freie_plaetze - 1
WHERE termin_id = 2
AND freie_plaetze > 0;
-- (Praxis) prüfen, ob reserviert wurde (affected rows). Wenn 0 -> ROLLBACK.
SAVEPOINT after_reserve;
-- Folgeschritt scheitert (Simulation: NOT NULL verletzt)
-- INSERT INTO buchungen(termin_id, name, email) VALUES (2, NULL, 'x@example.com'); -- würde fehlschlagen
-- Teil-Rollback: bis nach der Reservierung zurück
ROLLBACK TO after_reserve;
-- Kompensation: Platz wieder freigeben (innerhalb derselben TX)
UPDATE termin_kapazitaet
SET freie_plaetze = freie_plaetze + 1
WHERE termin_id = 2;
COMMIT;Isolation: Parallelität (Grundidee)
- Lost Update: Zwei Transaktionen überschreiben sich gegenseitig.
- Dirty Read: Eine Transaktion liest uncommitted Daten.
- Non-Repeatable Read: Ein Wert ändert sich zwischen zwei SELECTs.
- Phantom Read: Neue Zeilen tauchen „plötzlich“ auf (z. B. bei COUNT).
Praktisches Muster: SELECT … FOR UPDATE (DB-abhängig)
-- Beispiel (PostgreSQL/MySQL): Zeile sperren, bevor man sie ändert
BEGIN;
SELECT freie_plaetze
FROM termin_kapazitaet
WHERE termin_id = 2
FOR UPDATE;
UPDATE termin_kapazitaet
SET freie_plaetze = freie_plaetze - 1
WHERE termin_id = 2
AND freie_plaetze > 0;
COMMIT;Praxisaufgabe
- Lege zwei Tabellen an:
termin_kapazitaetundbuchungen(Minimalspalten wie unten). - Buche einen Platz für einen Termin in einer Transaktion: reservieren + Buchung schreiben.
- Simuliere einen Fehler (z. B. Termin existiert nicht) und stelle sicher: ROLLBACK verhindert Teiländerungen.
- (Bonus) Nutze
SAVEPOINT, um nur einen Teil zurückzurollen.
Lösungsvorschlag anzeigen
-- Minimal-Setup (didaktisch klein)
-- Hinweis: Optional kann buchungen.termin_id als FOREIGN KEY auf termine(termin_id) referenzieren,
-- wenn die Tabelle termine existiert (FK ist hier nicht zwingend für das Transaktionsprinzip).
CREATE TABLE termin_kapazitaet (
termin_id INTEGER PRIMARY KEY,
freie_plaetze INTEGER NOT NULL CHECK (freie_plaetze >= 0)
);
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
-- Optional (wenn termine existiert):
-- ,FOREIGN KEY (termin_id) REFERENCES termine(termin_id)
);
-- Beispiel-Daten
INSERT INTO termin_kapazitaet(termin_id, freie_plaetze) VALUES (2, 1);
-- Buchung in einer Transaktion (mit Safety-Check)
BEGIN;
UPDATE termin_kapazitaet
SET freie_plaetze = freie_plaetze - 1
WHERE termin_id = 2
AND freie_plaetze > 0;
-- SQLite: changes() / MySQL: ROW_COUNT() / Postgres: UPDATE ... RETURNING
-- Wenn affected_rows = 0 -> kein Platz -> ROLLBACK.
SELECT changes() AS affected_rows; -- (SQLite-Beispiel)
-- Nur wenn affected_rows = 1:
INSERT INTO buchungen(termin_id, name, email)
VALUES (2, 'Max Mustermann', 'max@example.com');
COMMIT;
-- Bei Fehlern / kein Platz:
-- ROLLBACK;Kurz-Takeaways
- BEGIN + COMMIT = Änderungen dauerhaft als Einheit.
- ROLLBACK = alles verwerfen (oder mit SAVEPOINT teilweise).
- ACID erklärt Zuverlässigkeit; Isolation verhindert Parallelitätsprobleme.
- Constraints/Indexe helfen – aber Transaktionen sind der „Safety-Gurt“ für Mehrschritt-Änderungen.
- Praxis: Bei „Reservierung + Insert“ immer affected rows prüfen, sonst droht Overbooking.