Übungsaufgaben zu INSERT, UPDATE und DELETE

https://bildung.social/@oerinformatik/

https://oer-informatik.de/sql-dml-uebungsaufgaben

SQL, DML, INSERT, UPDATE, DELETE, Fremdschlüssel, Transaktion, MariaDB, Datenmigration, referentielle Integrität, Übungsaufgaben, Prüfungsvorbereitung

tl/dr; (Bearbeitungszeit insgesamt 204 min, mehrere Aufgaben): Sechs Übungsaufgaben zum Einfügen, Ändern und Löschen von Datensätzen – mit Preisänderungen, Fallunterscheidungen, Archivtabellen, Fremdschlüsseln, Transaktionen, Anonymisierung, Datenübernahme in ein neues Tabellenschema, Terminumbuchungen und typischen Fehlern. Alle Lösungen sind ausgeblendet und mit Punkteverteilung versehen [AI-Assisted]. (Zuletzt geändert am 28.10.2026)

Dieser Artikel ist Teil einer Artikelreihe über die SQL-DML: Datensätze einfügen (INSERT) / Datensätze ändern (UPDATE) / Datensätze löschen (DELETE) / Übungsaufgaben zu INSERT, UPDATE und DELETE

Hinweise zur Bearbeitung: Als FIA, FIDP, ISE oder IKD musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1, Aufgabe 2.2, Aufgabe 2.3 und Aufgabe 2.4. Von IKD wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1 und Aufgabe 3.2) keine Probleme bereiten. Die Teilaufgaben einer Aufgabe bauen aufeinander auf: Jede Anweisung wirkt auf den Datenbestand, den die vorherigen Teilaufgaben hinterlassen haben. Schreib Deine Lösung zuerst vollständig auf, bevor Du die Beispiellösung aufklappst. Die SQL-Anweisungen sind in MariaDB-Notation formuliert; andere korrekte Lösungen sind ebenfalls richtig. „Heute“ ist in allen Aufgaben der 18.10.2026. Am Ende findest Du eine SQL-Kurzreferenz – Teilaufgaben, die darauf verweisen, setzen Funktionen voraus, die Du nicht auswendig kennen musst.

Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?

Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.

Erstelle neue Übungsaufgaben zum Thema „INSERT, UPDATE und DELETE“.

Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.

Fachliche Grundlage:
- https://oer-informatik.de/sql-dml-insert
- https://oer-informatik.de/sql-dml-update
- https://oer-informatik.de/sql-dml-delete
- https://oer-informatik.de/sql-dml-selektion-projektion
- https://oer-informatik.de/sql-dml-singlerowfunctions
- Stilbeispiel: https://oer-informatik.de/sql-dml-uebungsaufgaben

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- SQL
- DML
- INSERT
- UPDATE
- DELETE
- Fremdschlüssel
- Transaktion
- MariaDB
- Datenmigration
- referentielle Integrität

Kompetenzstufe: Kernkompetenz (alternativ: Basiskompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan

Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
   abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
   zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
   Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
   Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
   Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
   danach frage.

Aufgabe 2.1: Sortimentspflege im Fahrrad-Onlineshop (Kernkompetenz)

(ca. 35 min Bearbeitungszeit)

Du bist Auszubildende*r bei der CSB-Rad GmbH in Bielefeld, die Fahrräder und Komponenten über einen Onlineshop verkauft. Bastian Ludwig (Werkstattleitung und Einkauf) bittet Dich, das Sortiment in der Shop-Datenbank zu pflegen. Die Tabelle artikel ist so definiert (Auszug):

CREATE TABLE artikel (
  artikel_id      INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  artikelnr       VARCHAR(12)  NOT NULL UNIQUE,
  bezeichnung     VARCHAR(100) NOT NULL,
  kategorie_id    INT UNSIGNED NOT NULL,
  netto_preis     DECIMAL(10,2) NOT NULL,
  mwst_satz       DECIMAL(4,2) NOT NULL DEFAULT 19.00,
  lagerbestand    INT          NOT NULL DEFAULT 0,
  mindestbestand  INT          NOT NULL DEFAULT 0,
  gewicht_kg      DECIMAL(6,2) NULL,
  konfigurierbar  BOOLEAN      NOT NULL DEFAULT FALSE,
  aktiv           BOOLEAN      NOT NULL DEFAULT TRUE,
  CONSTRAINT chk_preis CHECK (netto_preis >= 0),
  CONSTRAINT fk_art_kategorie FOREIGN KEY (kategorie_id) REFERENCES kategorie (kategorie_id)
);

Weitere Tabellen (Primärschlüssel unterstrichen, Fremdschlüssel mit #):

  • kategorie (kategorie_id, bezeichnung, #ober_kategorie_id)

  • bestellposition (#bestellung_id, pos_nr, #artikel_id, menge, einzelpreis_netto, rabatt_prozent, …) – der Fremdschlüssel artikel_id ist ohne ON DELETE-Regel definiert.

Tabelle kategorie
kategorie_id bezeichnung ober_kategorie_id
1 Fahrräder NULL
2 E-Bikes 1
3 Lastenräder 1
4 Stadträder 1
5 Komponenten NULL
6 Rahmen 5
7 Schlösser 5
8 Beleuchtung 5
9 Bremsen 5
Tabelle artikel (Auszug, alle Artikel mit 19 % MwSt.)
artikel_id artikelnr bezeichnung kategorie_id netto_preis lagerbestand mindestbestand gewicht_kg aktiv
1 RAD-CITY-01 Stadtrad „Promenade“ 7-Gang 4 671.43 12 5 16.80 TRUE
2 RAD-EB-02 E-Bike „Kanal“ Mittelmotor 500 Wh 2 2520.17 4 3 24.50 TRUE
3 RAD-LR-03 Lastenrad „Long John“ individuell 3 3780.67 0 0 38.00 TRUE
4 RAH-IND-01 Rahmen nach Maß (Stahl, pulverbeschichtet) 6 840.34 0 0 2.40 TRUE
5 SCH-BUE-01 Bügelschloss, gleichschließend 7 62.18 0 0 1.30 TRUE
6 SCH-RAH-01 Rahmenschloss, gleichschließend 7 41.18 0 0 0.45 TRUE
7 LIC-SET-01 Lichtset Nabendynamo StVZO 8 75.63 48 20 0.35 TRUE
8 BRE-HYD-01 Hydraulische Scheibenbremse (Paar) 9 133.61 9 10 0.75 TRUE
9 RAD-EB-01 E-Bike „Altstadt“ Nabenmotor (Auslauf) 2 1595.80 1 0 23.10 FALSE

Artikel 9 wurde im Oktober 2026 einmal bestellt; die Bestellung wurde storniert, die Bestellposition ist aber noch gespeichert.

a) Bevor es losgeht, möchte Bastian wissen, welche Befehle Du gleich brauchst und welche eher in die Hände der Datenbankadministration gehören. Ordne in der Tabelle jeder Anweisung die Sprachfamilie (DDL, DML, DQL oder DCL) und die passende CRUD-Operation zu und gib an, ob sie die Struktur oder den Inhalt der Datenbank verändert. (4 Punkte)

Zuordnung der SQL-Anweisungen
Anweisung Sprachfamilie CRUD-Operation verändert …
INSERT INTO kategorie …
SELECT … FROM artikel …
UPDATE artikel SET …
DELETE FROM artikel WHERE …
CREATE TABLE artikel_archiv …
ALTER TABLE artikel MODIFY …
TRUNCATE TABLE import_haendler
GRANT SELECT ON csbrad.artikel TO 'shop_web'@'%'
Anweisung Sprachfamilie CRUD-Operation verändert …
INSERT INTO kategorie … DML Create Inhalt
SELECT … FROM artikel … DQL (im SQL-Standard: DML) Read nichts
UPDATE artikel SET … DML Update Inhalt
DELETE FROM artikel WHERE … DML Delete Inhalt
CREATE TABLE artikel_archiv … DDL Create (Struktur) Struktur
ALTER TABLE artikel MODIFY … DDL Update (Struktur) Struktur
TRUNCATE TABLE import_haendler DDL Delete (alle Zeilen) Inhalt (alle Zeilen auf einmal; technisch wird die Tabelle neu angelegt, der AUTO_INCREMENT-Zähler beginnt neu)
GRANT SELECT ON … TO … DCL keine (Rechtevergabe) Zugriffsrechte

Bewertung: je Zeile 0,5 P, wenn Sprachfamilie, CRUD-Operation und die Angabe „verändert …“ stimmen. Bei SELECT ist „DML“ mit Hinweis auf den SQL-Standard ebenfalls richtig.

b) Die Werkstatt verkauft künftig zwei Verschleißteile auch im Shop. Dafür wird zuerst eine neue Kategorie „Antrieb“ als Unterkategorie von „Komponenten“ benötigt. Füge die Kategorie und anschließend die beiden Artikel mit möglichst wenigen Anweisungen ein. Die Schlüssel sollen von der Datenbank vergeben werden; wo die Tabellendefinition einen passenden Standardwert vorsieht, soll dieser gelten. Das Gewicht der Kette ist noch nicht bekannt. (5 Punkte)

Neue Artikel
Artikelnr. Bezeichnung Kategorie Nettopreis Lagerbestand Mindestbestand Gewicht
BRE-BEL-02 Bremsbelag-Satz Scheibenbremse Bremsen 18,40 € 30 10 0,10 kg
KET-11-E Kette 11-fach E-Bike Antrieb (neu) 32,90 € 15 5 unbekannt

INSERT INTO kategorie (bezeichnung, ober_kategorie_id)
VALUES ('Antrieb', 5);

INSERT INTO artikel (artikelnr, bezeichnung, kategorie_id, netto_preis,
                     lagerbestand, mindestbestand, gewicht_kg)
VALUES ('BRE-BEL-02', 'Bremsbelag-Satz Scheibenbremse', 9, 18.40, 30, 10, 0.10),
       ('KET-11-E', 'Kette 11-fach E-Bike',
        (SELECT kategorie_id FROM kategorie WHERE bezeichnung = 'Antrieb'),
        32.90, 15, 5, NULL);

Ergebnis: Die Kategorie erhält die kategorie_id 10, die Artikel die artikel_id 10 und 11. mwst_satz (19.00), konfigurierbar (FALSE) und aktiv (TRUE) kommen aus den DEFAULT-Werten.

artikel_id artikelnr kategorie_id netto_preis mwst_satz gewicht_kg konfigurierbar aktiv
10 BRE-BEL-02 9 18.40 19.00 0.10 0 1
11 KET-11-E 10 32.90 19.00 NULL 0 1

Bewertung: INSERT der Kategorie mit Spaltenliste ohne Schlüssel 1 P; Spaltenliste beim Artikel ohne artikel_id und ohne Spalten mit passendem Standardwert 1 P; mehrere Zeilen in einer Anweisung 1 P; Fremdschlüssel der neuen Kategorie per Unterabfrage oder LAST_INSERT_ID() 1 P; Literale korrekt (Text in Hochkommata, Dezimalpunkt, NULL ohne Hochkommata) 1 P.

Alternativen: gewicht_kg weglassen (wird ebenfalls NULL). Statt der Unterabfrage ist LAST_INSERT_ID() in der Zeile der Kette ebenso richtig, wenn das Artikel-INSERT unmittelbar auf das Einfügen der Kategorie folgt: Die gerade laufende Anweisung verändert den Wert von LAST_INSERT_ID() nicht, er bleibt während des ganzen INSERT bei 10. Die feste Zahl 10 ist nur richtig, wenn man vorher geprüft hat, welcher Wert vergeben wurde (0,5 P Abzug ohne Begründung).

c) Der Schlosshersteller erhöht zum 1. November die Einkaufspreise; CSB-Rad gibt das weiter. Erhöhe die Nettopreise aller Artikel der Kategorie „Schlösser“ um 6 % und runde auf ganze Cent. Die Kategorie soll in der Anweisung über ihre Bezeichnung, nicht über ihre Nummer ausgewählt werden. Gib die neuen Preise an. (4 Punkte)

UPDATE artikel
SET netto_preis = ROUND(netto_preis * 1.06, 2)
WHERE kategorie_id = (SELECT kategorie_id
                      FROM kategorie
                      WHERE bezeichnung = 'Schlösser');

Neue Preise: Bügelschloss 62,18 € · 1,06 = 65,9108 € → 65,91 €, Rahmenschloss 41,18 € · 1,06 = 43,6508 € → 43,65 €.

Bewertung: UPDATE … SET mit Faktor 1.06 (selbstbezüglich) 1 P, ROUND(…, 2) 1 P, Auswahl über Unterabfrage auf kategorie 1 P, beide Preise 1 P.

Alternative (MariaDB): UPDATE artikel AS a INNER JOIN kategorie AS k ON k.kategorie_id = a.kategorie_id SET a.netto_preis = ROUND(a.netto_preis * 1.06, 2) WHERE k.bezeichnung = 'Schlösser';. Ohne ROUND rundet die Spalte DECIMAL(10,2) beim Speichern selbst (Ergebnis gleich, 0,5 P Abzug, weil die Absicht nicht erkennbar ist). Falsch: netto_preis + 6, netto_preis * 0.06 oder netto_preis * 6 %.

d) Die Tabelle bestellposition speichert einen eigenen Preis einzelpreis_netto (Auszug siehe unten). Begründe, warum die Preiserhöhung aus c) die bereits gespeicherten Bestellpositionen nicht verändern darf. (3 Punkte)

Tabelle bestellposition (Auszug)
bestellung_id pos_nr artikel_id menge einzelpreis_netto rabatt_prozent
1 2 5 2 62.18 0.0
1 3 7 1 75.63 0.0
5 1 6 12 41.18 5.0
7 1 9 1 1595.80 0.0
  • Vertraglich gilt der Preis zum Zeitpunkt der Bestellung; Kund*innen haben zu diesem Preis gekauft.

  • Rechnungen sind Buchungsbelege und müssen unverändert und nachvollziehbar bleiben (Aufbewahrungspflicht, GoBD); eine spätere Preisänderung darf alte Rechnungsbeträge nicht verfälschen.

  • Auswertungen (z. B. Umsatz eines Quartals) müssen später dieselben Zahlen liefern. Deshalb ist der Preis in der Bestellposition ein bewusst gespeicherter historischer Wert und keine Redundanz.

Im Datenbestand bleiben z. B. Bestellung 1, Position 2 (62,18 €) und Bestellung 5, Position 1 (41,18 €) unverändert.

Bewertung: je tragfähigem Argument 1 P.

e) Für die Komponenten soll ein neuer Mindestbestand gelten: Beleuchtung 25 Stück, Bremsen 12 Stück. Die übrigen Artikel bleiben unverändert. Erstelle eine SQL-Anweisung dafür und gib an, welche Artikel sie ändert. (4 Punkte)

UPDATE artikel
SET mindestbestand = CASE kategorie_id
                       WHEN 8 THEN 25
                       WHEN 9 THEN 12
                     END
WHERE kategorie_id IN (8, 9);

Geändert werden Artikel 7 (Lichtset, 25), Artikel 8 (Scheibenbremse, 12) und der neue Artikel 10 (Bremsbelag-Satz, bisher 10, jetzt 12).

Bewertung: CASE mit zwei Zweigen 2 P, WHERE auf die beiden Kategorien 1 P, betroffene Artikel einschließlich des neuen Artikels 10 1 P.

Ebenso volle Punktzahl: ohne WHERE, dafür mit ELSE mindestbestand (dann werden alle Zeilen durchlaufen, aber nur drei geändert). Ohne WHERE und ohne ELSE setzt das CASE bei allen anderen Artikeln NULL – das scheitert im Strict Mode an NOT NULL, ohne Strict Mode wird mit Warnung 0 gespeichert. Die Suche mit CASE WHEN kategorie_id = 8 THEN … ist ebenso richtig, ebenso zwei getrennte UPDATE-Anweisungen mit 1 P Abzug (nicht „eine Anweisung“).

f) Für eine Herbstaktion soll das Lichtset 10 % günstiger werden. Eine Kollegin hat dazu folgende Anweisung vorbereitet, die aber nicht wie gewünscht funktioniert:

UPDATE artikel
SET netto_preis = netto_preis - 10 %;
WHERE bezeichnung = 'Lichtset'

Nenne die Fehler, erläutere jeweils die Folge und korrigiere die Anweisung. (6 Punkte)

Fehler Folge
- 10 %: Ein Prozentzeichen für „Prozent“ gibt es in SQL nicht, % ist der Modulo-Operator. Syntaxfehler; gemeint ist netto_preis * 0.9 (oder netto_preis - netto_preis * 0.10).
Semikolon vor WHERE Die Anweisung endet vorzeitig: Wäre der Ausdruck gültig, würde der Preis aller Artikel geändert; das folgende WHERE … ist dann eine eigene, ungültige Anweisung.
bezeichnung = 'Lichtset' Die Bezeichnung lautet vollständig „Lichtset Nabendynamo StVZO“ – der Vergleich trifft keine Zeile. Sicherer ist der eindeutige Schlüssel (Artikelnummer oder artikel_id), notfalls LIKE 'Lichtset%'.
keine Rundung Ergebnis 68,067 €; ROUND(…, 2) macht die Rundung auf Cent ausdrücklich.

Korrektur:

UPDATE artikel
SET netto_preis = ROUND(netto_preis * 0.9, 2)
WHERE artikelnr = 'LIC-SET-01';

Ergebnis: 75,63 € · 0,9 = 68,067 € → 68,07 € (1 Zeile geändert).

Bewertung: drei Fehler mit Folge je 1 P (Rundung als vierter Punkt ersatzweise), Korrektur 3 P (Faktor 1 P, WHERE am richtigen Platz mit eindeutiger Bedingung 1 P, Rundung 1 P).

g) Bastian möchte ausgelaufene Artikel aus der Datenbank entfernen und schlägt vor:

DELETE FROM artikel
WHERE aktiv = FALSE;

ga) Erläutere, was beim Ausführen passiert. (2 Punkte)

gb) Erstelle eine Anweisung, die nur solche inaktiven Artikel löscht, die in keiner Bestellposition vorkommen, und nimm Stellung, wie mit Artikel 9 verfahren werden sollte. (4 Punkte)

ga) Die Bedingung trifft Artikel 9. Auf ihn verweist noch eine Bestellposition; der Fremdschlüssel bestellposition.artikel_id hat keine ON DELETE-Regel, es gilt also RESTRICT. MariaDB bricht mit Fehler 1451 ab („Cannot delete or update a parent row: a foreign key constraint fails …“), es wird nichts gelöscht. Die referentielle Integrität verhindert, dass Bestellpositionen auf einen nicht mehr vorhandenen Artikel zeigen. (2 P)

gb)

DELETE FROM artikel
WHERE aktiv = FALSE
  AND NOT EXISTS (SELECT *
                  FROM bestellposition AS bp
                  WHERE bp.artikel_id = artikel.artikel_id);

Ergebnis: 0 Zeilen gelöscht – Artikel 9 bleibt erhalten. Alternative: AND artikel_id NOT IN (SELECT artikel_id FROM bestellposition) (hier unproblematisch, weil artikel_id in bestellposition nie NULL ist).

Stellungnahme: Artikel 9 sollte nicht gelöscht werden, auch nicht über ON DELETE CASCADE (dann verschwänden Bestellpositionen und damit Teile von Bestell- und Rechnungsdaten, die aufbewahrt werden müssen). Er bleibt mit aktiv = FALSE gespeichert und wird im Shop nicht angezeigt; wer die Tabelle entlasten will, kopiert ihn in eine Archivtabelle, ohne den Verweis zu brechen.

Bewertung: DELETE mit NOT EXISTS/NOT IN und korrekter Korrelation 2 P, Ergebnis 1 P, Stellungnahme 1 P.

h) Für eine neue Versandkostenberechnung soll das Gewicht ab sofort bei jedem Artikel Pflicht sein. Jonas will dafür gleich ALTER TABLE artikel MODIFY gewicht_kg DECIMAL(6,2) NOT NULL; ausführen. Inzwischen hat der Großhändler für Verschleißteile seine Artikeldaten geliefert, sie liegen in der Tabelle herstellerdaten (Gewicht in Gramm):

Tabelle herstellerdaten
artikelnr gewicht_g
KET-11-E 248
BRE-BEL-02 112
LIC-SET-01 350

ha) Begründe, warum Jonas’ Anweisung im jetzigen Datenbestand nicht der erste Schritt sein kann, und beschreibe, was je nach SQL-Modus passiert. (2 Punkte)

hb) Übernimm fehlende Gewichte aus herstellerdaten (in kg, auf zwei Nachkommastellen gerundet), ohne bereits gepflegte Werte zu überschreiben, und mache die Spalte danach zum Pflichtfeld. (4 Punkte)

ha) Artikel 11 (Kette) hat noch kein Gewicht, in der Spalte steht also ein NULL. Im Strict Mode (Voreinstellung) lehnt MariaDB die Änderung auf NOT NULL deshalb ab („Invalid use of NULL value“) (1 P). Ohne Strict Mode würde das NULL mit einer Warnung in 0.00 umgewandelt – die Kette wöge dann scheinbar nichts und die Versandkosten würden falsch berechnet. Erst müssen die Lücken mit echten Werten gefüllt sein, dann kann die Struktur geändert werden (1 P).

hb)

UPDATE artikel AS a
  INNER JOIN herstellerdaten AS h ON h.artikelnr = a.artikelnr
SET a.gewicht_kg = ROUND(h.gewicht_g / 1000, 2)
WHERE a.gewicht_kg IS NULL;

-- Kontrolle: SELECT COUNT(*) FROM artikel WHERE gewicht_kg IS NULL;  -- 0
ALTER TABLE artikel
MODIFY gewicht_kg DECIMAL(6,2) NOT NULL;

Geändert wird nur Artikel 11: 248 g ergeben 0.25 kg. Der Bremsbelag-Satz BRE-BEL-02 hat schon 0.10 kg und bleibt unverändert, obwohl der Großhändler 112 g angibt – die Bedingung IS NULL schützt gepflegte Werte. LIC-SET-01 ist ebenfalls schon gepflegt.

Bewertung: Verbund bzw. Unterabfrage über die Artikelnummer 1 P, Umrechnung in kg mit ROUND(…, 2) 1 P, Bedingung IS NULL (= NULL trifft keine Zeile: 0 P) 1 P, danach ALTER TABLE … MODIFY … NOT NULL mit Datentyp 1 P. CHANGE gewicht_kg gewicht_kg DECIMAL(6,2) NOT NULL ist ebenso richtig. Hätte die Kontrolle noch Artikel ohne Gewicht gezeigt, müssten diese zuerst nachgewogen werden.

Aufgabe 2.2: Anmeldungen und Sessionplan eines Barcamps (Kernkompetenz)

(ca. 35 min Bearbeitungszeit)

Am Ratz und Rübe Berufskolleg (RRB) in Bielefeld findet am 13.03.2027 das Barcamp „Lernen.Digital.Leben 2027“ statt. Du gehörst zum Orga-Team von Dimitrios Papadopoulos und pflegst die Orga-Datenbank. Sie enthält unter anderem (Primärschlüssel unterstrichen, Fremdschlüssel mit #):

  • person (person_id, vorname, nachname, email, organisation, geburtsjahr)

  • anmeldung (anmeldung_id, #veranstaltung_id, #person_id, ticketart, angemeldet_am, essen, bedarf_barrierefreiheit, foto_einwilligung, checkin_zeit, badge_code)

  • raum (raum_id, bezeichnung, name, gebaeude, etage, plaetze, beamer, stream_faehig, barrierefrei)

  • slot (slot_id, #veranstaltung_id, beginn, ende)

  • session (session_id, #veranstaltung_id, titel, beschreibung, format, status, eingereicht_am, max_plaetze, #raum_id, #slot_id, livestream, live_untertitel)

  • session_person (#session_id, #person_id, rolle) – Referent*innen und Moderation

  • session_buchung (#session_id, #anmeldung_id, gebucht_am) – Buchungen für Sessions mit begrenzten Plätzen

  • orga_aufgabe (aufgabe_id, titel, #verantwortlich_id, faellig_am, status)

Auszug aus den Tabellendefinitionen:

CREATE TABLE anmeldung (
  anmeldung_id      INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  veranstaltung_id  INT UNSIGNED NOT NULL,
  person_id         INT UNSIGNED NOT NULL,
  ticketart         ENUM('Schueler','Lehrkraft','Extern','Referent','Orga','Aussteller') NOT NULL,
  angemeldet_am     DATETIME     NOT NULL,
  essen             ENUM('keine Angabe','vegetarisch','vegan','ohne Schwein','glutenfrei')
                    NOT NULL DEFAULT 'keine Angabe',
  bedarf_barrierefreiheit VARCHAR(200) NULL,
  foto_einwilligung BOOLEAN      NOT NULL DEFAULT FALSE,
  checkin_zeit      DATETIME     NULL,
  badge_code        CHAR(8)      NULL UNIQUE,
  CONSTRAINT uq_anmeldung UNIQUE (veranstaltung_id, person_id),
  FOREIGN KEY (veranstaltung_id) REFERENCES veranstaltung (veranstaltung_id),
  FOREIGN KEY (person_id)        REFERENCES person (person_id)
);
-- in session:         CONSTRAINT uq_raum_slot UNIQUE (raum_id, slot_id)
-- in session_person:  FOREIGN KEY (session_id)   REFERENCES session (session_id)     ON DELETE CASCADE
-- in session_buchung: FOREIGN KEY (session_id)   REFERENCES session (session_id)     ON DELETE CASCADE,
--                     FOREIGN KEY (anmeldung_id) REFERENCES anmeldung (anmeldung_id) ON DELETE CASCADE
-- in orga_aufgabe:    FOREIGN KEY (verantwortlich_id) REFERENCES person (person_id)
Tabelle raum (Auszug)
raum_id bezeichnung name plaetze barrierefrei
1 A.001 Aula 250 TRUE
2 A.104 NULL 32 TRUE
3 A.105 NULL 32 TRUE
4 B.012 Mensa 180 TRUE
5 B.201 IT-Labor 1 28 TRUE
6 B.202 IT-Labor 2 28 TRUE
7 C.110 Elektrowerkstatt 24 FALSE
8 C.020 Bibliothek 40 TRUE
Tabelle slot (alle Slots gehören zur Veranstaltung 1)
slot_id beginn ende
1 10:00:00 10:45:00
2 11:00:00 11:45:00
3 12:45:00 13:30:00
4 13:45:00 14:30:00
5 14:45:00 15:30:00
Tabelle session (Auszug)
session_id titel format status max_plaetze raum_id slot_id
1 KI-Assistenten im Unterricht – was geht, was nicht? Vortrag angenommen NULL 1 1
2 Reparieren statt wegwerfen: Laptops retten Workshop angenommen 12 7 2
3 Barrierefreie Videos: Untertitel und Gebärdensprache Workshop angenommen 20 5 3
4 Escape-Room mit Mikrocontrollern Workshop angenommen 16 6 4
5 Datenschutz im Schulalltag – Mythen und Fakten Diskussion eingereicht NULL NULL NULL
6 Barrierefreie Webseiten mit HTML und ARIA Tutorial angenommen 24 6 5
7 Krypto-Trading für Einsteiger Vortrag abgelehnt NULL NULL NULL

Bisher sind 14 Personen gespeichert (person_id 1 bis 14), jede mit genau einer Anmeldung (anmeldung_id = person_id). Fatma Öztürk (person_id 6, E-Mail fatma.oeztuerk@rrb-bielefeld.example) hat die Sessions 2, 4 und 6 gebucht; sie ist weder Referentin noch für eine Orga-Aufgabe verantwortlich.

a) Samira Benali von der Senne-Logistik GmbH (E-Mail samira.benali@example.org) hat sich am 18.10.2026 um 10:30 Uhr als Referentin angemeldet. Sie isst vegan, hat keinen Barrierefreiheitsbedarf angegeben und der Veröffentlichung von Fotos nicht zugestimmt. Ihr Badge erhält den Code BC27R015. Erstelle die SQL-Anweisungen, die Person und Anmeldung speichern. Die Schlüssel vergibt die Datenbank. (5 Punkte)

INSERT INTO person (vorname, nachname, email, organisation)
VALUES ('Samira', 'Benali', 'samira.benali@example.org', 'Senne-Logistik GmbH');

INSERT INTO anmeldung (veranstaltung_id, person_id, ticketart, angemeldet_am,
                       essen, badge_code)
VALUES (1, LAST_INSERT_ID(), 'Referent', '2026-10-18 10:30:00',
        'vegan', 'BC27R015');

Ergebnis: person_id 15, anmeldung_id 15; foto_einwilligung erhält den Standardwert FALSE, bedarf_barrierefreiheit, checkin_zeit und geburtsjahr bleiben NULL.

Bewertung: Reihenfolge Person vor Anmeldung (Fremdschlüssel) 1 P, INSERT Person 1 P, INSERT Anmeldung mit Spaltenliste 1 P, Fremdschlüssel per LAST_INSERT_ID() oder Unterabfrage (SELECT person_id FROM person WHERE email = 'samira.benali@example.org') 1 P, Datum/Zeit im ISO-Format und ENUM-Werte richtig geschrieben 1 P. foto_einwilligung = FALSE ausdrücklich anzugeben ist ebenso richtig.

b) Die Diskussion „Datenschutz im Schulalltag“ (Session 5) wurde angenommen. Sie soll im Raum A.105 im Slot ab 11:00 Uhr stattfinden und auf 32 Plätze begrenzt sein. Erstelle eine SQL-Anweisung, die alle Angaben speichert. Raum und Slot sollen über die Raumbezeichnung bzw. die Uhrzeit ausgewählt werden. (4 Punkte)

UPDATE session
SET status      = 'angenommen',
    raum_id     = (SELECT raum_id FROM raum WHERE bezeichnung = 'A.105'),
    slot_id     = (SELECT slot_id FROM slot WHERE beginn = '11:00:00'),
    max_plaetze = 32
WHERE session_id = 5;

Ergebnis: Session 5 hat jetzt status = 'angenommen', raum_id = 3, slot_id = 2, max_plaetze = 32.

Bewertung: mehrere Zuweisungen mit Komma 1 P, zwei Unterabfragen 2 P, WHERE auf den Primärschlüssel 1 P. Bei mehreren Veranstaltungen müsste die Slot-Unterabfrage zusätzlich AND veranstaltung_id = 1 enthalten. Werden die Zuweisungen mit AND verbunden, ist die Anweisung falsch (0,5 P für den Ansatz).

c) Gerhard Vogt leitet den Reparatur-Workshop und ist auf einen Aufzug angewiesen. Dimitrios vermutet, dass Sessions in nicht barrierefreien Räumen geplant sind.

ca) Ermittle mit einer SQL-Abfrage alle angenommenen Sessions, die in einem nicht barrierefreien Raum liegen. (3 Punkte)

cb) Verlege die betroffene Session in das IT-Labor 1 (B.201); der Slot bleibt gleich. Begründe, warum die Änderung nicht gegen die Bedingung uq_raum_slot verstößt. (3 Punkte)

ca)

SELECT s.session_id, s.titel, r.bezeichnung, r.barrierefrei
FROM session AS s
  INNER JOIN raum AS r ON r.raum_id = s.raum_id
WHERE s.status = 'angenommen'
  AND r.barrierefrei = FALSE;
session_id titel bezeichnung barrierefrei
2 Reparieren statt wegwerfen: Laptops retten C.110 0

Bewertung: Verbund 1 P, beide Bedingungen 1 P, Ergebnis 1 P.

cb)

UPDATE session
SET raum_id = (SELECT raum_id FROM raum WHERE bezeichnung = 'B.201')
WHERE session_id = 2;

uq_raum_slot verlangt, dass jede Kombination aus Raum und Slot nur einmal vorkommt. B.201 (Raum 5) ist nur im Slot 3 belegt (Session 3); Session 2 liegt im Slot 2, die Kombination (5, 2) ist frei. Wäre sie belegt, würde MariaDB das UPDATE mit „Duplicate entry“ ablehnen.

Bewertung: UPDATE 2 P (WHERE session_id = 2 oder gleichwertige Bedingung), Begründung 1 P. Der Weg „erst mit SELECT prüfen, dann mit derselben Bedingung ändern“ ist bewusst so gewählt: So sieht man vorher, welche Zeilen betroffen sind.

d) Sessions ohne Platzbegrenzung sollen auf die Zahl der Plätze ihres Raums begrenzt werden. Ein Teammitglied hat dazu mit

SELECT session_id, max_plaetze FROM session WHERE max_plaetze = NULL;

nachgesehen und keine Session gefunden. Erläutere, warum diese Abfrage nie eine Zeile liefert. Erstelle anschließend eine SQL-Anweisung, die bei allen angenommenen Sessions ohne Platzbegrenzung max_plaetze auf die Platzzahl des zugeordneten Raums setzt, und gib an, welche Session sie ändert. (6 Punkte)

NULL steht für „unbekannt“. Ein Vergleich mit NULL (=, <>, < …) ergibt deshalb ebenfalls „unbekannt“ und nie „wahr“ – keine Zeile erfüllt die Bedingung. Leere Felder findet man nur mit IS NULL. (2 P)

UPDATE session
SET max_plaetze = (SELECT r.plaetze
                   FROM raum AS r
                   WHERE r.raum_id = session.raum_id)
WHERE status = 'angenommen'
  AND max_plaetze IS NULL;

Geändert wird nur Session 1 (Aula): max_plaetze = 250. Session 5 hat seit b) bereits 32 Plätze, Session 7 ist abgelehnt.

Bewertung: korrelierte Unterabfrage im SET 2 P, WHERE mit IS NULL und Status 1 P, Ergebnis 1 P.

Alternative (MariaDB): UPDATE session AS s INNER JOIN raum AS r ON r.raum_id = s.raum_id SET s.max_plaetze = r.plaetze WHERE s.status = 'angenommen' AND s.max_plaetze IS NULL;

e) Abgelehnte und zurückgezogene Sessions sollen aus dem Sessionplan verschwinden, für die Dokumentation des Call for Participation aber erhalten bleiben. Lege dazu eine Tabelle session_archiv mit derselben Struktur wie session an, kopiere die betroffenen Sessions hinein und lösche sie anschließend aus session. Erläutere, welche Datensätze in anderen Tabellen beim Löschen mitgelöscht würden. (6 Punkte)

CREATE TABLE session_archiv LIKE session;

INSERT INTO session_archiv
SELECT *
FROM session
WHERE status IN ('abgelehnt', 'zurueckgezogen');

DELETE FROM session
WHERE session_id IN (SELECT session_id FROM session_archiv);

Ergebnis: Session 7 („Krypto-Trading für Einsteiger“) steht im Archiv und nicht mehr in session (6 Sessions verbleiben).

Mitgelöscht würden wegen ON DELETE CASCADE die zugehörigen Zeilen in session_person (Rollen), session_buchung (Buchungen) und – in der vollständigen Datenbank – feedback. Bei Session 7 gibt es keine solchen Zeilen; bei einer angenommenen, später zurückgezogenen Session gingen die Buchungen aber verloren und die Teilnehmenden müssten informiert werden. Die Referent*innen-Zuordnung landet nicht im Archiv – soll sie erhalten bleiben, braucht man auch eine session_person_archiv.

Bewertung: CREATE TABLE … LIKE 1 P (ausgeschriebenes CREATE TABLE mit allen Spalten ebenso), INSERT … SELECT * mit IN 2 P, DELETE mit Bezug auf das Archiv oder derselben Bedingung 2 P, Erläuterung CASCADE 1 P.

SELECT * ist hier zulässig, weil beide Tabellen dieselbe Spaltenreihenfolge haben. Hinweis: CREATE TABLE … LIKE übernimmt Indizes, aber keine Fremdschlüssel; für ein Archiv ist das meist erwünscht. Ebenfalls richtig ist CREATE TABLE session_archiv AS SELECT * FROM session WHERE status IN ('abgelehnt', 'zurueckgezogen'); – es ersetzt CREATE TABLE und INSERT (dann ohne Primärschlüssel und Indizes).

f) Fatma Öztürk hat ihre Teilnahme abgesagt.

fa) Lösche ihre Anmeldung. Die Person soll über ihre E-Mail-Adresse ausgewählt werden. (2 Punkte)

fb) Erläutere, was mit ihren Session-Buchungen geschieht und warum MariaDB trotzdem „1 row affected“ meldet. (2 Punkte)

fc) Aus Gründen der Datenminimierung sollen alle Personen gelöscht werden, die weder eine Anmeldung haben noch als Referent*in oder Moderation eingetragen noch für eine Orga-Aufgabe verantwortlich sind. Erstelle die SQL-Anweisung und gib an, wer gelöscht wird. (4 Punkte)

fa)

DELETE FROM anmeldung
WHERE person_id = (SELECT person_id
                   FROM person
                   WHERE email = 'fatma.oeztuerk@rrb-bielefeld.example');

Bewertung: DELETE FROM … WHERE 1 P, Unterabfrage über die E-Mail-Adresse 1 P (bei mehreren Veranstaltungen zusätzlich AND veranstaltung_id = 1).

fb) Der Fremdschlüssel session_buchung.anmeldung_id ist mit ON DELETE CASCADE definiert: Ihre drei Buchungen (Sessions 2, 4 und 6) werden automatisch mitgelöscht; die Plätze werden frei. MariaDB zählt als betroffene Zeilen nur die der Tabelle, aus der ausdrücklich gelöscht wurde, nicht die kaskadierten Zeilen. (2 P)

fc)

DELETE FROM person
WHERE NOT EXISTS (SELECT * FROM anmeldung AS a
                  WHERE a.person_id = person.person_id)
  AND NOT EXISTS (SELECT * FROM session_person AS sp
                  WHERE sp.person_id = person.person_id)
  AND NOT EXISTS (SELECT * FROM orga_aufgabe AS oa
                  WHERE oa.verantwortlich_id = person.person_id);

Gelöscht wird nur Fatma Öztürk (person_id 6); 14 Personen bleiben.

Bewertung: drei NOT EXISTS- bzw. NOT IN-Bedingungen mit AND 3 P, Ergebnis 1 P. Mit NOT IN (SELECT person_id FROM anmeldung) usw. ist es ebenso richtig, solange die Unterabfragen kein NULL liefern (die Spalten sind hier alle NOT NULL). Ohne die Bedingung zu orga_aufgabe würde das DELETE bei einer Person mit Orga-Aufgabe am Fremdschlüssel scheitern.

g) Erledigte Orga-Aufgaben mit einer Fälligkeit vom 1. September bis einschließlich 31. Oktober 2026 sollen gelöscht werden. Nutze die SQL-Kurzreferenz in der Anlage. (3 Punkte)

Tabelle orga_aufgabe
aufgabe_id titel verantwortlich_id faellig_am status
1 Call for Participation veröffentlichen 2 2026-10-01 erledigt
2 Gäste-WLAN mit Captive Portal einrichten 3 2027-02-26 offen
3 Check-in-Terminals und Badge-Drucker testen 4 2027-03-06 offen
4 Schriftdolmetschung für Session 3 buchen 11 2026-12-15 in Arbeit
5 Stromplan für Ausstellerstände (230 V) erstellen 4 2027-02-19 offen
6 Sessionplan finalisieren 2 2027-02-12 offen
DELETE FROM orga_aufgabe
WHERE status = 'erledigt'
  AND faellig_am BETWEEN '2026-09-01' AND '2026-10-31';

Gelöscht wird die Aufgabe „Call for Participation veröffentlichen“ (fällig am 01.10.2026).

Bewertung: Bedingung auf den Status 1 P, BETWEEN mit ISO-Datumswerten 1 P, Verknüpfung mit AND 1 P. BETWEEN schließt beide Grenzen ein; bei faellig_am (Typ DATE) ist das unproblematisch. >= '2026-09-01' AND <= '2026-10-31' ist ebenso richtig; '01.09.2026' ist kein gültiges Datumsliteral.

Aufgabe 2.3: Löschkonzept einer Freizeit-App umsetzen (Kernkompetenz)

(ca. 36 min Bearbeitungszeit)

Du bist Auszubildende*r bei der Let’s Meet GmbH in Bielefeld. Die App „Let’s Meet“ bringt Menschen ab 16 Jahren für gemeinsame Freizeitaktivitäten zusammen. Die externe Datenschutzbeauftragte Dr. Marta Kowalczyk hat ein Löschkonzept beschlossen, das Du zusammen mit Deinem Mit-Azubi Jonas Weber in der MariaDB-Datenbank umsetzt:

  1. Stattgefundene Aktivitäten, deren Beginn mehr als 14 Tage zurückliegt, werden ohne Personenbezug in eine Archivtabelle übernommen (für Statistiken). Danach werden die Aktivität und alle Teilnahmen und Bewertungen dazu gelöscht.

  2. Abgesagte Aktivitäten werden nach 14 Tagen ohne Archiv gelöscht.

  3. Gesperrte Konten, deren letzter Login mehr als 180 Tage zurückliegt, werden anonymisiert.

Auszug aus dem Datenmodell (Primärschlüssel unterstrichen, Fremdschlüssel mit #):

  • benutzer (benutzer_id, vorname, nachname, email, passwort_hash, geburtsdatum, geschlecht, strasse, hausnr, plz, ort, verifiziert, profil_sichtbar, status, registriert_am, letzter_login)

  • hobby (hobby_id, bezeichnung, kategorie)

  • treffpunkt (treffpunkt_id, name, strasse, plz, ort, …)

  • aktivitaet (aktivitaet_id, titel, #hobby_id, #ersteller_id, #treffpunkt_id, beginn, dauer_min, max_teilnehmende, kosten_eur, mindestalter, status, …)

  • teilnahme (#aktivitaet_id, #benutzer_id, angemeldet_am, status) – Status u. a. 'teilgenommen', 'nicht_erschienen', 'abgemeldet'

  • bewertung (bewertung_id, #aktivitaet_id, #bewerter_id, sterne, kommentar, erstellt_am)

  • meldung (meldung_id, #melder_id, #gemeldet_id, #aktivitaet_id, grund, beschreibung, …, ergebnis)

Fremdschlüssel auf aktivitaet: teilnahme und bewertung mit ON DELETE CASCADE, meldung mit ON DELETE SET NULL. Die Spalten strasse, hausnr und geschlecht in benutzer sind optional, status ist ein ENUM('aktiv','gesperrt','geloescht').

Tabelle aktivitaet (Auszug, mit Bezeichnungen aus den verbundenen Tabellen)
aktivitaet_id titel hobby (aus hobby) ort (aus treffpunkt) beginn status
1 Brettspielabend: Kooperative Spiele Brettspiele Bielefeld 2026-09-25 19:00:00 stattgefunden
2 Sonntagswanderung Teutoburger Wald (12 km) Wandern Bielefeld 2026-10-04 10:00:00 stattgefunden
3 Feierabend-Kick Fußball Bielefeld 2026-10-21 18:30:00 geplant
6 Schach im Park Schach Bielefeld 2026-09-27 15:00:00 abgesagt
8 Kochabend: Mezze mit Weinbegleitung Kochen Herford 2026-10-17 18:00:00 stattgefunden

Zu Aktivität 1 gibt es fünf Teilnahmen (vier mit Status 'teilgenommen', eine 'nicht_erschienen'), zwei Bewertungen (5 und 4 Sterne) und eine Meldung. Zu Aktivität 6 gibt es eine Teilnahme mit Status 'abgemeldet'.

Die Archivtabelle hat Marta bereits anlegen lassen:

CREATE TABLE aktivitaet_archiv (
  aktivitaet_id  INT UNSIGNED NOT NULL PRIMARY KEY,
  titel          VARCHAR(100) NOT NULL,
  hobby          VARCHAR(50)  NOT NULL,     -- Bezeichnung als Text
  ort            VARCHAR(60)  NOT NULL,     -- Ort des Treffpunkts
  monat          CHAR(7)      NOT NULL,     -- 'JJJJ-MM'
  teilnehmende   TINYINT UNSIGNED NOT NULL, -- Teilnahmen mit Status 'teilgenommen'
  sterne_schnitt DECIMAL(3,2) NULL          -- NULL, wenn es keine Bewertung gibt
);

a) Erstelle eine SQL-Anweisung, die Regel 1 umsetzt, soweit es das Archivieren betrifft: Alle stattgefundenen Aktivitäten, deren Beginn mehr als 14 Tage vor dem heutigen Tag liegt, sollen mit einer Anweisung in die Archivtabelle übernommen werden. Nutze die SQL-Kurzreferenz in der Anlage. Gib den archivierten Datensatz an. (8 Punkte)

INSERT INTO aktivitaet_archiv (aktivitaet_id, titel, hobby, ort, monat,
                               teilnehmende, sterne_schnitt)
SELECT a.aktivitaet_id,
       a.titel,
       h.bezeichnung,
       tp.ort,
       DATE_FORMAT(a.beginn, '%Y-%m'),
       (SELECT COUNT(*)
        FROM teilnahme AS t
        WHERE t.aktivitaet_id = a.aktivitaet_id
          AND t.status = 'teilgenommen'),
       (SELECT AVG(b.sterne)
        FROM bewertung AS b
        WHERE b.aktivitaet_id = a.aktivitaet_id)
FROM aktivitaet AS a
  INNER JOIN hobby AS h       ON h.hobby_id = a.hobby_id
  INNER JOIN treffpunkt AS tp ON tp.treffpunkt_id = a.treffpunkt_id
WHERE a.status = 'stattgefunden'
  AND a.beginn < CURDATE() - INTERVAL 14 DAY;
aktivitaet_id titel hobby ort monat teilnehmende sterne_schnitt
1 Brettspielabend: Kooperative Spiele Brettspiele Bielefeld 2026-09 4 4.50

CURDATE() - INTERVAL 14 DAY ist der 04.10.2026 (00:00 Uhr). Die Wanderung (Aktivität 2) beginnt am 04.10.2026 um 10:00 Uhr und ist damit noch nicht älter als 14 Tage.

Bewertung: INSERT INTO … (Spaltenliste) SELECT 1 P, Verbund mit hobby und treffpunkt 1 P, DATE_FORMAT (oder CONCAT(YEAR(…), '-', LPAD(MONTH(…), 2, '0'))) 1 P, Teilnehmende mit Statusbedingung 1 P, Durchschnitt der Sterne 1 P, Statusbedingung 1 P, Datumsbedingung 1 P, Ergebniszeile 1 P.

Typischer Fehler: teilnahme und bewertung gleichzeitig per LEFT JOIN verbinden und dann gruppieren. Jede der 4 Teilnahmen wird dann mit jeder der 2 Bewertungen kombiniert: COUNT liefert 8 statt 4 Teilnehmende (der Durchschnitt bleibt gleich, weil jede Bewertung gleich oft vervielfacht wird). Abhilfe sind die Unterabfragen in der Spaltenliste – oder COUNT(DISTINCT t.benutzer_id) (dann aber nicht für SUM). Eine Lösung ohne Statusbedingung bei den Teilnahmen zählt auch die Teilnahme mit Status 'nicht_erschienen' mit (5 statt 4 Teilnehmende). Mit NOW() - INTERVAL 14 DAY statt CURDATE() wird nach 10:00 Uhr auch die Wanderung (Aktivität 2) erfasst; das ist vertretbar, wenn man „14 Tage“ auf die Uhrzeit genau versteht (Datumsbedingung trotzdem 1 P).

b) Erstelle die Anweisung, die anschließend die archivierten Aktivitäten löscht. Erläutere, was dabei in den Tabellen teilnahme, bewertung und meldung geschieht. (4 Punkte)

DELETE FROM aktivitaet
WHERE aktivitaet_id IN (SELECT aktivitaet_id FROM aktivitaet_archiv);
  • teilnahme und bewertung: Wegen ON DELETE CASCADE werden die fünf Teilnahmen und zwei Bewertungen von Aktivität 1 automatisch mitgelöscht.

  • meldung: Die Meldung bleibt erhalten (Nachvollziehbarkeit der Moderation), ihr Fremdschlüssel aktivitaet_id wird wegen ON DELETE SET NULL auf NULL gesetzt.

Bewertung: DELETE mit Unterabfrage auf das Archiv 2 P (dieselbe Bedingung wie in a) ist ebenfalls richtig, siehe aber c), 1 P), Erläuterung CASCADE 1 P, SET NULL 1 P.

c) Jonas fragt, warum Du in b) auf die Archivtabelle verweist, statt einfach die Bedingung aus a) zu wiederholen, und ob es schlimm wäre, wenn zwischen a) und b) ein Fehler auftritt. Begründe Dein Vorgehen und schreibe den Rahmen, mit dem a) und b) gemeinsam ausgeführt werden. (4 Punkte)

Verweis auf das Archiv: Gelöscht wird nur, was nachweislich archiviert wurde. Eine wiederholte Bedingung kann eine andere Menge treffen – etwa wenn zwischen den Anweisungen der Status einer Aktivität auf 'stattgefunden' gesetzt wird oder um Mitternacht CURDATE() wechselt. Dann würde eine Aktivität gelöscht, ohne archiviert zu sein. (1 P)

Fehler zwischen a) und b): Ohne Transaktion bliebe ein halber Zustand zurück (archiviert, aber nicht gelöscht – oder bei umgekehrter Reihenfolge gelöscht, aber nicht archiviert, was die Daten für immer vernichtet). Eine Transaktion macht beide Schritte zu einer Einheit: Entweder beide wirken oder keiner. (1 P)

START TRANSACTION;

INSERT INTO aktivitaet_archiv (…)
SELECT …;                      -- Anweisung aus a)

DELETE FROM aktivitaet
WHERE aktivitaet_id IN (SELECT aktivitaet_id FROM aktivitaet_archiv);

COMMIT;                        -- bei einem Fehler stattdessen ROLLBACK;

Bewertung: Rahmen mit START TRANSACTION (oder BEGIN) und COMMIT 1 P, ROLLBACK für den Fehlerfall 1 P. Das CREATE TABLE der Archivtabelle gehört nicht in die Transaktion – DDL-Anweisungen lösen in MariaDB einen impliziten COMMIT aus.

d) Setze Regel 2 um und gib an, welche Datensätze gelöscht werden. (3 Punkte)

DELETE FROM aktivitaet
WHERE status = 'abgesagt'
  AND beginn < CURDATE() - INTERVAL 14 DAY;

Gelöscht wird Aktivität 6 („Schach im Park“, 27.09.2026) und per CASCADE ihre eine Teilnahme.

Bewertung: Statusbedingung 1 P, Datumsbedingung 1 P, Ergebnis mit Hinweis auf die kaskadierte Teilnahme 1 P. DATEDIFF(CURDATE(), beginn) > 14 ist ebenso richtig.

e) Setze Regel 3 um. Bei der Anonymisierung sollen Vor- und Nachname durch „gelöscht“ ersetzt werden und die E-Mail-Adresse durch geloescht-<benutzer_id>@anonym.example (die Spalte ist UNIQUE). Straße, Hausnummer und Geschlecht werden geleert, der Status wird auf 'geloescht' gesetzt. Nutze die SQL-Kurzreferenz.

ea) Erstelle die SQL-Anweisung. Im Datenbestand ist nur Elena Wagner (benutzer_id 11, gesperrt, letzter Login am 01.03.2026) betroffen. (6 Punkte)

eb) In der Tabelle meldung ist Elena (gemeldet_id 11) als gemeldete Person einer Meldung wegen Spam eingetragen; die Meldung bleibt gespeichert. Nimm Stellung, ob der Datensatz danach anonym ist. (2 Punkte)

ea)

UPDATE benutzer
SET vorname    = 'gelöscht',
    nachname   = 'gelöscht',
    email      = CONCAT('geloescht-', benutzer_id, '@anonym.example'),
    strasse    = NULL,
    hausnr     = NULL,
    geschlecht = NULL,
    status     = 'geloescht'
WHERE status = 'gesperrt'
  AND DATEDIFF(CURDATE(), letzter_login) > 180;

Ergebnis: Das Konto 11 hat jetzt vorname = nachname = 'gelöscht', email = 'geloescht-11@anonym.example', strasse, hausnr, geschlecht sind NULL, status = 'geloescht' (231 Tage seit dem letzten Login).

Bewertung: sieben Zuweisungen mit Komma 2 P (davon CONCAT mit der eigenen benutzer_id 1 P), NULL setzen mit = NULL im SET 1 P, Statusbedingung 1 P, Datumsbedingung 1 P, Status wird mitgeändert (sonst trifft die Regel das Konto beim nächsten Lauf erneut) 1 P. letzter_login < CURDATE() - INTERVAL 180 DAY ist ebenso richtig. Eine konstante E-Mail-Adresse für alle Konten scheitert an UNIQUE, sobald zwei Konten anonymisiert werden.

eb) Nicht vollständig: Geburtsdatum, Postleitzahl, Ort, Registrierungszeitpunkt und der Passwort-Hash bleiben stehen; zusammen mit der verbleibenden Meldung, in der Elena als gemeldete Person eingetragen ist (bei anderen Konten auch mit Teilnahmen oder Freundschaften), lässt sich die Person ggf. wieder bestimmen. Damit ist der Datensatz eher pseudonymisiert. Vorschlag: Geburtsdatum auf das Geburtsjahr vergröbern, Passwort-Hash überschreiben, nicht mehr benötigte verknüpfte Daten löschen – und prüfen, ob Meldungen aus Gründen der Nachvollziehbarkeit (berechtigtes Interesse) noch gebraucht werden. (2 P für Einschätzung mit Begründung)

f) Jonas wollte Konten ohne vollständige Adresse als „nicht verifiziert“ kennzeichnen und hat dazu geschrieben:

UPDATE benutzer SET verifiziert = FALSE;
WHERE strasse = NULL OR hausnr = NULL;

Im Datenbestand fehlen Straße und Hausnummer bei Jana Meier (benutzer_id 8, verifiziert = FALSE) und bei dem in e) anonymisierten Konto 11; alle übrigen Konten haben eine vollständige Adresse.

Beschreibe die Fehler und ihre Wirkung und korrigiere die Anweisung so, dass anonymisierte Konten nicht betroffen sind. (4 Punkte)

  • Das Semikolon nach FALSE beendet das UPDATE: Es setzt verifiziert = FALSE bei allen Konten. Die zweite Zeile ist eine eigene Anweisung und erzeugt einen Syntaxfehler – der Schaden ist aber schon passiert (ohne Transaktion nicht rückgängig zu machen). (1 P)

  • = NULL ist nie wahr; auch mit richtig gesetztem WHERE würde keine Zeile getroffen. Es muss IS NULL heißen. (1 P)

UPDATE benutzer
SET verifiziert = FALSE
WHERE (strasse IS NULL OR hausnr IS NULL)
  AND status <> 'geloescht';

Getroffen wird nur Jana Meier (8). Weil sie bereits nicht verifiziert ist, meldet MariaDB „Rows matched: 1 Changed: 0“. Ohne die Klammern bindet AND stärker als OR: Die Bedingung hieße strasse IS NULL OR (hausnr IS NULL AND status <> 'geloescht') und würde anonymisierte Konten (dort ist strasse NULL) doch erfassen – im Datenbestand Elena Wagner (11). (2 P für die Korrektur einschließlich Klammern)

g) In der Tabelle altdaten_import liegt noch ein Excel-Export des früheren Dienstleisters. Die neue Kontaktverwaltung speichert Vorwahl und Rufnummer getrennt, die Spalte telefon enthält beides mit Leerzeichen dazwischen – manchmal auch mit weiteren Leerzeichen innerhalb der Rufnummer:

Tabelle altdaten_import (Auszug)
zeile name telefon geburtsdatum
1 Hoffmann, Sabine 0521 4471230 1988-11-23
2 Haddad, Malik 05205 98314 1902-03-08
10 Çelik, Deniz 030 221 9087 1995-06-14

ga) Jonas möchte die neuen Spalten vorwahl und rufnummer als INT anlegen, „weil da ja nur Ziffern drinstehen“. Begründe, warum das ungeeignet ist, und erstelle mit passenden Datentypen die Anweisungen, mit denen Du die Spalten ergänzt, die Inhalte aus telefon überträgst (die Rufnummer ohne Leerzeichen) und telefon anschließend entfernst. Nutze die SQL-Kurzreferenz. (6 Punkte)

gb) Bei Malik Haddad ist das Geburtsdatum als '1902-03-08' statt '2002-03-08' erfasst. Korrigiere den Eintrag so, dass auch bei einer zweiten Ausführung kein anderer Datensatz verändert wird. (2 Punkte)

ga) Mit INT ginge die führende Null verloren: Aus 0521 würde 521, aus 030 würde 30 – die Vorwahl wäre unbrauchbar. Telefonnummern sind Kennungen, mit denen niemand rechnet; außerdem könnten später Zeichen wie +49 vorkommen. Passend sind Textspalten, z. B. VARCHAR(6) und VARCHAR(12).

ALTER TABLE altdaten_import
ADD COLUMN vorwahl   VARCHAR(6),
ADD COLUMN rufnummer VARCHAR(12);

UPDATE altdaten_import
SET vorwahl   = SUBSTRING_INDEX(telefon, ' ', 1),
    rufnummer = REPLACE(SUBSTRING(telefon, LOCATE(' ', telefon) + 1), ' ', '');

-- Kontrolle: SELECT telefon, vorwahl, rufnummer FROM altdaten_import;
ALTER TABLE altdaten_import
DROP COLUMN telefon;

Ergebnis (Auszug):

zeile vorwahl rufnummer
1 0521 4471230
2 05205 98314
10 030 2219087

Bewertung: Begründung gegen INT (führende Null, keine Rechengröße) 2 P, ADD COLUMN mit Textdatentyp 1 P, Vorwahl über SUBSTRING_INDEX(telefon, ' ', 1) 1 P, Rufnummer als Rest nach dem ersten Leerzeichen mit REPLACE 1 P, DROP COLUMN erst nach dem UPDATE 1 P. SUBSTRING_INDEX(telefon, ' ', -1) für die Rufnummer liefert in Zeile 10 nur 9087 – genau solche Fälle zeigt die Kontrolle mit SELECT vor dem DROP COLUMN, das sich nicht zurücknehmen lässt.

gb)

UPDATE altdaten_import
SET geburtsdatum = '2002-03-08'
WHERE name = 'Haddad, Malik'
  AND geburtsdatum = '1902-03-08';

Bewertung: Zuweisung im ISO-Format 1 P, Bedingung, die genau diesen Datensatz trifft (Primärschlüssel zeile = 2 ist ebenso richtig) 1 P. Durch die Bedingung auf den alten Wert ändert eine zweite Ausführung nichts mehr.

Aufgabe 2.4: Datenpflege in einer Mitfahr-App (Kernkompetenz)

(ca. 35 min Bearbeitungszeit)

Die Schülervertretung des Ratz und Rübe Berufskollegs (RRB) hat die Mitfahr-App „RatzFahrt“ angestoßen: Auszubildende aus dem Umland bilden für die Berufsschultage Fahrgemeinschaften. Du arbeitest im FIA-Projektteam und übernimmst Wartungsaufgaben, die Andrea Wolf (Datenschutzbeauftragte der Schule und Administratorin der App) festgelegt hat. Auszug aus dem Datenmodell (Primärschlüssel unterstrichen, Fremdschlüssel mit #):

  • nutzer (nutzer_id, pseudonym, vorname, nachname, email_schule, geburtsdatum, #klasse_id, rolle, einwilligung_eltern, fuehrerschein, registriert_am, aktiv)

  • haltepunkt (haltepunkt_id, name, ort, plz, breitengrad, laengengrad, entfernung_km)

  • fahrzeug (fahrzeug_id, #nutzer_id, typ, farbe, sitzplaetze)

  • angebot (angebot_id, #fahrer_id, #fahrzeug_id, #haltepunkt_id, richtung, datum, abfahrt, freie_plaetze, kostenbeitrag_eur, status, erstellt_am)

  • gesuch (gesuch_id, #nutzer_id, #haltepunkt_id, richtung, datum, zeit_von, zeit_bis, status, erstellt_am)

  • buchung (buchung_id, #angebot_id, #mitfahrer_id, #gesuch_id, status, angefragt_am, beantwortet_am)

  • bewertung (bewertung_id, #buchung_id, #von_nutzer_id, sterne, puenktlich, erstellt_am)

Auszug aus den Tabellendefinitionen:

CREATE TABLE fahrzeug (
  fahrzeug_id  INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  nutzer_id    INT UNSIGNED NOT NULL,
  typ          ENUM('PKW','Motorrad','Kleinbus') NOT NULL DEFAULT 'PKW',
  farbe        VARCHAR(20)  NULL,
  sitzplaetze  TINYINT UNSIGNED NOT NULL,        -- Mitfahrplätze ohne Fahrer*in
  CONSTRAINT chk_sitze CHECK (sitzplaetze BETWEEN 1 AND 8),
  FOREIGN KEY (nutzer_id) REFERENCES nutzer (nutzer_id) ON DELETE CASCADE
);
-- angebot:   status ENUM('offen','voll','abgesagt','durchgefuehrt'),
--            FOREIGN KEY (fahrer_id) und (fahrzeug_id) ohne ON DELETE-Regel
-- gesuch:    status ENUM('offen','vermittelt','zurueckgezogen','abgelaufen')
-- buchung:   status ENUM('angefragt','bestaetigt','abgelehnt','storniert'),
--            FOREIGN KEY (angebot_id) REFERENCES angebot (angebot_id) ON DELETE CASCADE,
--            FOREIGN KEY (gesuch_id)  REFERENCES gesuch (gesuch_id)   ON DELETE SET NULL
-- bewertung: FOREIGN KEY (buchung_id) REFERENCES buchung (buchung_id) ON DELETE CASCADE
Tabelle angebot (Auszug, mit Angaben aus haltepunkt)
angebot_id fahrer_id fahrzeug_id haltepunkt (Ort, km zur Schule) richtung datum freie_plaetze kostenbeitrag_eur status
1 1 1 1 (Gütersloh, 18,6) zur Schule 2026-10-19 2 1.80 offen
2 1 1 1 (Gütersloh, 18,6) nach Hause 2026-10-19 3 1.80 offen
3 6 3 3 (Halle, 15,2) zur Schule 2026-10-19 0 1.50 voll
4 7 4 2 (Herford, 17,9) zur Schule 2026-10-20 3 1.70 offen
5 9 5 4 (Bad Salzuflen, 19,4) zur Schule 2026-10-22 6 2.50 offen
6 5 2 1 (Gütersloh, 18,6) nach Hause 2026-10-15 3 0.00 durchgefuehrt
7 7 4 2 (Herford, 17,9) nach Hause 2026-10-14 4 1.70 abgesagt
Tabelle gesuch (Auszug)
gesuch_id nutzer_id datum status
1 2 2026-10-20 offen
2 3 2026-10-19 vermittelt
3 4 2026-10-19 vermittelt
4 8 2026-10-23 offen
5 4 2026-10-15 vermittelt
Tabelle buchung (zu Buchung 4 gibt es zwei Bewertungen)
buchung_id angebot_id mitfahrer_id gesuch_id status
1 3 3 2 bestaetigt
2 1 4 3 bestaetigt
3 1 8 NULL abgelehnt
4 6 4 5 bestaetigt
5 4 2 NULL angefragt

Minderjährige sind zurzeit Aylin Kaya (nutzer_id 2, geboren 02.06.2009, Einwilligung der Eltern vom 03.09.2026) und Lina Hartwig (8, geboren 23.11.2008, ohne Einwilligung). Ben Richter (1) ist am 17.01.2026 volljährig geworden.

a) Minderjährige dürfen die App nur mit Einwilligung der Erziehungsberechtigten nutzen. Deaktiviere alle Konten von Minderjährigen ohne Einwilligung. Nutze die SQL-Kurzreferenz. (4 Punkte)

UPDATE nutzer
SET aktiv = FALSE
WHERE TIMESTAMPDIFF(YEAR, geburtsdatum, CURDATE()) < 18
  AND einwilligung_eltern IS NULL;

Deaktiviert wird nur Lina Hartwig (8).

Bewertung: SET aktiv = FALSE 1 P, Altersberechnung mit TIMESTAMPDIFF (oder geburtsdatum > CURDATE() - INTERVAL 18 YEAR) 2 P, IS NULL 1 P. YEAR(CURDATE()) - YEAR(geburtsdatum) < 18 ist ungenau (berücksichtigt den Geburtstag nicht): 1 P für die Altersbedingung.

b) Offene Gesuche deaktivierter Konten sollen nicht mehr auf den Flurmonitoren erscheinen. Lösche sie und erläutere, warum Linas abgelehnte Buchungsanfrage dabei nicht verloren geht. (4 Punkte)

DELETE FROM gesuch
WHERE status = 'offen'
  AND nutzer_id IN (SELECT nutzer_id
                    FROM nutzer
                    WHERE aktiv = FALSE);

Gelöscht wird Gesuch 4. Linas Buchung 3 bezieht sich auf Angebot 1, nicht auf ein Gesuch (gesuch_id ist NULL); und selbst wenn sie auf das Gesuch verwiese, würde wegen ON DELETE SET NULL nur der Verweis geleert, die Buchung bliebe erhalten.

Bewertung: DELETE mit Unterabfrage 2 P, Statusbedingung 1 P, Erläuterung SET NULL bzw. fehlender Verweis 1 P.

c) Der Kostenbeitrag darf höchstens 0,10 € je Kilometer Entfernung des Haltepunkts zur Schule betragen. Setze bei allen offenen Angeboten, die diese Grenze überschreiten, den Kostenbeitrag auf den Höchstbetrag (auf Cent gerundet). Verwende einen Verbund im UPDATE (MariaDB-Syntax, siehe Kurzreferenz). Gib an, welches Angebot sich wie ändert. (6 Punkte)

UPDATE angebot AS a
  INNER JOIN haltepunkt AS h ON h.haltepunkt_id = a.haltepunkt_id
SET a.kostenbeitrag_eur = ROUND(h.entfernung_km * 0.10, 2)
WHERE a.status = 'offen'
  AND a.kostenbeitrag_eur > h.entfernung_km * 0.10;

Geändert wird nur Angebot 5 von Niklas Fischer (Bad Salzuflen, 19,4 km): von 2,50 € auf 1,94 €. Alle anderen offenen Angebote liegen unter ihrer Grenze (z. B. Gütersloh: 1,80 € ≤ 1,86 €).

Bewertung: UPDATE … INNER JOIN … ON 2 P, Zuweisung mit Berechnung und Rundung 1 P, Bedingung „überschreitet“ 1 P, Statusbedingung 1 P, Ergebnis 1 P.

Alternative ohne Verbund (Standard-SQL, korrelierte Unterabfragen):

UPDATE angebot
SET kostenbeitrag_eur = (SELECT ROUND(h.entfernung_km * 0.10, 2)
                         FROM haltepunkt AS h
                         WHERE h.haltepunkt_id = angebot.haltepunkt_id)
WHERE status = 'offen'
  AND kostenbeitrag_eur > (SELECT h.entfernung_km * 0.10
                           FROM haltepunkt AS h
                           WHERE h.haltepunkt_id = angebot.haltepunkt_id);

Wird abgerundet statt gerundet (FLOOR(h.entfernung_km * 10) / 100), ist das ebenso richtig – „höchstens“ ist damit sogar sicher eingehalten.

d) Am Abend des 20.10.2026 soll der Tagesabschluss laufen:

  1. Buchungsanfragen, die bis dahin unbeantwortet geblieben sind ('angefragt') und zu einem noch offenen Angebot bis einschließlich 20.10.2026 gehören, werden auf 'storniert' gesetzt.

  2. Offene und volle Angebote bis einschließlich 20.10.2026 gelten als durchgeführt.

  3. Offene Gesuche bis einschließlich 20.10.2026 sind abgelaufen.

Erstelle die drei SQL-Anweisungen und begründe, warum Schritt 1 vor Schritt 2 ausgeführt werden muss. (6 Punkte)

UPDATE buchung
SET status = 'storniert'
WHERE status = 'angefragt'
  AND angebot_id IN (SELECT angebot_id
                     FROM angebot
                     WHERE status = 'offen'
                       AND datum <= '2026-10-20');

UPDATE angebot
SET status = 'durchgefuehrt'
WHERE status IN ('offen', 'voll')
  AND datum <= '2026-10-20';

UPDATE gesuch
SET status = 'abgelaufen'
WHERE status = 'offen'
  AND datum <= '2026-10-20';

Ergebnis: Buchung 5 → 'storniert'; Angebote 1, 2, 3 und 4 → 'durchgefuehrt'; Gesuch 1 → 'abgelaufen'.

Reihenfolge: Schritt 1 erkennt die betroffenen Angebote am Status 'offen'. Nach Schritt 2 hätten sie diesen Status nicht mehr, die Unterabfrage fände kein Angebot und die Anfrage bliebe auf 'angefragt' stehen.

Bewertung: je Anweisung 1,5 P (Bedingungen mit IN bzw. <= und Datumsliteral im ISO-Format), Begründung 1,5 P. Mit CURDATE() statt '2026-10-20' ist es ebenfalls richtig, wenn der Abschluss am selben Tag läuft.

e) Neue Fahrzeuge werden in der Tabelle fahrzeug eingetragen.

ea) Marie Schulz (nutzer_id 7) hat einen neuen PKW mit 4 Mitfahrplätzen; die Farbe hat sie nicht angegeben. Erstelle die SQL-Anweisung mit möglichst kurzer Spaltenliste und gib den entstehenden Datensatz an. (2 Punkte)

eb) Niklas Fischer (9) möchte einen zweiten Kleinbus (grau) mit 9 Mitfahrplätzen eintragen. Erläutere, was bei der Anweisung INSERT INTO fahrzeug (nutzer_id, typ, farbe, sitzplaetze) VALUES (9, 'Kleinbus', 'grau', 9); geschieht. (2 Punkte)

ea)

INSERT INTO fahrzeug (nutzer_id, sitzplaetze)
VALUES (7, 4);
fahrzeug_id nutzer_id typ farbe sitzplaetze
6 7 PKW NULL 4

typ kommt aus dem DEFAULT, farbe bleibt NULL, die fahrzeug_id vergibt AUTO_INCREMENT. (1 P Anweisung, 1 P Datensatz)

eb) Die Bedingung chk_sitze erlaubt nur 1 bis 8 Mitfahrplätze. MariaDB lehnt das INSERT ab (Fehler 4025 „CONSTRAINT chk_sitze failed“), es wird kein Datensatz gespeichert. Ein Kleinbus mit Fahrer*in und 9 Mitfahrenden bräuchte ohnehin eine andere Führerscheinklasse – die Regel ist also auch fachlich sinnvoll. (2 P)

f) Zum Ende des Schuljahres müssen alle personenbezogenen Fahrtdaten gelöscht werden. Andrea möchte die Löschroutine vorab testen: Mit dem Stichtag 16.10.2026 sollen alle Angebote und Gesuche mit einem Datum vor dem Stichtag samt zugehörigen Buchungen und Bewertungen gelöscht werden. Weil es ein Test ist, soll am Ende alles wieder zurückgenommen werden.

fa) Erstelle die SQL-Anweisungen in einer Reihenfolge, die auch ohne ON DELETE CASCADE funktionieren würde, eingebettet in eine Transaktion, die am Ende zurückgenommen wird. Gib an, wie viele Datensätze je Tabelle gelöscht werden. (8 Punkte)

fb) Begründe die Reihenfolge und erläutere, welche Anweisungen wegen der vorhandenen ON DELETE-Regeln eigentlich entbehrlich wären. (3 Punkte)

fc) Andrea überlegt, für den echten Lauf am Schuljahresende TRUNCATE TABLE buchung; zu verwenden, „weil das schneller ist“. Nimm Stellung. (3 Punkte)

fa)

START TRANSACTION;

DELETE FROM bewertung
WHERE buchung_id IN (SELECT bu.buchung_id
                     FROM buchung AS bu
                       INNER JOIN angebot AS a ON a.angebot_id = bu.angebot_id
                     WHERE a.datum < '2026-10-16');

DELETE FROM buchung
WHERE angebot_id IN (SELECT angebot_id
                     FROM angebot
                     WHERE datum < '2026-10-16');

DELETE FROM angebot
WHERE datum < '2026-10-16';

DELETE FROM gesuch
WHERE datum < '2026-10-16';

ROLLBACK;
Gelöschte Datensätze im Testlauf
Tabelle gelöscht Datensätze
bewertung 2 beide Bewertungen zu Buchung 4
buchung 1 Buchung 4 (Angebot 6)
angebot 2 Angebote 6 und 7
gesuch 1 Gesuch 5

Nach ROLLBACK sind alle Datensätze wieder vorhanden.

Bewertung: Transaktion mit START TRANSACTION und ROLLBACK 2 P, DELETE für bewertung mit Unterabfrage über buchung und angebot 2 P, buchung 1 P, angebot 1 P, gesuch 1 P, Anzahlen 1 P.

fb) Die Datensätze werden von unten nach oben gelöscht: Bewertungen verweisen auf Buchungen, Buchungen auf Angebote. Ohne CASCADE würde das Löschen eines Angebots mit Buchungen am Fremdschlüssel scheitern (Fehler 1451). Das Gesuch kann vor oder nach den Buchungen gelöscht werden, weil buchung.gesuch_id mit SET NULL definiert ist (sonst müsste es nach den Buchungen kommen). Mit den vorhandenen Regeln wären die ersten beiden DELETE-Anweisungen entbehrlich: DELETE FROM angebot … löscht per CASCADE die Buchungen und darüber per CASCADE die Bewertungen. Die ausdrückliche Reihenfolge macht die Routine aber unabhängig von den Regeln und zeigt im Protokoll, was gelöscht wird. (1 P Reihenfolge, 1 P Gesuch/SET NULL, 1 P Kaskade)

fc) TRUNCATE ist hier ungeeignet:

  • Es löscht immer alle Zeilen, eine Bedingung (Stichtag) ist nicht möglich.

  • Es gehört zur DDL und beendet in MariaDB eine laufende Transaktion mit einem impliziten COMMIT; ein ROLLBACK ist danach nicht möglich.

  • Bei InnoDB wird TRUNCATE für eine Tabelle abgelehnt, auf die ein Fremdschlüssel einer anderen Tabelle verweist (hier bewertung.buchung_id).

Bei den kleinen Datenmengen einer Schul-App ist der Geschwindigkeitsvorteil zudem unerheblich. DELETE mit Bedingung in einer Transaktion ist die richtige Wahl. (je Argument 1 P)

Aufgabe 3.1: Händlerdaten ins neue Händlerportal übernehmen (Profilkompetenz)

(ca. 32 min Bearbeitungszeit)

Die CSB-Rad GmbH löst ihr altes Bestellprogramm für den Fachhandel durch ein Händlerportal ab. Natalia Wróbel (Buchhaltung und Controlling) hat die Händlerdaten aus dem alten Programm bereits bereinigt und in die Übergangstabelle haendler_bereinigt geladen. Die Spaltennamen stammen noch aus dem alten Export. Deine Aufgabe ist es, die Daten in die neuen Tabellen des Portals zu übernehmen.

CREATE TABLE haendler_bereinigt (
  `HNr`            INT UNSIGNED NOT NULL PRIMARY KEY,
  `Firma`          VARCHAR(100) NOT NULL,
  `Ansprechperson` VARCHAR(80)  NULL,
  `Strasse`        VARCHAR(100) NULL,
  `PLZ`            CHAR(5)      NOT NULL,
  `Ort`            VARCHAR(60)  NOT NULL,
  `E-Mail`         VARCHAR(100) NULL,
  `Rabatt`         DECIMAL(4,1) NULL,
  `Kunde seit`     DATE         NULL,
  `Status`         CHAR(1)      NOT NULL,   -- A aktiv, D Dublette, G Geschäft geschlossen, K Klärung offen
  `Bestellweg`     VARCHAR(10)  NOT NULL    -- 'Programm' = bisher über das alte Bestellprogramm
);
Inhalt der Übergangstabelle haendler_bereinigt (ohne die Spalten Ansprechperson, Strasse und E-Mail; sie sind außer bei 1026 überall gefüllt)
HNr Firma PLZ Ort Rabatt Kunde seit Status Bestellweg
1004 Radhaus Bielefeld-Süd GmbH 33649 Bielefeld 4.0 2024-11-05 A Programm
1011 Zweirad Lohmann e. K. 32756 Detmold 4.0 2019-03-01 A Telefon
1012 Radhaus Bielefeld Süd 33649 Bielefeld 5.0 NULL D Programm
1017 Velowerk Münster GmbH 48153 Münster 4.0 2020-07-12 A Programm
1023 Leipziger Radkontor GmbH 04275 Leipzig 4.0 2022-03-14 A E-Mail
1026 Fahrradladen am Sparrenberg 33602 Bielefeld NULL 2018-04-01 K Telefon
1031 Radsport Kemper GmbH 33378 Rheda-Wiedenbrück 4.0 2018-08-20 G Programm
1034 Pedale & Co. OHG 32105 Bad Salzuflen 4.0 NULL A Programm

Die neuen Tabellen des Portals:

CREATE TABLE bestellweg (
  bestellweg_id  INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  bezeichnung    VARCHAR(20)  NOT NULL UNIQUE
);

CREATE TABLE haendler (
  haendler_nr    INT UNSIGNED NOT NULL PRIMARY KEY,
  firma          VARCHAR(100) NOT NULL,
  ansprechperson VARCHAR(80)  NULL,
  strasse        VARCHAR(100) NOT NULL,
  plz            CHAR(5)      NOT NULL,
  ort            VARCHAR(60)  NOT NULL,
  email          VARCHAR(100) NOT NULL UNIQUE,
  rabatt_prozent DECIMAL(4,1) NOT NULL DEFAULT 0.0,
  kunde_seit     DATE         NULL,
  bestellweg_id  INT UNSIGNED NULL,
  portal_aktiv   BOOLEAN      NOT NULL DEFAULT FALSE,
  angelegt_am    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_h_bestellweg FOREIGN KEY (bestellweg_id) REFERENCES bestellweg (bestellweg_id)
);

a) Lege in der Tabelle bestellweg mit einer Anweisung die Bestellwege „Portal“, „Telefon“ und „E-Mail“ an. Die Schlüssel vergibt die Datenbank. (2 Punkte)

INSERT INTO bestellweg (bezeichnung)
VALUES ('Portal'), ('Telefon'), ('E-Mail');

Bewertung: Spaltenliste ohne Schlüsselspalte (die Werte vergibt AUTO_INCREMENT) 1 Punkt, drei Wertetupel in einer Anweisung mit Texten in Hochkommata 1 Punkt. Die Datenbank vergibt die Schlüssel 1, 2 und 3.

b) Erläutere, was referentielle Integrität bedeutet und warum bestellweg deshalb vor haendler befüllt werden muss. (3 Punkte)

Referentielle Integrität bedeutet, dass jeder Fremdschlüsselwert auf einen vorhandenen Primärschlüsselwert der referenzierten Tabelle verweist oder NULL ist (1 Punkt). Das DBMS prüft das bei jedem INSERT, UPDATE und DELETE (1 Punkt). haendler.bestellweg_id verweist auf bestellweg: Gäbe es dort noch keine Zeilen, würde jede Übernahme mit gesetztem Bestellweg abgelehnt – also werden Nachschlage- und Elterntabellen zuerst befüllt, abhängige Tabellen danach (1 Punkt).

c) Übernimm mit einer Anweisung alle aktiven Händler aus haendler_bereinigt in haendler. Wer bisher über das alte Programm bestellt hat, erhält den Bestellweg „Portal“; alle anderen behalten ihren Bestellweg. portal_aktiv und angelegt_am sollen ihre Standardwerte erhalten. Die Anweisung soll unabhängig davon funktionieren, welche Schlüssel die Bestellwege in a) bekommen haben. (9 Punkte)

INSERT INTO haendler (haendler_nr, firma, ansprechperson, strasse, plz, ort,
                      email, rabatt_prozent, kunde_seit, bestellweg_id)
SELECT h.`HNr`, h.`Firma`, h.`Ansprechperson`, h.`Strasse`, h.`PLZ`, h.`Ort`,
       h.`E-Mail`, h.`Rabatt`, h.`Kunde seit`, w.bestellweg_id
FROM haendler_bereinigt AS h
  LEFT JOIN bestellweg AS w
         ON w.bezeichnung = CASE h.`Bestellweg`
                              WHEN 'Programm' THEN 'Portal'
                              ELSE h.`Bestellweg`
                            END
WHERE h.`Status` = 'A';

Bewertung: Spaltenliste von haendler ohne portal_aktiv und angelegt_am, damit beide ihre Standardwerte erhalten (2 Punkte); SELECT mit denselben Spalten in derselben Reihenfolge – zugeordnet wird nur nach der Position, die Spaltennamen dürfen abweichen (2 Punkte); Spaltennamen mit Bindestrich bzw. Leerzeichen in Backticks (1 Punkt); Verbund mit bestellweg statt fester Schlüssel (2 Punkte); Umsetzen von „Programm“ auf „Portal“ mit CASE (1 Punkt); Bedingung Status = 'A' (1 Punkt).

Ergebnis: 5 Zeilen eingefügt.

Tabelle haendler nach der Übernahme (Auszug)
haendler_nr firma kunde_seit bestellweg_id portal_aktiv
1004 Radhaus Bielefeld-Süd GmbH 2024-11-05 1 0
1011 Zweirad Lohmann e. K. 2019-03-01 2 0
1017 Velowerk Münster GmbH 2020-07-12 1 0
1023 Leipziger Radkontor GmbH 2022-03-14 3 0
1034 Pedale & Co. OHG NULL 1 0

Der LEFT JOIN sorgt dafür, dass ein Händler auch dann übernommen wird, wenn sein Bestellweg nicht in bestellweg vorkommt (dann mit NULL); ein INNER JOIN ist hier ebenfalls richtig, weil alle Bestellwege vorhanden sind. Ebenfalls richtig ist eine Unterabfrage in der Spaltenliste: (SELECT w.bestellweg_id FROM bestellweg AS w WHERE w.bezeichnung = CASE … END). Die Zeilen 1012 (Dublette), 1026 (Klärung) und 1031 (geschlossen) werden bewusst nicht übernommen. Zur Kontrolle vergleicht man die Zeilenzahl der Quelle (SELECT COUNT(*) FROM haendler_bereinigt WHERE Status = 'A'; liefert 5) mit den eingefügten Zeilen.

d) Jonas, Dein Mit-Azubi, schlägt für c) eine kürzere Anweisung vor, „weil die Spalten doch inhaltlich zusammenpassen“:

INSERT INTO haendler
SELECT * FROM haendler_bereinigt WHERE Status = 'A';

Beurteile den Vorschlag. (3 Punkte)

Der Vorschlag scheitert: Ohne Spaltenliste erwartet haendler Werte für alle 12 Spalten, SELECT * liefert aber nur 11 (MariaDB: „Column count doesn’t match value count“) (1 Punkt). Selbst bei gleicher Spaltenzahl ordnet MariaDB nur nach der Position zu, nicht nach Namen oder Inhalt: Status ('A') würde in bestellweg_id landen und Bestellweg ('Programm') in portal_aktiv – das scheitert oder speichert Unsinn (1 Punkt). Außerdem fehlt die Umsetzung „Programm“ → „Portal“ über die Tabelle bestellweg. SELECT * eignet sich nur, wenn Quelle und Ziel exakt gleich aufgebaut sind (z. B. eine mit CREATE TABLE … LIKE angelegte Archivtabelle) (1 Punkt).

e) Die erste Pilotgruppe des Portals bilden alle Händler mit dem Bestellweg „Portal“, die seit mindestens einem Jahr Kunde sind. Schalte das Portal für sie frei (portal_aktiv), ohne die Schlüssel der Bestellwege oder das heutige Datum fest in die Anweisung zu schreiben. Gib an, welche Händler danach freigeschaltet sind, und begründe, warum Pedale & Co. nicht dazugehört. (6 Punkte)

UPDATE haendler
SET portal_aktiv = TRUE
WHERE bestellweg_id = (SELECT bestellweg_id
                       FROM bestellweg
                       WHERE bezeichnung = 'Portal')
  AND kunde_seit <= CURDATE() - INTERVAL 1 YEAR;

Bewertung: UPDATE … SET portal_aktiv = TRUE (1 Punkt); Bestellweg über eine Unterabfrage auf die Bezeichnung (2 Punkte); Datumsbedingung relativ zum heutigen Datum (1 Punkt). Freigeschaltet sind 1004 (Radhaus Bielefeld-Süd, Kunde seit 05.11.2024) und 1017 (Velowerk Münster) (1 Punkt). Pedale & Co. hat in kunde_seit den Wert NULL; jeder Vergleich mit NULL ergibt unbekannt, und WHERE übernimmt nur Zeilen, bei denen die Bedingung wahr ist (1 Punkt). Zweirad Lohmann und Leipziger Radkontor bestellen nicht über das Portal.

f) Beim Velowerk Münster ist ab sofort Elif Demir Ansprechperson, erreichbar unter elif.demir@velowerk-ms.example. Ändere den Datensatz und begründe, welche Bedingung Du dafür wählst. (3 Punkte)

UPDATE haendler
SET ansprechperson = 'Elif Demir',
    email          = 'elif.demir@velowerk-ms.example'
WHERE haendler_nr = 1017;

Bewertung: zwei Zuweisungen mit Komma getrennt (1 Punkt), Bedingung auf den Primärschlüssel (1 Punkt), Begründung (1 Punkt): Nur der Primärschlüssel identifiziert genau einen Datensatz. Firmennamen sind nicht als eindeutig definiert und werden unterschiedlich geschrieben (siehe die Dublette „Radhaus Bielefeld Süd“); eine Bedingung wie WHERE firma = 'Velowerk Münster GmbH' träfe womöglich mehrere oder gar keine Zeilen.

g) Jonas hat für einen neuen Händler folgende Anweisung vorbereitet, die MariaDB ablehnt:

INSERT INTO haendler (haendler_nr, firma, ansprechperson, strasse, plz, ort, e-mail,
                      rabatt_prozent, kunde_seit)
VALUES (1040, Fahrradwerkstatt Kessebohm, 'Anke Kessebohm', 'Herforder Straße 120',
        33609, 'Bielefeld', 'werkstatt@kessebohm.example', 3,5, 01.11.2026);

Nenne vier Fehler und gib die korrigierte Anweisung an. (6 Punkte)

Fehler (je 1 Punkt, höchstens 4):

  • Die Spalte heißt email; e-mail liest MariaDB als Rechnung e minus mail – in einer Spaltenliste ein Syntaxfehler.

  • Der Firmenname ist ein Text und muss in Hochkommata stehen.

  • 3,5 mit Dezimalkomma sind zwei Werte (3 und 5); damit stimmt die Zahl der Werte nicht mehr mit der Spaltenliste überein. Richtig ist 3.5.

  • Das Datum muss als Zeichenkette im ISO-Format stehen: '2026-11-01'.

  • Die Postleitzahl ist als CHAR(5) definiert und gehört in Hochkommata; als Zahl verlöre z. B. 04275 die führende Null.

INSERT INTO haendler (haendler_nr, firma, ansprechperson, strasse, plz, ort, email,
                      rabatt_prozent, kunde_seit)
VALUES (1040, 'Fahrradwerkstatt Kessebohm', 'Anke Kessebohm', 'Herforder Straße 120',
        '33609', 'Bielefeld', 'werkstatt@kessebohm.example', 3.5, '2026-11-01');

Korrigierte Anweisung: 2 Punkte. bestellweg_id bleibt NULL, portal_aktiv erhält FALSE, angelegt_am den aktuellen Zeitpunkt.

h) Nach erfolgreicher Übernahme sollen aus haendler_bereinigt alle Zeilen gelöscht werden, die bereits in haendler stehen; die übrigen bleiben für die Klärung erhalten. Formuliere die Anweisung und gib an, welche Zeilen übrig bleiben. (3 Punkte)

DELETE FROM haendler_bereinigt
WHERE `HNr` IN (SELECT haendler_nr
                FROM haendler);

Bewertung: DELETE FROM mit Bedingung (1 Punkt), Unterabfrage auf die übernommenen Schlüssel (1 Punkt), Ergebnis (1 Punkt): 5 Zeilen gelöscht, übrig bleiben 1012 (Dublette), 1026 (Klärung offen) und 1031 (geschlossen). TRUNCATE wäre falsch, weil es alle Zeilen entfernt. Eine Bedingung WHERE Status = 'A' führt hier zum selben Ergebnis, prüft aber nicht, ob die Übernahme wirklich geklappt hat.

Aufgabe 3.2: Termine, Prüfprotokolle und Stammdaten in der Werkstatt (Profilkompetenz)

(ca. 31 min Bearbeitungszeit)

In der Werkstatt der CSB-Rad GmbH in Bielefeld-Sennestadt plant Bastian Ludwig (Werkstattleitung) Montagen und Prüfungen für Anlagen und Mechaniker*innen in festen Zeitfenstern (Slot 1 = 8 Uhr, 2 = 10 Uhr, 3 = 13 Uhr, 4 = 15 Uhr). Du betreust als Auszubildende*r die Werkstattdatenbank. Ausschnitt aus dem Datenbankschema:

CREATE TABLE anlage (
  anlage_id   INT UNSIGNED NOT NULL PRIMARY KEY,
  bezeichnung VARCHAR(40)  NOT NULL UNIQUE
);

CREATE TABLE mitarbeiter (
  mitarbeiter_id INT UNSIGNED NOT NULL PRIMARY KEY,
  vorname        VARCHAR(40)  NOT NULL,
  nachname       VARCHAR(40)  NOT NULL,
  kuerzel        CHAR(3)      NOT NULL UNIQUE
);

CREATE TABLE montagetermin (
  termin_id      INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  anlage_id      INT UNSIGNED NOT NULL,
  mitarbeiter_id INT UNSIGNED NOT NULL,
  datum          DATE         NOT NULL,
  slot           TINYINT UNSIGNED NOT NULL,
  auftragsnr     CHAR(11)     NOT NULL,
  CONSTRAINT uq_belegung UNIQUE (anlage_id, datum, slot),
  CONSTRAINT uq_einsatz  UNIQUE (mitarbeiter_id, datum, slot),
  CONSTRAINT fk_mt_anlage FOREIGN KEY (anlage_id) REFERENCES anlage (anlage_id),
  CONSTRAINT fk_mt_person FOREIGN KEY (mitarbeiter_id) REFERENCES mitarbeiter (mitarbeiter_id)
);

CREATE TABLE pruefprotokoll (
  protokoll_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  zeitpunkt    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  anlage_id    INT UNSIGNED NOT NULL,
  auftragsnr   CHAR(11)     NOT NULL,
  akku_temp_c  DECIMAL(3,1) NOT NULL,
  bestanden    BOOLEAN      NOT NULL,
  CONSTRAINT fk_pp_anlage FOREIGN KEY (anlage_id) REFERENCES anlage (anlage_id)
);

CREATE TABLE leistungsart (
  leistungsart_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  bezeichnung     VARCHAR(60)  NOT NULL UNIQUE,
  stundensatz     DECIMAL(5,2) NOT NULL,
  aktiv           BOOLEAN      NOT NULL DEFAULT TRUE,
  CONSTRAINT chk_satz CHECK (stundensatz > 0)
);

CREATE TABLE werkstattauftrag (
  auftragsnr  CHAR(11)     NOT NULL PRIMARY KEY,
  kunde_id    INT UNSIGNED NOT NULL,
  annahme     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  zahlstatus  VARCHAR(10)  NOT NULL DEFAULT 'offen'
);

CREATE TABLE arbeitsposition (
  auftragsnr      CHAR(11)     NOT NULL,
  pos_nr          TINYINT UNSIGNED NOT NULL,
  leistungsart_id INT UNSIGNED NOT NULL,
  stunden         DECIMAL(4,2) NOT NULL,
  PRIMARY KEY (auftragsnr, pos_nr),
  CONSTRAINT fk_ap_auftrag  FOREIGN KEY (auftragsnr)      REFERENCES werkstattauftrag (auftragsnr) ON DELETE CASCADE,
  CONSTRAINT fk_ap_leistung FOREIGN KEY (leistungsart_id) REFERENCES leistungsart (leistungsart_id)
);
Tabelle anlage
anlage_id bezeichnung
1 Montageplatz 1
2 Montageplatz 2
3 Prüfstand E-Antrieb
Tabelle mitarbeiter
mitarbeiter_id vorname nachname kuerzel
1 Lea Brandt LBR
2 Timo Kaya TKA
3 Yusuf Demir YDE
Tabelle montagetermin
termin_id anlage_id mitarbeiter_id datum slot auftragsnr
1 3 1 2026-11-10 1 W-2026-0151
2 1 1 2026-11-10 3 W-2026-0152
3 2 3 2026-11-10 3 W-2026-0153
4 3 1 2026-11-11 2 W-2026-0154
5 2 1 2026-11-10 4 W-2026-0155
6 1 2 2026-11-10 1 W-2026-0160
Tabelle leistungsart
leistungsart_id bezeichnung stundensatz aktiv
1 Inspektion Basis 58.00 1
2 Bremsen entlüften 68.00 1
3 Nabenschaltung einstellen 56.00 1
4 Akku-Diagnose 78.00 1
Tabelle werkstattauftrag (Auszug)
auftragsnr zahlstatus
W-2026-0141 1
W-2026-0142 bezahlt
W-2026-0147 0
W-2026-0148 1
W-2026-0150 offen
W-2026-0151 0
Tabelle arbeitsposition
auftragsnr pos_nr leistungsart_id stunden
W-2026-0141 1 1 1.00
W-2026-0141 2 3 0.50
W-2026-0147 1 2 0.75
W-2026-0151 1 4 1.25

Lea Brandt ist am 10.11.2026 kurzfristig zu einer Herstellerschulung eingeladen. Bastian Ludwig möchte, dass Timo Kaya ihre Termine an diesem Tag übernimmt; ihre übrigen Termine bleiben bei ihr.

a) Bevor Du etwas änderst, brauchst Du einen Überblick: Ermittle mit einer Abfrage Leas Termine am 10.11.2026 mit Slot, Anlagenbezeichnung und Auftragsnummer, nach Slot sortiert. Eine zusätzliche Spalte soll zeigen, welchen Auftrag Timo Kaya im selben Slot bereits hat (leer, wenn er frei ist). Wähle die Personen über ihr Kürzel aus. (4 Punkte)

SELECT t.slot, a.bezeichnung, t.auftragsnr,
       k.auftragsnr AS schon_bei_timo
FROM montagetermin AS t
  INNER JOIN mitarbeiter AS m ON m.mitarbeiter_id = t.mitarbeiter_id
  INNER JOIN anlage      AS a ON a.anlage_id      = t.anlage_id
  LEFT JOIN  montagetermin AS k
         ON  k.datum = t.datum
         AND k.slot  = t.slot
         AND k.mitarbeiter_id = (SELECT mitarbeiter_id FROM mitarbeiter WHERE kuerzel = 'TKA')
WHERE m.kuerzel = 'LBR'
  AND t.datum = '2026-11-10'
ORDER BY t.slot;
Überblick vor der Umbuchung
slot bezeichnung auftragsnr schon_bei_timo
1 Prüfstand E-Antrieb W-2026-0151 W-2026-0160
3 Montageplatz 1 W-2026-0152 NULL
4 Montageplatz 2 W-2026-0155 NULL

Bewertung: Verbund mit mitarbeiter und anlage, Auswahl über das Kürzel und ISO-Datum (2 Punkte), LEFT JOIN auf eine zweite Instanz von montagetermin mit Datum, Slot und Timo als Bedingung im ON (1 Punkt), Spalten und Sortierung (1 Punkt). Steht Timos Bedingung im WHERE statt im ON, fallen die Zeilen ohne Treffer weg – dann fehlen die Slots 3 und 4. Der Termin am 11.11.2026 (W-2026-0154) gehört nicht ins Ergebnis.

b) Jonas hat die Umbuchung schon vorbereitet und will sie ausführen:

UPDATE montagetermin
SET mitarbeiter_id = 2
WHERE mitarbeiter_id = 1;

Beurteile die Anweisung: Was meldet MariaDB, wie viele Termine sind danach umgebucht, und was wäre auch ohne Fehlermeldung falsch? (4 Punkte)

MariaDB bricht mit Duplicate entry '2-2026-11-10-1' for key 'uq_einsatz' ab (1 Punkt): Timo ist am 10.11.2026 in Slot 1 schon mit W-2026-0160 eingeplant und kann nicht zwei Termine gleichzeitig haben. Bei InnoDB-Tabellen wird eine einzelne Anweisung ganz oder gar nicht ausgeführt – auch Zeilen, die schon geändert waren, werden zurückgenommen, es sind 0 Termine umgebucht (1 Punkt).

Auch ohne den Konflikt wäre die Anweisung falsch: Ohne Datumsbedingung würde auch Leas Termin am 11.11.2026 (W-2026-0154) an Timo gehen (1 Punkt). Außerdem verlässt sich Jonas auf auswendig gewusste Schlüsselwerte; die Auswahl über das eindeutige Kürzel ist lesbarer und weniger fehleranfällig (1 Punkt).

c) Bastian Ludwig entscheidet: Timos bisheriger Termin W-2026-0160 geht an Yusuf Demir, der in Slot 1 frei ist. Formuliere die Anweisungen, die den Konflikt auflösen und Leas Termine vom 10.11.2026 auf Timo umbuchen, so dass nie ein halber Stand gespeichert wird. Wähle die Personen über ihr Kürzel aus. (5 Punkte)

START TRANSACTION;

UPDATE montagetermin
SET mitarbeiter_id = (SELECT mitarbeiter_id FROM mitarbeiter WHERE kuerzel = 'YDE')
WHERE auftragsnr = 'W-2026-0160';

UPDATE montagetermin
SET mitarbeiter_id = (SELECT mitarbeiter_id FROM mitarbeiter WHERE kuerzel = 'TKA')
WHERE datum = '2026-11-10'
  AND mitarbeiter_id = (SELECT mitarbeiter_id FROM mitarbeiter WHERE kuerzel = 'LBR');

COMMIT;

Bewertung: Transaktion mit Abschluss (1 Punkt), Konflikt zuerst auflösen (1 Punkt), zweites UPDATE mit Datums- und Personenbedingung (2 Punkte), Personen über Unterabfragen auf das Kürzel (1 Punkt). Danach betreut Timo am 10.11.2026 die Slots 1, 3 und 4 (W-2026-0151, W-2026-0152, W-2026-0155), Yusuf die Slots 1 und 3 (W-2026-0160, W-2026-0153); W-2026-0154 am 11.11.2026 bleibt bei Lea. In umgekehrter Reihenfolge scheitert das zweite UPDATE wie in b). Ebenfalls richtig ist, Leas Slot-1-Termin direkt an Yusuf zu geben und nur die übrigen an Timo.

d) Die Tabelle montagetermin hat zwei Eindeutigkeitsregeln. Erläutere an je einem Beispiel aus den Daten, welche Doppelbuchung uq_belegung und welche uq_einsatz verhindert. (2 Punkte)

uq_belegung verhindert, dass eine Anlage im selben Zeitfenster zweimal belegt wird: Ein weiterer Termin auf dem Prüfstand E-Antrieb am 10.11.2026 in Slot 1 würde abgelehnt, auch wenn ihn eine andere Person übernehmen soll (1 Punkt). uq_einsatz verhindert, dass eine Person gleichzeitig an zwei Anlagen eingeplant ist – genau das hat in b) die Umbuchung von W-2026-0151 auf Timo blockiert (1 Punkt).

e) Der Prüfstand schreibt jede Messung in die Tabelle pruefprotokoll. Füge die Messung für Auftrag W-2026-0154 ein: Akkutemperatur 31,4 °C, Prüfung nicht bestanden. Den Zeitpunkt soll die Datenbank selbst setzen. (3 Punkte)

INSERT INTO pruefprotokoll (anlage_id, auftragsnr, akku_temp_c, bestanden)
VALUES (3, 'W-2026-0154', 31.4, FALSE);

Bewertung: Spaltenliste ohne protokoll_id und zeitpunkt – beide setzt die Datenbank (AUTO_INCREMENT, DEFAULT CURRENT_TIMESTAMP) (1 Punkt); Dezimalpunkt statt Komma (1 Punkt); Wahrheitswert FALSE bzw. 0 ohne Hochkommata und Auftragsnummer als Text (1 Punkt). Ebenfalls richtig ist die Anlage über eine Unterabfrage ((SELECT anlage_id FROM anlage WHERE bezeichnung = 'Prüfstand E-Antrieb')).

f) Alle Stundensätze steigen zum 01.01.2027 um 2,5 %. Formuliere die Anweisung (Ergebnis auf Cent gerundet) und gib den neuen Stundensatz für „Akku-Diagnose“ an. (3 Punkte)

UPDATE leistungsart
SET stundensatz = ROUND(stundensatz * 1.025, 2);

Bewertung: Faktor 1.025 (1 Punkt), ROUND(…, 2) (1 Punkt), neuer Satz Akku-Diagnose 78,00 € · 1,025 = 79,95 € (1 Punkt). Ein WHERE ist hier nicht nötig, weil ausdrücklich alle Zeilen geändert werden sollen; die übrigen Sätze lauten danach 59,45 €, 69,70 € und 57,40 €.

g) Durch einen Fehler im alten Kassenprogramm steht in zahlstatus bei einigen Aufträgen '1' (bezahlt) oder '0' (offen) statt des Texts. Bereinige beide Fälle mit einer Anweisung, ohne andere Zeilen zu verändern. (4 Punkte)

UPDATE werkstattauftrag
SET zahlstatus = CASE zahlstatus
                   WHEN '1' THEN 'bezahlt'
                   WHEN '0' THEN 'offen'
                 END
WHERE zahlstatus IN ('0', '1');

Bewertung: CASE mit beiden Fällen (2 Punkte), Bedingung, die nur die fehlerhaften Zeilen auswählt (1 Punkt), Werte als Text in Hochkommata (1 Punkt). 4 Zeilen werden geändert (0141 und 0148 auf „bezahlt“, 0147 und 0151 auf „offen“). Ohne WHERE würde CASE bei den übrigen Zeilen keinen Zweig finden und NULL liefern – das scheitert an NOT NULL bzw. löscht den Status. Ebenfalls richtig ist ELSE zahlstatus statt der Bedingung. Zwei einzelne UPDATE-Anweisungen lösen das Problem auch, erfüllen aber nicht die Vorgabe „eine Anweisung“ (1 Punkt Abzug).

h) Jonas schlägt für g) vor: UPDATE werkstattauftrag SET zahlstatus = 'bezahlt' WHERE bezahlt = 1; Beurteile den Vorschlag. (2 Punkte)

Die Bedingung prüft eine Spalte bezahlt, die es nicht gibt – MariaDB meldet „Unknown column ‘bezahlt’ in ‘where clause’“ (1 Punkt). Gemeint ist die fehlerhafte Spalte selbst: WHERE zahlstatus = '1'. Außerdem bleibt der Fall '0' → „offen“ unbearbeitet (1 Punkt).

i) Die Leistungsart „Nabenschaltung einstellen“ wird nicht mehr angeboten. Jonas will sie mit folgender Anweisung entfernen:

DELETE FROM leistungsart
WHERE bezeichnung = 'Nabenschaltung einstellen';

ia) Erläutere, was beim Ausführen passiert, und begründe es mit der Tabellendefinition. (3 Punkte)

Das DELETE wird abgelehnt (MariaDB-Fehler 1451 „Cannot delete or update a parent row: a foreign key constraint fails“) (1 Punkt). Die Arbeitsposition 2 des Auftrags W-2026-0141 verweist über fk_ap_leistung auf die Leistungsart 3 (1 Punkt). Für diesen Fremdschlüssel ist keine ON DELETE-Regel angegeben, also gilt RESTRICT: Ein Elterndatensatz mit abhängigen Datensätzen darf nicht gelöscht werden, sonst ginge die referentielle Integrität verloren (1 Punkt).

ib) Jemand schlägt vor, den Fremdschlüssel fk_ap_leistung auf ON DELETE CASCADE (Löschweitergabe) umzustellen. Beurteile den Vorschlag und gib eine bessere Lösung als SQL-Anweisung an. (4 Punkte)

Mit Löschweitergabe würden beim Löschen der Leistungsart auch alle Arbeitspositionen gelöscht, die sie verwenden – hier die Position des abgeschlossenen Auftrags W-2026-0141. Damit fehlen abgerechnete Leistungen in Aufträgen und Rechnungen, die aufbewahrt werden müssen (2 Punkte). CASCADE passt nur für Teile, die ohne ihr Ganzes keinen Sinn haben (wie fk_ap_auftrag: Positionen eines gelöschten Auftrags). Besser ist es, die Leistungsart nur zu deaktivieren (2 Punkte):

UPDATE leistungsart
SET aktiv = FALSE
WHERE leistungsart_id = 3;

Die Werkstattsoftware bietet dann nur noch aktive Leistungsarten an; alte Aufträge bleiben vollständig. Die Bedingung über die eindeutige Bezeichnung ist ebenfalls richtig.

Anlage: SQL-Kurzreferenz DML (MariaDB)

Auszug der in diesen Aufgaben nützlichen Sprachelemente. Eckige Klammern kennzeichnen optionale Teile.

SQL-Kurzreferenz DML für MariaDB
Syntax Bedeutung
INSERT INTO tabelle (spalte1, spalte2, …) VALUES (wert1, wert2, …) [, (…)] fügt einen oder mehrere Datensätze ein; nicht genannte Spalten erhalten DEFAULT bzw. NULL
INSERT INTO tabelle (spalten) SELECT … fügt das Ergebnis einer Abfrage ein (Spaltenzahl, Reihenfolge und Datentypen müssen passen)
LAST_INSERT_ID() zuletzt in dieser Verbindung vergebener AUTO_INCREMENT-Wert
UPDATE tabelle SET spalte1 = ausdruck1 [, spalte2 = ausdruck2 …] [WHERE bedingung] ändert die Zeilen, die die Bedingung erfüllen (ohne WHERE: alle)
UPDATE t1 [AS a] INNER JOIN t2 [AS b] ON bedingung SET a.spalte = … [WHERE …] ändert Zeilen mit Bezug auf eine verbundene Tabelle (MariaDB)
DELETE FROM tabelle [WHERE bedingung] löscht die Zeilen, die die Bedingung erfüllen (ohne WHERE: alle)
CREATE TABLE neu LIKE vorhanden legt eine leere Tabelle mit gleicher Struktur an (MariaDB)
ALTER TABLE tabelle ADD COLUMN spalte typ [, ADD COLUMN …] ergänzt Spalten
ALTER TABLE tabelle MODIFY spalte typ [NOT NULL] ändert Datentyp bzw. Pflichtfeld-Eigenschaft einer Spalte
ALTER TABLE tabelle DROP COLUMN spalte entfernt eine Spalte
START TRANSACTION … COMMIT bzw. ROLLBACK Anweisungen als Einheit ausführen bzw. zurücknehmen
wert [NOT] IN (SELECT …) / [NOT] EXISTS (SELECT …) Bedingung mit Unterabfrage
ausdruck BETWEEN a AND b Bereich einschließlich der Grenzen
ausdruck IS [NOT] NULL Test auf NULL
CASE WHEN bedingung THEN wert [WHEN …] [ELSE wert] END Fallunterscheidung; Kurzform CASE ausdruck WHEN wert THEN …
ROUND(zahl, stellen) runden
CONCAT(text1, text2, …) Texte (und Zahlen) aneinanderhängen
SUBSTRING_INDEX(text, trennzeichen, n) Teil von text vor dem n-ten Trennzeichen; bei negativem n Teil nach dem n-ten Trennzeichen von rechts
TRIM(text) entfernt Leerzeichen am Anfang und Ende
SUBSTRING(text, start [, laenge]), LOCATE(suchtext, text) Teil von text ab Position start; Position des ersten Vorkommens von suchtext (0, wenn nicht enthalten)
REPLACE(text, alt, neu) ersetzt jedes Vorkommen von alt in text durch neu
CURDATE(), NOW() heutiges Datum, aktueller Zeitpunkt
datum - INTERVAL n DAY (auch MONTH, YEAR) Datum n Tage (Monate, Jahre) früher
datum + INTERVAL n DAY, DATE_ADD(datum, INTERVAL n DAY) Datum n Tage später
`Spaltenname mit Leerzeichen` Bezeichner mit Leer- oder Sonderzeichen in Backticks
DATEDIFF(ende, start) Differenz in Tagen
TIMESTAMPDIFF(YEAR, start, ende) (auch MONTH, DAY) Differenz in vollen Einheiten (z. B. Alter in Jahren)
DATE_FORMAT(datum, format) Datum als Text, z. B. '%Y-%m' → '2026-10', '%d.%m.%Y' → '18.10.2026'
YEAR(datum), MONTH(datum), DAY(datum) Teil eines Datums
COUNT(*), SUM(…), AVG(…), MIN(…), MAX(…) Aggregatfunktionen

Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zu INSERT, UPDATE und DELETE” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/sql-dml-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/db-sql/sql. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 28.10.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: