Übungsaufgaben zum Anlegen und Ändern von Tabellen (SQL-DDL)
https://bildung.social/@oerinformatik/
https://oer-informatik.de/sql-ddl-uebungsaufgaben
SQL, DDL, CREATE TABLE, ALTER TABLE, Constraints, Fremdschlüssel, Index, MariaDB, Übungsaufgaben, Prüfungsvorbereitung
tl/dr; (Bearbeitungszeit insgesamt 140 min, mehrere Aufgaben): Vier Übungsaufgaben zur SQL-DDL: Tabellen mit Datentypen, Primär- und Fremdschlüsseln, NOT NULL, UNIQUE, DEFAULT und CHECK anlegen, bestehende Tabellen mit ALTER TABLE umbauen, Indizes setzen, Tabellen archivieren und löschen, fehlerhafte CREATE-TABLE-Anweisungen korrigieren sowie in einer Mitfahr-App einen Datentyp-Entwurf auswerten, eine Tabelle nachträglich per Fremdschlüssel einbinden und Zählwerte mit Constraints absichern und über eine View auswerten. 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-DDL: Grundlagen der DDL / Datentypen / Constraints / Übungsaufgaben zur DDL
Hinweise zur Bearbeitung: Als FIA, FISY, FIDV, ISE oder IKS musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1, Aufgabe 2.2 und Aufgabe 2.3. Von IKS wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1) keine Probleme bereiten. Die Teilaufgaben einer Aufgabe bauen aufeinander auf. Schreib Deine Lösung zuerst vollständig auf, bevor Du die Beispiellösung aufklappst. Die Beispiellösungen stehen in Tabs für MariaDB/MySQL (voreingestellt), MS SQL Server, PostgreSQL, Oracle (ab Version 23ai) und SQLite; Fehlernummern und Erläuterungen beziehen sich auf MariaDB. Andere korrekte Lösungen (z. B. Constraints inline statt als eigene Zeile, andere sinnvolle Längen bei VARCHAR, andere Constraint-Namen) sind ebenfalls richtig. Für die Aufgaben 2.2 und 2.3 legt ein Testdaten-Skript für MariaDB den Ausgangszustand an; die Ergebnisse in den Beispiellösungen beziehen sich auf diese Daten. „Heute“ ist in allen Aufgaben der 19.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 „Anlegen und Ändern von Tabellen
(SQL-DDL)“.
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-ddl-basics
- https://oer-informatik.de/sql-ddl-datentypen
- https://oer-informatik.de/sql-ddl-constraint
- https://oer-informatik.de/relationenmodell
- Stilbeispiel: https://oer-informatik.de/sql-ddl-uebungsaufgaben
Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- SQL
- DDL
- CREATE TABLE
- ALTER TABLE
- Constraints
- Fremdschlüssel
- Index
- MariaDB
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: Die Werkstattdatenbank eines Fahrradshops anlegen (Kernkompetenz)
(ca. 32 min Bearbeitungszeit)
Du bist Auszubildende*r im Entwicklungsteam der CSB-Rad GmbH in Bielefeld. Ab Januar 2027 nimmt die Werkstatt in Sennestadt Inspektionen und Reparaturen über das Shopsystem an. Das Datenmodell ist mit Werkstattleiter Bastian Ludwig abgestimmt (Primärschlüssel unterstrichen, Fremdschlüssel mit # gekennzeichnet):
mitarbeiter (mitarbeiter_id, vorname, nachname)
leistungsart (leistungsart_id, bezeichnung, stundensatz)
fahrradmodell (modell_id, hersteller, bezeichnung)
fahrrad (fahrrad_id, rahmennummer, farbe, #modell_id, #kunde_id)
werkstattauftrag (auftrag_id, auftragsnr, annahme_am, fertig_bis, km_stand, #fahrrad_id, #annahme_mitarbeiter_id)
arbeitsposition (#auftrag_id, pos_nr, beschreibung, #leistungsart_id, #mitarbeiter_id, stunden, stundensatz)
materialposition (#auftrag_id, #artikel_id, menge, einzelpreis)
Die Tabellen kunde (kunde_id INT UNSIGNED) und artikel (artikel_id INT UNSIGNED) gibt es bereits in der Shop-Datenbank. Alle Primärschlüssel sollen – soweit nicht anders angegeben – vom System fortlaufend vergeben werden. Ein Beispielauftrag aus der Werkstatt:
WERKSTATTAUFTRAG W-2026-0147 Annahme: 14.10.2026, 08:40 Uhr (Bastian Ludwig)
Fertig bis: 16.10.2026 Kilometerstand: 1.412 km
Fahrrad: E-Bike „Kanal“ Mittelmotor 500 Wh, moosgrün, Rahmennummer CSB2608EB0213
Pos Leistung Leistungsart ausgeführt von Std. €/Std.
1 Erstinspektion E-Bike Inspektion Selin Arslan 1,50 68,00
2 Bremsbeläge und Kette erneuert Reparatur Selin Arslan 0,50 68,00
3 Software-Update Motorsteuerung Elektronik Paul Neubauer 0,25 78,00
a) Erstelle die SQL-Anweisung, mit der die Tabelle leistungsart angelegt wird. Jede Bezeichnung (höchstens 30 Zeichen) muss angegeben werden und darf nur einmal vorkommen. Der Stundensatz muss angegeben werden, ist centgenau, größer als 0 € und kleiner als 1.000 €. (5 Punkte)
CREATE TABLE leistungsart (
leistungsart_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
bezeichnung VARCHAR(30) NOT NULL UNIQUE,
stundensatz DECIMAL(5,2) NOT NULL,
CONSTRAINT chk_la_satz CHECK (stundensatz > 0)
);Bewertung: Primärschlüssel mit AUTO_INCREMENT 1 Punkt, bezeichnung mit VARCHAR(30) und NOT NULL 1 Punkt, UNIQUE 1 Punkt, DECIMAL(5,2) mit NOT NULL 1 Punkt, CHECK 1 Punkt.
Worauf es ankommt:
DECIMAL(5,2)speichert fünf Stellen, davon zwei Nachkommastellen – also höchstens 999,99 €. Damit ist die Obergrenze schon durch den Datentyp gesichert;CHECK (stundensatz > 0 AND stundensatz < 1000)ist ebenfalls richtig.Ein Versuch, „Inspektion“ ein zweites Mal einzutragen, scheitert an
UNIQUE(MariaDB-Fehler 1062 „Duplicate entry“), ein Stundensatz von 0 an derCHECK-Bedingung (Fehler 4025 „CONSTRAINTchk_la_satzfailed“).CHECK-Bedingungen werden von MariaDB seit Version 10.2 geprüft; ältere MySQL-Versionen (vor 8.0.16) haben sie zwar akzeptiert, aber ignoriert.
b) Dein Mit-Azubi hat für einige Spalten folgende Datentypen vorgeschlagen. Nimm zu jedem Vorschlag Stellung und gib, wenn nötig, einen besseren Datentyp an. (4 Punkte)
| Spalte | Vorschlag |
|---|---|
arbeitsposition.stundensatz |
FLOAT |
werkstattauftrag.auftragsnr (z. B. W-2026-0147) |
INT |
werkstattauftrag.km_stand |
SMALLINT UNSIGNED |
arbeitsposition.pos_nr |
TINYINT UNSIGNED |
| Spalte | Vorschlag | Stellungnahme |
|---|---|---|
stundensatz |
FLOAT |
ungeeignet: Gleitkommazahlen speichern Dezimalbrüche wie 0,10 nur näherungsweise, bei Summen entstehen Rundungsfehler. Geldbeträge als Festkommazahl DECIMAL(5,2) speichern. |
auftragsnr |
INT |
ungeeignet: Die Nummer enthält Buchstaben und Bindestriche, die führende Null in 0147 ginge verloren, gerechnet wird damit nicht. Besser CHAR(11) (feste Länge) oder VARCHAR(15). |
km_stand |
SMALLINT UNSIGNED |
zu klein: Der Wertebereich endet bei 65.535 km; E-Bikes im Pendel- oder Kurierbetrieb erreichen das. Besser MEDIUMINT UNSIGNED (bis 16.777.215) oder INT UNSIGNED. |
pos_nr |
TINYINT UNSIGNED |
geeignet: 0 bis 255 Positionen je Auftrag reichen; der Typ belegt nur 1 Byte. |
Je Zeile 1 Punkt (Urteil mit Begründung).
c) Formuliere die SQL-Anweisung für die Tabelle fahrrad. Die Rahmennummer (bis zu 20 Zeichen) ist bei Rädern anderer Hersteller manchmal unleserlich und darf dann fehlen; wenn sie angegeben ist, darf sie aber nur bei einem Fahrrad vorkommen. Modell und Kund*in müssen immer angegeben werden. Vergib für alle Constraints sprechende Namen. (5 Punkte)
CREATE TABLE fahrrad (
fahrrad_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
rahmennummer VARCHAR(20) NULL,
farbe VARCHAR(30) NULL,
modell_id INT UNSIGNED NOT NULL,
kunde_id INT UNSIGNED NOT NULL,
CONSTRAINT pk_fahrrad PRIMARY KEY (fahrrad_id),
CONSTRAINT uq_fahrrad_rahmennr UNIQUE (rahmennummer),
CONSTRAINT fk_fahrrad_modell FOREIGN KEY (modell_id) REFERENCES fahrradmodell (modell_id),
CONSTRAINT fk_fahrrad_kunde FOREIGN KEY (kunde_id) REFERENCES kunde (kunde_id)
);Bewertung: Spalten mit passenden Datentypen 1 Punkt, rahmennummer mit NULL und benanntem UNIQUE 1 Punkt, NOT NULL bei beiden Fremdschlüsseln 1 Punkt, je benanntem Fremdschlüssel 1 Punkt.
Worauf es ankommt:
UNIQUEerlaubt in MariaDB beliebig vieleNULL-Werte – zwei Räder ohne lesbare Rahmennummer sind also kein Widerspruch. Das ist der Unterschied zum Primärschlüssel, der nieNULLsein darf.Die Fremdschlüsselspalten müssen denselben Datentyp wie die referenzierten Primärschlüssel haben, einschließlich
UNSIGNED. Sonst lehnt MariaDB die Tabelle ab (Fehler 1005, „errno: 150 Foreign key constraint is incorrectly formed“).MariaDB benennt Primärschlüssel immer
PRIMARY; der Namepk_fahrradwird akzeptiert, aber ignoriert. Benannte Constraints sind vor allem fürUNIQUE,CHECKundFOREIGN KEYnützlich: Fehlermeldungen nennen den Namen, und man kann die Bedingung später gezielt löschen.
d) Erstelle die SQL-Anweisung für die Tabelle werkstattauftrag. Die Auftragsnummer ist eindeutig und Pflicht. Wird beim Anlegen kein Annahmezeitpunkt angegeben, soll der aktuelle Zeitpunkt gespeichert werden. Der Termin „fertig bis“ ist anfangs manchmal noch offen, darf aber nicht vor dem Tag der Annahme liegen. Wer den Auftrag angenommen hat, muss gespeichert werden. (6 Punkte)
CREATE TABLE werkstattauftrag (
auftrag_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
auftragsnr CHAR(11) NOT NULL UNIQUE,
annahme_am DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
fertig_bis DATE NULL,
km_stand INT UNSIGNED NULL,
fahrrad_id INT UNSIGNED NOT NULL,
annahme_mitarbeiter_id INT UNSIGNED NOT NULL,
CONSTRAINT chk_wa_fertig CHECK (fertig_bis >= DATE(annahme_am)),
CONSTRAINT fk_wa_fahrrad FOREIGN KEY (fahrrad_id) REFERENCES fahrrad (fahrrad_id),
CONSTRAINT fk_wa_mitarbeiter FOREIGN KEY (annahme_mitarbeiter_id) REFERENCES mitarbeiter (mitarbeiter_id)
);Bewertung: auftragsnr mit NOT NULL UNIQUE 1 Punkt, DEFAULT CURRENT_TIMESTAMP 1 Punkt, fertig_bis als DATE NULL 1 Punkt, CHECK 1 Punkt, je Fremdschlüssel 1 Punkt.
Worauf es ankommt:
Ist
fertig_bisNULL, ist die Bedingung „unbekannt“ – das gilt beiCHECKals erfüllt. Der offene Termin ist also erlaubt, ein Termin vor dem Annahmetag (z. B. Annahme 14.10., fertig bis 13.10.) wird abgelehnt.Der Fremdschlüssel auf die Mitarbeitenden heißt
annahme_mitarbeiter_id, weilmitarbeiterauch inarbeitsposition(Ausführung) vorkommt. Der Name der Fremdschlüsselspalte muss nicht mit dem des Primärschlüssels übereinstimmen – entscheidend istREFERENCES mitarbeiter (mitarbeiter_id).Der Datentyp
DATETIME(Datum und Uhrzeit) passt zur Annahme um 08:40 Uhr,DATEreicht für den Fertigstellungstermin. Datumswerte werden in MariaDB im Format'2026-10-16'geschrieben, nicht'16.10.2026'.
e) Für die Tabelle arbeitsposition gilt: Die Positionsnummer beginnt in jedem Auftrag wieder bei 1. Der Stundensatz wird aus der Leistungsart übernommen und muss gespeichert werden, weil er sich später ändern kann. Es werden mindestens Viertelstunden abgerechnet, die Stundenzahl muss also größer als 0 sein.
ea) Erstelle die SQL-Anweisung für die Tabelle. Wird ein Werkstattauftrag gelöscht, sollen seine Arbeitspositionen automatisch mitgelöscht werden; Leistungsarten und Mitarbeitende, die in Positionen vorkommen, dürfen dagegen nicht gelöscht werden können. (6 Punkte)
eb) Begründe, warum für die Fremdschlüssel unterschiedliche Regeln beim Löschen sinnvoll sind. (2 Punkte)
ea)
CREATE TABLE arbeitsposition (
auftrag_id INT UNSIGNED NOT NULL,
pos_nr TINYINT UNSIGNED NOT NULL,
beschreibung VARCHAR(200) NOT NULL,
leistungsart_id INT UNSIGNED NOT NULL,
mitarbeiter_id INT UNSIGNED NOT NULL,
stunden DECIMAL(4,2) NOT NULL,
stundensatz DECIMAL(5,2) NOT NULL,
PRIMARY KEY (auftrag_id, pos_nr),
CONSTRAINT chk_ap_stunden CHECK (stunden > 0),
CONSTRAINT fk_ap_auftrag FOREIGN KEY (auftrag_id) REFERENCES werkstattauftrag (auftrag_id) ON DELETE CASCADE,
CONSTRAINT fk_ap_leistungsart FOREIGN KEY (leistungsart_id) REFERENCES leistungsart (leistungsart_id),
CONSTRAINT fk_ap_mitarbeiter FOREIGN KEY (mitarbeiter_id) REFERENCES mitarbeiter (mitarbeiter_id)
);Bewertung: zusammengesetzter Primärschlüssel 2 Punkte, ON DELETE CASCADE beim Auftrag 1 Punkt, Fremdschlüssel auf Leistungsart und Mitarbeitende 1 Punkt, stunden und stundensatz als DECIMAL mit NOT NULL 1 Punkt, CHECK 1 Punkt.
Worauf es ankommt:
Der Primärschlüssel
(auftrag_id, pos_nr)ist zusammengesetzt: Position 1 gibt es in jedem Auftrag. Ein Primärschlüssel nur auspos_nrließe genau einen Auftrag mit Position 1 zu, einer nur ausauftrag_idnur eine Position je Auftrag.Ohne
ON DELETE-Angabe giltRESTRICT(in MariaDB gleichbedeutend mitNO ACTION): Eine Leistungsart, auf die noch eine Position verweist, kann nicht gelöscht werden (Fehler 1451 „Cannot delete or update a parent row“).ON DELETE RESTRICTausdrücklich hinzuschreiben ist ebenfalls richtig.Weil der Primärschlüssel mit
auftrag_idbeginnt, nutzt MariaDB ihn auch als Index für den Fremdschlüssel auf den Auftrag. Für die beiden anderen Fremdschlüssel legt InnoDB automatisch einen Index an.
eb) (je Argument 1 Punkt) Eine Arbeitsposition ist ohne ihren Auftrag sinnlos (schwacher Entitätstyp, Komposition): Wird ein – z. B. versehentlich angelegter – Auftrag gelöscht, sollen keine verwaisten Positionen zurückbleiben, deshalb CASCADE. Leistungsarten und Mitarbeitende sind dagegen eigenständige Stammdaten. Würden sie samt Positionen gelöscht, verschwänden abgerechnete Arbeiten aus alten Aufträgen und Rechnungen wären nicht mehr nachvollziehbar; deshalb RESTRICT. Nicht mehr angebotene Leistungsarten werden besser deaktiviert (siehe g).
f) Für ein Testsystem soll ein Skript alle sieben Werkstatt-Tabellen anlegen und am Ende des Tests wieder löschen. Gib eine Reihenfolge an, in der die Tabellen angelegt werden können, erstelle die DROP-Anweisungen für das Aufräumen in einer funktionierenden Reihenfolge und begründe die Reihenfolge. (4 Punkte)
Anlegen (1 Punkt), z. B.: mitarbeiter, leistungsart, fahrradmodell, fahrrad, werkstattauftrag, arbeitsposition, materialposition. Die ersten drei Tabellen haben keine Fremdschlüssel und können in beliebiger Reihenfolge stehen; fahrrad braucht fahrradmodell (und kunde), werkstattauftrag braucht fahrrad und mitarbeiter, die Positionen brauchen den Auftrag.
Löschen (2 Punkte), genau umgekehrt – erst die Tabellen, die auf andere verweisen:
DROP TABLE IF EXISTS materialposition;
DROP TABLE IF EXISTS arbeitsposition;
DROP TABLE IF EXISTS werkstattauftrag;
DROP TABLE IF EXISTS fahrrad;
DROP TABLE IF EXISTS fahrradmodell;
DROP TABLE IF EXISTS leistungsart;
DROP TABLE IF EXISTS mitarbeiter;Begründung (1 Punkt): Ein Fremdschlüssel kann nur auf eine Tabelle verweisen, die schon existiert. Umgekehrt verweigert MariaDB das Löschen einer Tabelle, auf die noch ein Fremdschlüssel verweist (Fehler 1451) – auch dann, wenn sie leer ist. IF EXISTS verhindert, dass das Skript abbricht, wenn eine Tabelle nicht angelegt wurde.
Alternative für das Testsystem: vorher SET FOREIGN_KEY_CHECKS = 0; setzen, danach wieder = 1. Im Produktivsystem ist das Abschalten der Prüfung tabu.
g) Bastian möchte Leistungsarten, die nicht mehr angeboten werden, deaktivieren statt löschen. Ergänze die Tabelle leistungsart um eine passende Pflichtspalte aktiv und erläutere, welchen Wert die bereits gespeicherten Leistungsarten erhalten. (3 Punkte)
ALTER TABLE leistungsart
ADD COLUMN aktiv BOOLEAN NOT NULL DEFAULT TRUE;Bewertung: ALTER TABLE … ADD COLUMN 1 Punkt, Datentyp BOOLEAN mit NOT NULL 1 Punkt, Erläuterung 1 Punkt.
Erläuterung: Durch DEFAULT TRUE erhalten alle vorhandenen Datensätze beim Hinzufügen der Spalte den Wert TRUE (in MariaDB intern 1, denn BOOLEAN ist ein Alias für TINYINT(1)); Inspektion, Reparatur und Elektronik bleiben also aktiv. Ohne DEFAULT würde MariaDB bei NOT NULL den „impliziten Standardwert“ des Typs eintragen, hier 0 – alle Leistungsarten wären dann versehentlich deaktiviert.
Aufgabe 2.2: Eine Freizeit-App beschleunigen und umbauen (Kernkompetenz)
(ca. 40 min Bearbeitungszeit)
Du bist Auszubildende*r im Entwicklungsteam der Let’s Meet GmbH in Bielefeld. Die App vermittelt gemeinsame Freizeitaktivitäten; die MariaDB-Datenbank enthält inzwischen 41.800 Konten, jeden Monat kommen rund 3.100 Aktivitäten hinzu. Entwicklungsleiter Yusuf Demir hat beobachtet, dass die Startseite („Was ist los in den nächsten sieben Tagen?“) und die Suche nach barrierefreien Treffpunkten immer langsamer werden. Außerdem stehen einige Umbauten an. Die Tabellen mit Testbestand legt Teil 1 des Testdaten-Skripts an. Auszug aus den Tabellen:
CREATE TABLE benutzer (
benutzer_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
vorname VARCHAR(50) NOT NULL,
nachname VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
plz CHAR(5) NOT NULL,
ort VARCHAR(60) NOT NULL,
verifiziert BOOLEAN NOT NULL DEFAULT FALSE,
registriert_am DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
-- weitere Spalten …
);
CREATE TABLE treffpunkt (
treffpunkt_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
plz CHAR(5) NOT NULL,
barrierefrei BOOLEAN NOT NULL DEFAULT FALSE
-- weitere Spalten …
);
CREATE TABLE aktivitaet (
aktivitaet_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
titel VARCHAR(100) NOT NULL,
treffpunkt_id INT UNSIGNED NOT NULL,
beginn DATETIME NOT NULL,
CONSTRAINT fk_akt_treffpunkt FOREIGN KEY (treffpunkt_id) REFERENCES treffpunkt (treffpunkt_id)
-- weitere Spalten …
);
CREATE TABLE telefon (
telefon_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
benutzer_id INT UNSIGNED NOT NULL,
nummer VARCHAR(25) NOT NULL,
typ ENUM('mobil','festnetz') NOT NULL DEFAULT 'mobil',
CONSTRAINT fk_telefon_benutzer FOREIGN KEY (benutzer_id) REFERENCES benutzer (benutzer_id) ON DELETE CASCADE
);Die Startseite fragt die Aktivitäten so ab:
SELECT aktivitaet_id, titel, beginn
FROM aktivitaet
WHERE beginn >= CURDATE() AND beginn < CURDATE() + INTERVAL 7 DAY
ORDER BY beginn;Yusuf hat sich mit EXPLAIN vor der Abfrage anzeigen lassen, wie MariaDB sie ausführt (Ausgabe gekürzt):
| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | aktivitaet | ALL | NULL | NULL | 98412 | Using where; Using filesort |
a) Erläutere anhand der Spalten type, key, rows und Extra, wie MariaDB die Abfrage zurzeit ausführt und warum die Startseite mit jedem Monat langsamer wird. (3 Punkte)
type=ALLundkey=NULL: MariaDB nutzt keinen Index (possible_keysist ebenfalls leer, es gibt also keinen passenden). Die Tabelle wird vollständig Zeile für Zeile gelesen (full table scan). (1 Punkt)rows= 98412: Für die wenigen Aktivitäten der nächsten sieben Tage prüft MariaDB schätzungsweise alle rund 98.000 Zeilen. Weil jeden Monat rund 3.100 Aktivitäten hinzukommen, wächst dieser Aufwand ständig mit – die Antwortzeit steigt mit der Tabellengröße, obwohl das Ergebnis etwa gleich groß bleibt. (1 Punkt)Extra:Using where– die Bedingung wird erst nach dem Lesen jeder Zeile geprüft;Using filesort– die gefundenen Zeilen werden anschließend in einem eigenen Schritt nachbeginnsortiert. Ein Index auf der Spalte könnte beides ersetzen, weil er die Werte schon sortiert vorhält und MariaDB direkt zum ersten passenden Zeitpunkt springen kann. (1 Punkt)
b) Leite aus der Abfrage und dem Ausführungsplan einen passenden Index ab und lege ihn an. Im Team heißen Indizes nach dem Muster idx_<tabelle>_<spalte>. Beschreibe, woran Du in einer erneuten EXPLAIN-Ausgabe erkennst, dass der Index wirkt. Im Team kommt außerdem der Vorschlag auf, „sicherheitshalber“ jede Spalte von aktivitaet einzeln zu indizieren – nenne zwei Gründe, die dagegen sprechen. (5 Punkte)
CREATE INDEX idx_aktivitaet_beginn ON aktivitaet (beginn);Gleichwertig: ALTER TABLE aktivitaet ADD INDEX idx_aktivitaet_beginn (beginn);
Bewertung Index (2 Punkte): Spalte beginn gewählt, weil sie in WHERE (Bereichsbedingung) und in ORDER BY steht, 1 Punkt; CREATE INDEX mit Name nach Muster und ON aktivitaet (beginn) 1 Punkt.
Erneute EXPLAIN-Ausgabe (1 Punkt): key zeigt idx_aktivitaet_beginn, type wechselt von ALL auf range, der Schätzwert in rows sinkt auf die Größenordnung der Treffer (einige Hundert statt rund 98.000), und Using filesort verschwindet, weil die Zeilen über den Index schon sortiert gelesen werden. Im Testbestand liefert die Abfrage für den 19.10.2026 die Aktivitäten 3 („Feierabend-Kick“), 4 („Sprachtandem Deutsch–Arabisch“) und 5 („Bouldern für Einsteiger*innen“); bei nur acht Zeilen liest MariaDB allerdings trotz Index oft einfach die ganze Tabelle.
Gründe gegen „alles indizieren“ (je 1 Punkt, zwei verlangt):
Jeder Index belegt zusätzlichen Speicher, bei großen Tabellen leicht so viel wie die Daten selbst.
Bei jedem
INSERT,UPDATEundDELETEmüssen alle betroffenen Indizes mit angepasst werden – das Eintragen der monatlich 3.100 neuen Aktivitäten wird langsamer.Viele Spalten werden nie zum Filtern, Verknüpfen oder Sortieren verwendet (z. B.
titel); deren Index nutzt keine Abfrage. Bei Spalten mit wenigen verschiedenen Werten liest das DBMS ohnehin lieber die ganze Tabelle.
c) Die Suche nach Treffpunkten filtert fast immer nach der Postleitzahl, oft zusätzlich nach barrierefrei = TRUE.
ca) Erstelle einen Index, der beide Suchen unterstützt, und begründe die Reihenfolge der Spalten. (2 Punkte)
cb) Dein Mit-Azubi Jonas Weber schlägt vor, auch die Spalten benutzer.email und aktivitaet.treffpunkt_id zu indizieren. Nimm Stellung. (2 Punkte)
ca)
CREATE INDEX idx_treffpunkt_plz_barrierefrei ON treffpunkt (plz, barrierefrei);(1 Punkt) Begründung (1 Punkt): Ein zusammengesetzter Index ist zuerst nach der ersten Spalte sortiert, innerhalb gleicher Werte nach der zweiten – wie ein Telefonbuch nach Nachname, dann Vorname. Er hilft deshalb bei Bedingungen auf die erste Spalte allein und auf beide Spalten (leftmost prefix), nicht aber bei einer Suche nur nach der zweiten Spalte. Da plz immer gefiltert wird, gehört sie nach vorn; ein Index auf barrierefrei allein wäre wegen der nur zwei Werte kaum nützlich.
cb) (je Spalte 1 Punkt) Beide Indizes sind überflüssig: Für email gibt es durch UNIQUE bereits einen eindeutigen Index. Für die Fremdschlüsselspalte treffpunkt_id legt InnoDB beim Anlegen des Fremdschlüssels automatisch einen Index an, falls noch keiner existiert. Ein zweiter Index auf dieselbe Spalte kostet nur Speicher und Schreibzeit. Welche Indizes vorhanden sind, zeigt SHOW INDEX FROM benutzer;.
d) In der Tabelle telefon soll dieselbe Nummer bei einem Konto nicht doppelt gespeichert werden können. Jonas möchte dazu die Spalte nummer als UNIQUE kennzeichnen. Erstelle eine bessere SQL-Anweisung und begründe Deine Lösung. Erläutere außerdem, was geschieht, wenn beim Anlegen bereits doppelte Einträge vorhanden sind. (3 Punkte)
ALTER TABLE telefon
ADD CONSTRAINT uq_telefon_konto_nummer UNIQUE (benutzer_id, nummer);Gleichwertig: CREATE UNIQUE INDEX uq_telefon_konto_nummer ON telefon (benutzer_id, nummer); (1 Punkt)
Begründung (1 Punkt): Ein UNIQUE nur auf nummer würde verbieten, dass zwei Konten dieselbe Nummer angeben – etwa Mitglieder eines Haushalts mit gemeinsamem Festnetzanschluss. Gefordert ist die Eindeutigkeit je Konto, also über die Kombination beider Spalten.
Duplikate (1 Punkt): MariaDB prüft beim Anlegen alle vorhandenen Zeilen. Gibt es schon ein doppeltes Paar, bricht die Anweisung mit Fehler 1062 („Duplicate entry … for key …“) ab und der Index wird nicht angelegt. Die Duplikate müssen vorher gesucht (GROUP BY benutzer_id, nummer HAVING COUNT(*) > 1) und bereinigt werden. Im aktuellen Bestand gibt es keine, die Anweisung läuft also durch.
e) Let’s Meet bucht für einige Aktivitäten Kursräume in einem Stadtteilzentrum. Dessen Raumliste liegt in der Tabelle raum_import; die Spalte raum VARCHAR(20) enthält Gebäudebuchstabe und Raumnummer, getrennt durch einen Bindestrich:
| zeile | raum | plaetze |
|---|---|---|
| 1 | B-204 | 24 |
| 2 | A-012 | 16 |
| 3 | A-105 | 30 |
| 4 | C-001 | 12 |
| 5 | Saal | 120 |
| 6 | B-110 | 20 |
Jonas sollte raum in die Spalten gebaeude und raumnummer aufteilen. Er hat folgendes Skript geschrieben und in seinem SQL-Client am Stück ausführen lassen; der Client meldete beim UPDATE einen Fehler und arbeitete danach einfach weiter:
ALTER TABLE raum_import
ADD COLUMN gebaeude CHAR(1) NOT NULL,
ADD COLUMN raumnummer SMALLINT NOT NULL;
UPDATE raum_import
SET gebaeude = SUBSTRING_INDEX(raum, '-', -1),
raumnummer = SUBSTRING_INDEX(raum, '-', 1);
ALTER TABLE raum_import DROP COLUMN raum;Zum Glück gibt es eine Sicherung: Teil 1 des Testdaten-Skripts stellt den Ausgangszustand wieder her. Nutze die SQL-Kurzreferenz in der Anlage.
ea) Beschreibe, was die drei Anweisungen nacheinander bewirken und in welchem Zustand die Tabelle raum_import nach dem Skript ist. (3 Punkte)
eb) Selbst wenn das UPDATE fehlerfrei durchliefe, wäre das Ergebnis nicht brauchbar. Benenne drei weitere Schwachstellen des Skripts. (3 Punkte)
ec) Erstelle ein verbessertes Skript, das die Aufteilung zuverlässig erledigt und vor dem Entfernen der alten Spalte prüft, ob alle Zeilen aufgeteilt werden konnten. (4 Punkte)
ea) (je Anweisung 1 Punkt)
ALTER TABLE … ADDläuft durch. Weil keinDEFAULTangegeben ist, füllt MariaDB die neuen Pflichtspalten in den sechs vorhandenen Zeilen mit dem impliziten Standardwert:gebaeude=''(Leerstring),raumnummer=0.Das
UPDATEscheitert schon in Zeile 1: Die Argumente vonSUBSTRING_INDEXsind vertauscht,gebaeudesoll'204'aufnehmen, passt aber nur ein Zeichen. Im strikten Modus bricht MariaDB mit Fehler 1406 („Data too long for column ‘gebaeude’ at row 1“) ab; die Anweisung wird als Ganzes zurückgenommen, keine Zeile ist geändert.Das
DROP COLUMNwird trotzdem ausgeführt und lässt sich als DDL-Anweisung nicht mitROLLBACKzurücknehmen. Danach enthältraum_importnur nochzeile,plaetze,gebaeude=''undraumnummer=0– die Raumbezeichnungen sind verloren.
eb) (je Schwachstelle 1 Punkt, drei verlangt)
raumnummeralsSMALLINT: Führende Nullen gehen verloren (A-012→12,C-001→1), obwohl sie zur Bezeichnung gehören (z. B. Erdgeschoss). Mit der Nummer wird nicht gerechnet – passend ist ein Text wieVARCHAR(5).Der Sonderfall
Saalohne Bindestrich wird nicht behandelt:SUBSTRING_INDEXliefert dann beide Male den ganzen Text, und beide Spalten bekämen Unsinn.NOT NULLohne Standardwert gleich beim Hinzufügen: Fehlende Werte werden durch''und0verdeckt, statt alsNULLauffindbar zu bleiben; für den Saal gäbe es zudem keinen erlaubten „unbekannt“-Wert.Vor dem
DROP COLUMNwird nichts geprüft und keine Sicherung angelegt; ein Fehler in den Schritten davor führt direkt zu Datenverlust.
ec)
ALTER TABLE raum_import
ADD COLUMN gebaeude CHAR(1) NULL,
ADD COLUMN raumnummer VARCHAR(5) NULL;
UPDATE raum_import
SET gebaeude = CASE WHEN raum LIKE '_-%' THEN LEFT(raum, 1) END,
raumnummer = CASE WHEN raum LIKE '_-%' THEN SUBSTRING(raum, 3) END;
-- vor dem Löschen: Zeilen suchen, die nicht aufgeteilt werden konnten
SELECT zeile, raum, gebaeude, raumnummer
FROM raum_import
WHERE gebaeude IS NULL OR raumnummer IS NULL;
-- erst wenn alle Sonderfälle geklärt sind (und eine Sicherung existiert):
ALTER TABLE raum_import DROP COLUMN raum;Ergebnis vor dem DROP COLUMN:
| zeile | raum | gebaeude | raumnummer |
|---|---|---|---|
| 1 | B-204 | B | 204 |
| 2 | A-012 | A | 012 |
| 4 | C-001 | C | 001 |
| 5 | Saal | NULL | NULL |
Die Prüfabfrage liefert nur Zeile 5. Für den Saal klärst Du mit dem Stadtteilzentrum, in welchem Gebäude er liegt, und trägst ihn per UPDATE nach (oder lässt ihn bewusst ohne Raumnummer) – erst danach wird raum entfernt.
Bewertung: neue Spalten mit Textdatentyp und zunächst ohne NOT NULL 1 Punkt, Gebäude und Raumnummer richtig herausgelöst 1 Punkt, Sonderfall ohne Bindestrich abgefangen 1 Punkt, Prüfabfrage vor dem DROP COLUMN 1 Punkt. Statt LEFT/SUBSTRING ist auch SUBSTRING_INDEX(raum, '-', 1) bzw. SUBSTRING_INDEX(raum, '-', -1) richtig, wenn der Sonderfall abgefangen wird.
f) Die App soll Teilnehmende künftig mit einem Anzeigenamen wie „Sabine H.“ begrüßen: Vorname, Leerzeichen, erster Buchstabe des Nachnamens, Punkt. Im Team gibt es drei Vorschläge für die neue Spalte anzeigename in benutzer:
Vorschlag A (Jonas): Spalte gleich als
VARCHAR(55) NOT NULL DEFAULT ''hinzufügen und dann mit einemUPDATEfür alle Konten füllen. Bei neuen Konten setzt die App den Anzeigenamen.Vorschlag B (Yusuf): Spalte zunächst ohne
NOT NULLhinzufügen, mit einemUPDATEfüllen und danach perMODIFYzum Pflichtfeld machen. Neue Konten wie bei A.Vorschlag C: eine generierte Spalte, die MariaDB aus
vornameundnachnameberechnet.
fa) Vergleiche die drei Vorschläge: Was steht nach dem Umbau in den 13 vorhandenen Konten, was geschieht, wenn die App bei einem neuen Konto den Anzeigenamen vergisst, und was passiert, wenn jemand den Nachnamen ändert? Empfiehl einen Vorschlag und gib die SQL-Anweisung(en) dafür an. (6 Punkte)
fb) Yusuf überlegt, anzeigename zusätzlich als UNIQUE zu kennzeichnen, „damit es keine Verwechslungen gibt“. Im aktuellen Bestand gäbe es keinen Konflikt. Nimm Stellung. (2 Punkte)
fa) Vergleich (je Vorschlag 1 Punkt):
A: NOT NULL DEFAULT '' |
B: erst füllen, dann MODIFY |
C: generierte Spalte | |
|---|---|---|---|
| vorhandene Konten | nach dem UPDATE „Sabine H.“ … „Deniz Ç.“ |
wie A | sofort „Sabine H.“ … „Deniz Ç.“, ohne UPDATE |
| App vergisst den Wert | '' wird gespeichert – formal Pflicht, inhaltlich leer |
Fehler 1364 („Field ‘anzeigename’ doesn’t have a default value“), das Konto wird nicht angelegt | kann nicht passieren, die Datenbank berechnet den Wert |
| Nachname ändert sich | alter Anzeigename bleibt stehen (redundant, veraltet) | wie A | Anzeigename passt sich automatisch an |
Empfehlung (1 Punkt): Vorschlag C, weil der Anzeigename vollständig aus anderen Spalten derselben Zeile folgt und nie veralten kann. Soll der Anzeigename später frei wählbar sein, ist B die richtige Wahl: Dann ist er kein abgeleiteter Wert mehr, und die Pflicht wird ohne leeren Standardwert durchgesetzt.
SQL für C (2 Punkte):
ALTER TABLE benutzer
ADD COLUMN anzeigename VARCHAR(55)
AS (CONCAT(vorname, ' ', LEFT(nachname, 1), '.')) VIRTUAL;Wer B empfiehlt, gibt diese drei Anweisungen an (ebenfalls 2 Punkte):
ALTER TABLE benutzer ADD COLUMN anzeigename VARCHAR(55) NULL;
UPDATE benutzer SET anzeigename = CONCAT(vorname, ' ', LEFT(nachname, 1), '.');
ALTER TABLE benutzer MODIFY COLUMN anzeigename VARCHAR(55) NOT NULL;Worauf es ankommt:
Bei B ist die Reihenfolge entscheidend: Solange Zeilen ohne Wert existieren, bricht
MODIFY … NOT NULLim strikten Modus mit Fehler 1138 („Invalid use of NULL value“) ab. BeiMODIFYwird die komplette Spaltendefinition angegeben.In eine generierte Spalte kann niemand selbst schreiben; ein
INSERToderUPDATE, dasanzeigenamesetzen will, wird abgewiesen.VIRTUALbelegt keinen Speicher,STOREDlohnt sich, wenn oft nach dem Anzeigenamen gesucht wird.VARCHAR(55), weilvornamebis zu 50 Zeichen lang sein darf und noch Leerzeichen, Initiale und Punkt dazukommen.
fb) (2 Punkte) Davon ist abzuraten. Der Anzeigename ist aus Vorname und Initiale gebildet und naturgemäß nicht eindeutig: Bei 41.800 Konten gibt es viele „Sabine H.“ oder „Malik H.“. Mit UNIQUE würde schon die Registrierung des zweiten Kontos dieses Namens mit Fehler 1062 abbrechen. Verwechslungen vermeidet man über den Primärschlüssel und z. B. ein Profilbild, nicht über den Namen. (Wer für frei wählbare Nutzernamen argumentiert, die die Person selbst festlegt, darf UNIQUE dafür befürworten.)
g) Datenschutzbeauftragte Dr. Marta Kowalczyk verlangt, dass jede Verifizierung mit Verfahren, Zeitpunkt und Ergebnis dokumentiert wird. Bisher gibt es nur die Spalte benutzer.verifiziert. Für ein Konto können mehrere Versuche gespeichert werden; wird ein Konto gelöscht, sollen auch seine Verifizierungen gelöscht werden.
ga) Lege mit einer SQL-Anweisung die Tabelle verifizierung an. Als Verfahren sind „Ausweis-App“, „Video-Ident“ und „Altbestand“ möglich, als Ergebnis „bestanden“ und „abgelehnt“. (4 Punkte)
gb) Schreibe die Anweisungen, mit denen Du die bisher verifizierten Konten als „Altbestand“ mit dem Registrierungszeitpunkt als Prüfzeitpunkt in die neue Tabelle überträgst und danach die Spalte verifiziert entfernst. (4 Punkte)
gc) Jonas möchte die Anweisungen aus gb) in START TRANSACTION; … COMMIT; einschließen, „dann kann ich bei einem Fehler einfach ROLLBACK machen“. Nimm Stellung. (3 Punkte)
ga)
CREATE TABLE verifizierung (
verifizierung_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
benutzer_id INT UNSIGNED NOT NULL,
verfahren ENUM('Ausweis-App','Video-Ident','Altbestand') NOT NULL,
geprueft_am DATETIME NOT NULL,
ergebnis ENUM('bestanden','abgelehnt') NOT NULL,
CONSTRAINT fk_ver_benutzer FOREIGN KEY (benutzer_id)
REFERENCES benutzer (benutzer_id) ON DELETE CASCADE
);Bewertung: Primärschlüssel 1 Punkt, Fremdschlüssel mit ON DELETE CASCADE 1 Punkt, verfahren und ergebnis mit festen Werten 1 Punkt, geprueft_am DATETIME und NOT NULL 1 Punkt.
Statt ENUM sind auch VARCHAR mit CHECK (verfahren IN (…)) oder eigene Wertetabellen mit Fremdschlüssel richtig. Weil mehrere Versuche je Konto möglich sind, darf benutzer_id allein nicht Primärschlüssel sein; (benutzer_id, geprueft_am) als zusammengesetzter Schlüssel ist eine Alternative.
gb)
INSERT INTO verifizierung (benutzer_id, verfahren, geprueft_am, ergebnis)
SELECT benutzer_id, 'Altbestand', registriert_am, 'bestanden'
FROM benutzer
WHERE verifiziert = TRUE;
ALTER TABLE benutzer DROP COLUMN verifiziert;Bewertung: INSERT … SELECT mit Spaltenliste 1 Punkt, feste Werte und registriert_am 1 Punkt, Bedingung 1 Punkt, DROP COLUMN erst nach dem Übertragen 1 Punkt.
Im Testbestand werden 11 Konten übertragen (alle außer Tobias Lange und Jana Meier). Ob ein Konto verifiziert ist, ergibt sich künftig aus einer Abfrage, z. B. EXISTS (SELECT 1 FROM verifizierung v WHERE v.benutzer_id = b.benutzer_id AND v.ergebnis = 'bestanden').
gc) (3 Punkte: implizites COMMIT 1 Punkt, Folge für INSERT und ROLLBACK 1 Punkt, sicheres Vorgehen 1 Punkt) Das funktioniert nur teilweise: DDL-Anweisungen wie ALTER TABLE, CREATE TABLE oder DROP TABLE lösen in MariaDB ein implizites COMMIT aus und lassen sich nicht zurücknehmen. Das ALTER TABLE … DROP COLUMN beendet die Transaktion: Das vorherige INSERT wird dabei festgeschrieben, die gelöschte Spalte ist mit ROLLBACK nicht wiederherstellbar. Sicher ist folgendes Vorgehen: vorher eine Sicherung erstellen (z. B. mariadb-dump), nach dem INSERT prüfen, ob die Anzahl übertragener Zeilen der Anzahl verifizierter Konten entspricht (hier 11), und erst dann die Spalte löschen. Eine Transaktion um das INSERT allein ist sinnvoll. In PostgreSQL, SQL Server und SQLite lassen sich DDL-Anweisungen dagegen innerhalb einer Transaktion zurücknehmen; Oracle schreibt wie MariaDB implizit fest.
Aufgabe 2.3: Schichtplanung und Aufräumen in der Barcamp-Datenbank (Kernkompetenz)
(ca. 32 min Bearbeitungszeit)
Du bist Auszubildende*r und hilfst im Orga-Team des Barcamps „Lernen.Digital.Leben 2027“ am Ratz und Rübe Berufskolleg (RRB) in Bielefeld mit. Für die rund 40 Helfenden am 13.03.2027 soll die Schichtplanung in die bestehende Barcamp-Datenbank aufgenommen werden. Das Datenmodell hat Dimitrios Papadopoulos (Gesamtleitung) bereits freigegeben:
aufgabenbereich (aufgabenbereich_id, bezeichnung, beschreibung, #verantwortlich_id → person)
schicht (schicht_id, beginn, ende, max_helfende, #veranstaltung_id, #aufgabenbereich_id, #raum_id – nur bei Raumbetreuung, sonst leer)
einsatz (#schicht_id, #person_id, eingetragen_am, erschienen) – eine Person kann mehrere Schichten übernehmen
geraeteausgabe (#geraet_id, #schicht_id, #person_id, ausgegeben_um, zurueck_um) – ein Gerät wird je Schicht an höchstens eine Person dieser Schicht ausgegeben
Die Tabellen person, veranstaltung, raum und geraet sind vorhanden; ihre Primärschlüssel haben alle den Typ INT UNSIGNED. Außerdem gibt es die Tabelle orga_aufgabe (aufgabe_id, titel, verantwortlich_id, faellig_am, status) mit sechs Aufgaben. Laut Aufsichtsregel der Schule muss jede Schicht mit mindestens zwei Personen besetzt sein.
Den Ausgangszustand mit Testdaten legt Teil 2 des Testdaten-Skripts an; Teil 3 ergänzt nach Teilaufgabe a Schichten und Einsätze, Teil 4 enthält Testanweisungen zu Teilaufgabe b.
a) Hannah Klein (Schülerin der FIA-Klasse am RRB) hat ein Skript für die ersten drei Tabellen geschrieben. Es läuft nicht und erfüllt auch die Anforderungen noch nicht vollständig:
CREATE TABLE schicht (
schicht_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
veranstaltung_id INT UNSIGNED NOT NULL,
aufgabenbereich_id INT UNSIGNED NOT NULL,
raum_id INT UNSIGNED NOT NULL,
beginn VARCHAR(5),
ende VARCHAR(5),
max_helfende TINYINT UNSIGNED NOT NULL DEFAULT 2
PRIMARY KEY (schicht_id),
FOREIGN KEY (veranstaltung_id) REFERENCES veranstaltung (veranstaltung_id),
FOREIGN KEY (aufgabenbereich_id) REFERENCES aufgabenbereich (aufgabenbereich_id),
FOREIGN KEY (raum_id) REFERENCES raum (raum_id)
);
CREATE TABLE aufgabenbereich (
aufgabenbereich_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
bezeichnung string NOT NULL,
beschreibung VARCHAR(300),
verantwortlich_id INT NOT NULL,
FOREIGN KEY (verantwortlich_id) REFERENCES person (person_id)
);
CREATE TABLE einsatz (
schicht_id INT UNSIGNED NOT NULL,
person_id INT UNSIGNED NOT NULL,
eingetragen_am DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
erschienen BOOLEAN NOT NULL DEFAULT 'nein',
PRIMARY KEY (person_id),
FOREIGN KEY (schicht_id) REFERENCES schicht (schicht_id),
FOREIGN KEY (person_id) REFERENCES person (person_id)
);Finde acht Fehler und gib jeweils an, wie er korrigiert wird. (8 Punkte)
Je gefundenem und korrigiertem Fehler 1 Punkt:
| Nr. | Fehler | Korrektur |
|---|---|---|
| 1 | Reihenfolge: schicht verweist auf aufgabenbereich, das erst danach angelegt wird – MariaDB lehnt den Fremdschlüssel ab (Fehler 1005, errno 150). |
aufgabenbereich zuerst anlegen, dann schicht, dann einsatz. |
| 2 | Nach DEFAULT 2 fehlt das Komma vor PRIMARY KEY (Syntaxfehler 1064). |
max_helfende TINYINT UNSIGNED NOT NULL DEFAULT 2, |
| 3 | raum_id ist NOT NULL, nur Schichten der Raumbetreuung haben aber einen Raum. |
raum_id INT UNSIGNED NULL |
| 4 | beginn und ende als Text: Uhrzeiten lassen sich so weder sicher vergleichen noch prüfen (als Text gilt „8:30“ > „10:30“), ungültige Werte wie „25:99“ wären möglich; außerdem fehlt die Eingabepflicht. |
beginn TIME NOT NULL, ende TIME NOT NULL |
| 5 | string ist kein SQL-Datentyp (Syntaxfehler). |
bezeichnung VARCHAR(40) NOT NULL |
| 6 | verantwortlich_id INT passt nicht zu person.person_id INT UNSIGNED; Fremdschlüssel brauchen denselben Typ einschließlich Vorzeichen (Fehler 1005, errno 150). |
verantwortlich_id INT UNSIGNED NOT NULL |
| 7 | BOOLEAN (intern TINYINT(1)) kann den Standardwert 'nein' nicht aufnehmen (Fehler 1067 „Invalid default value“). |
erschienen BOOLEAN NOT NULL DEFAULT FALSE |
| 8 | Primärschlüssel nur person_id: Jede Person könnte nur eine einzige Schicht übernehmen. |
PRIMARY KEY (schicht_id, person_id) |
Weitere sinnvolle Verbesserungen (ohne Punkte, als Alternative für einen der Fehler aber gleichwertig): CHECK (ende > beginn) und CHECK (max_helfende >= 2), damit die Aufsichtsregel der Schule überhaupt erfüllbar ist, Namen für die Constraints.
Korrigiert lautet die Tabelle schicht z. B.:
CREATE TABLE schicht (
schicht_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
veranstaltung_id INT UNSIGNED NOT NULL,
aufgabenbereich_id INT UNSIGNED NOT NULL,
raum_id INT UNSIGNED NULL,
beginn TIME NOT NULL,
ende TIME NOT NULL,
max_helfende TINYINT UNSIGNED NOT NULL DEFAULT 2,
PRIMARY KEY (schicht_id),
CONSTRAINT chk_schicht_zeit CHECK (ende > beginn),
FOREIGN KEY (veranstaltung_id) REFERENCES veranstaltung (veranstaltung_id),
FOREIGN KEY (aufgabenbereich_id) REFERENCES aufgabenbereich (aufgabenbereich_id),
FOREIGN KEY (raum_id) REFERENCES raum (raum_id)
);b) Erstelle die SQL-Anweisung für die Tabelle geraeteausgabe. Die Datenbank soll selbst sicherstellen, dass ein Gerät nur an eine Person ausgegeben wird, die in dieser Schicht eingetragen ist. Die Rückgabezeit ist bei der Ausgabe noch leer und darf nicht vor der Ausgabezeit liegen. (5 Punkte)
CREATE TABLE geraeteausgabe (
geraet_id INT UNSIGNED NOT NULL,
schicht_id INT UNSIGNED NOT NULL,
person_id INT UNSIGNED NOT NULL,
ausgegeben_um TIME NOT NULL,
zurueck_um TIME NULL,
PRIMARY KEY (geraet_id, schicht_id),
CONSTRAINT chk_ga_zeit CHECK (zurueck_um >= ausgegeben_um),
CONSTRAINT fk_ga_geraet FOREIGN KEY (geraet_id) REFERENCES geraet (geraet_id),
CONSTRAINT fk_ga_einsatz FOREIGN KEY (schicht_id, person_id)
REFERENCES einsatz (schicht_id, person_id)
);Bewertung: zusammengesetzter Primärschlüssel 1 Punkt, Fremdschlüssel auf geraet 1 Punkt, zusammengesetzter Fremdschlüssel auf einsatz 2 Punkte, Zeitspalten mit CHECK 1 Punkt.
Worauf es ankommt:
Ein Fremdschlüssel aus zwei Spalten muss auf einen Primärschlüssel (oder
UNIQUE-Schlüssel) mit denselben zwei Spalten in derselben Reihenfolge verweisen. Zwei getrennte Fremdschlüssel aufschichtundpersonwürden auch eine Person akzeptieren, die gar nicht in der Schicht eingetragen ist.person_idmussNOT NULLsein: Ist ein Teil eines zusammengesetzten FremdschlüsselsNULL, prüft MariaDB den Fremdschlüssel nicht.Ist
zurueck_umNULL, gilt dieCHECK-Bedingung als erfüllt;CHECK (zurueck_um IS NULL OR zurueck_um >= ausgegeben_um)ist gleichwertig und deutlicher. „Nicht vor“ heißt>=: Eine Rückgabe zur selben Uhrzeit ist erlaubt.Test mit den Testdaten (Skript, Teil 3 und 4): Ist Hannah (Person 14) in Schicht 1 eingetragen, Emre (Person 4) aber nicht, wird die Ausgabe des Terminals CHK-01 an Hannah gespeichert, die an Emre mit Fehler 1452 („Cannot add or update a child row“) abgelehnt; eine zweite Ausgabe von CHK-01 in Schicht 1 scheitert am Primärschlüssel.
c) Die sechs vorhandenen Orga-Aufgaben sollen künftig einem Aufgabenbereich zugeordnet werden können. Binde die Tabelle aufgabenbereich mit einer SQL-Anweisung in die Tabelle orga_aufgabe ein. Wird ein Aufgabenbereich gelöscht, sollen die Orga-Aufgaben erhalten bleiben. Begründe, warum die neue Spalte vorerst kein Pflichtfeld sein kann. (5 Punkte)
ALTER TABLE orga_aufgabe
ADD COLUMN aufgabenbereich_id INT UNSIGNED NULL,
ADD CONSTRAINT fk_oa_bereich FOREIGN KEY (aufgabenbereich_id)
REFERENCES aufgabenbereich (aufgabenbereich_id) ON DELETE SET NULL;Bewertung: ADD COLUMN mit passendem Typ 1 Punkt, ADD CONSTRAINT … FOREIGN KEY … REFERENCES 2 Punkte, ON DELETE SET NULL 1 Punkt, Begründung 1 Punkt.
Begründung: Die sechs Orga-Aufgaben existieren schon und haben noch keinen Aufgabenbereich. Eine NOT NULL-Spalte bräuchte sofort einen gültigen Wert für alle Zeilen – 0 oder ein willkürlicher Bereich würde den Fremdschlüssel verletzen bzw. falsche Zuordnungen erzeugen. Außerdem verlangt ON DELETE SET NULL, dass die Spalte NULL aufnehmen kann. Erst wenn alle Aufgaben zugeordnet sind, könnte man sie zum Pflichtfeld machen (dann aber mit ON DELETE RESTRICT).
Zwei getrennte Anweisungen (ALTER TABLE … ADD COLUMN …; und ALTER TABLE … ADD FOREIGN KEY …;) sind ebenfalls richtig.
d) Für die Anmeldung wurde bis Anfang Oktober ein Online-Formular genutzt; sein Export liegt unbereinigt in der Tabelle formular_export (zeile, zeitstempel, name, email, schule_firma, teilnahme_als, sessionidee, essen, hinweise); zeile ist eine fortlaufende Nummer, alle übrigen Spalten sind Text und erlauben NULL. Alle Anmeldungen wurden inzwischen in die Tabellen person und anmeldung übernommen. Aus Gründen der Datenminimierung soll der Export gelöscht werden. Die eingereichten Session-Ideen möchte Dimitrios aber ohne Personenbezug für die Themensuche im nächsten Jahr aufheben.
da) Ermittle mit einer SQL-Abfrage alle Zeilen des Exports, die eine Session-Idee enthalten. (2 Punkte)
db) Erstelle die Tabelle ideen_archiv mit einem eigenen Schlüssel, den Spalten teilnahme_als und sessionidee aus dem Export und dem Jahr des Barcamps (Standardwert 2027), und übertrage die Ideen ohne Duplikate. (4 Punkte)
dc) Erstelle die Anweisung, die den Export vollständig entfernt, und nimm Stellung, warum hier weder DELETE FROM formular_export; noch TRUNCATE TABLE formular_export; gewählt wird. (5 Punkte)
da) (2 Punkte)
SELECT zeile, name, teilnahme_als, sessionidee
FROM formular_export
WHERE sessionidee IS NOT NULL;Ergebnis: fünf Zeilen (1 Oluwaseun Adeyemi, 3 und 4 Gerhard Vogt – doppelt abgeschickt, 5 Lisa Brandt, 7 Mia Schröder). Die zusätzliche Bedingung AND sessionidee <> '' ist ebenfalls richtig.
db)
CREATE TABLE ideen_archiv (
idee_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
teilnahme_als VARCHAR(50) NULL,
sessionidee VARCHAR(300) NOT NULL,
jahr SMALLINT UNSIGNED NOT NULL DEFAULT 2027
);
INSERT INTO ideen_archiv (teilnahme_als, sessionidee)
SELECT DISTINCT teilnahme_als, sessionidee
FROM formular_export
WHERE sessionidee IS NOT NULL;Bewertung: Tabelle mit Schlüssel, sinnvollen Datentypen und DEFAULT 2027 2 Punkte, INSERT … SELECT mit Spaltenliste 1 Punkt, DISTINCT und Bedingung 1 Punkt.
Ergebnis: 4 Ideen; die doppelte Zeile von Gerhard Vogt wird durch DISTINCT nur einmal übernommen. jahr erhält den Standardwert, weil die Spalte im INSERT fehlt.
Alternative: CREATE TABLE ideen_archiv AS SELECT DISTINCT teilnahme_als, sessionidee FROM formular_export WHERE sessionidee IS NOT NULL; legt die Tabelle und die Daten in einem Schritt an, übernimmt aber keinen Primärschlüssel und keine Indizes; die Spalte jahr entsteht gar nicht, und weil die Textspalten des Exports NULL erlauben, ist auch sessionidee nicht NOT NULL. Das müsste mit ALTER TABLE ergänzt werden. Umgekehrt übernimmt CREATE TABLE … LIKE die Struktur samt Indizes, aber ohne Daten und ohne Fremdschlüssel.
dc)
DROP TABLE formular_export;(1 Punkt; DROP TABLE IF EXISTS ebenfalls richtig)
Stellungnahme (4 Punkte):
DELETE FROM formular_export;(DML) löscht nur die Datensätze, Zeile für Zeile; die leere Tabelle mit ihrer Struktur bleibt bestehen. Gefordert ist, den Export vollständig zu entfernen – die Tabelle wird nicht mehr gebraucht.TRUNCATE TABLE formular_export;(DDL) leert die Tabelle schneller, indem sie sie intern neu anlegt, und setzt denAUTO_INCREMENT-Zähler zurück. Auch hier bleibt die leere Tabelle bestehen.DROP TABLE(DDL) entfernt Struktur und Inhalt. WieTRUNCATEist es nicht mitROLLBACKumkehrbar. Deshalb vorher prüfen, ob wirklich alle Ideen archiviert sind (Ergebnis von db).Würde eine andere Tabelle per Fremdschlüssel auf den Export verweisen, verweigerten
DROPundTRUNCATEdie Ausführung; das ist hier nicht der Fall.
e) Für das nächste Barcamp möchte Emre Aksoy die Check-in-Terminals ausprobieren, ohne die echte Datenbank barcamp zu gefährden.
ea) Erstelle die Anweisungen, mit denen Du eine Datenbank barcamp_test anlegst (nur, falls sie noch nicht existiert), sie auswählst, darin eine Tabelle raum mit derselben Struktur wie barcamp.raum anlegst und deren Struktur anzeigen lässt. Nutze die SQL-Kurzreferenz. (3 Punkte)
eb) Dimitrios schlägt vor, auch im Echtbetrieb für jedes Barcamp-Jahr eine eigene Datenbank (barcamp2027, barcamp2028 …) anzulegen. Nimm Stellung. (3 Punkte)
ea)
CREATE DATABASE IF NOT EXISTS barcamp_test;
USE barcamp_test;
CREATE TABLE raum LIKE barcamp.raum;
DESCRIBE raum;Bewertung: CREATE DATABASE IF NOT EXISTS und USE 1 Punkt, CREATE TABLE … LIKE 1 Punkt, DESCRIBE (oder SHOW CREATE TABLE raum; bzw. SHOW COLUMNS FROM raum;) 1 Punkt.
CREATE TABLE … LIKE übernimmt Spalten, Datentypen, Standardwerte und Indizes (einschließlich Primärschlüssel), aber keine Daten und keine Fremdschlüssel. Daten lassen sich anschließend mit INSERT INTO raum SELECT * FROM barcamp.raum; kopieren. In MariaDB sind DATABASE und SCHEMA gleichbedeutend.
eb) (3 Punkte; begründete Ablehnung mit drei Argumenten oder begründete Zustimmung)
Abzuraten, weil
das Datenmodell schon mehrere Veranstaltungen kennt (
veranstaltung_idinanmeldung,slot,session,schicht); jedes Jahr ist nur ein weiterer Datensatz inveranstaltung,Stammdaten wie
person,raumodergeraetsonst in jeder Datenbank doppelt gepflegt würden (Redundanz, Änderungsanomalien) und Fremdschlüssel über Datenbankgrenzen hinweg unübersichtlich werden,Auswertungen über mehrere Jahre („Wie hat sich die Teilnehmendenzahl entwickelt?“) dann Abfragen über mehrere Datenbanken bräuchten,
jede Strukturänderung (z. B. die neue Spalte aus c) in allen Datenbanken wiederholt werden müsste.
Für die Datenminimierung reicht es, personenbezogene Daten vergangener Barcamps nach Ablauf der Frist zu löschen bzw. zu anonymisieren. Eine getrennte Testdatenbank wie in ea) ist dagegen sinnvoll.
Aufgabe 3.1: Ausbildungsbetriebe und Parkplatzbelegung in der Mitfahr-App (Profilkompetenz)
(ca. 36 min Bearbeitungszeit)
Du bist Auszubildende*r im Projektteam der Mitfahr-App „RatzFahrt“ des Ratz und Rübe Berufskollegs in Bielefeld. Die MariaDB-Datenbank ratzfahrt läuft seit September; in der Tabelle nutzer sind zehn Personen registriert. Zwei neue Wünsche liegen auf dem Tisch: Mehrere Ausbildungsbetriebe wollen Fahrgemeinschaften ihrer Auszubildenden mit einem Bonus je Fahrt fördern, und die Schul-IT möchte in der App anzeigen, wie voll die Schulparkplätze gerade sind. Auszug aus der bestehenden Datenbank:
CREATE TABLE nutzer (
nutzer_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
pseudonym VARCHAR(30) NOT NULL UNIQUE,
vorname VARCHAR(50) NOT NULL,
nachname VARCHAR(50) NOT NULL,
email_schule VARCHAR(100) NOT NULL UNIQUE,
geburtsdatum DATE NOT NULL,
klasse_id INT UNSIGNED NULL, -- NULL bei Lehrkräften
-- weitere Spalten
CONSTRAINT fk_nutzer_klasse FOREIGN KEY (klasse_id) REFERENCES klasse (klasse_id)
);
CREATE TABLE parkplatz (
parkplatz_id TINYINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
bezeichnung VARCHAR(40) NOT NULL UNIQUE,
stellplaetze SMALLINT NOT NULL
);
-- Inhalt: (1, 'Parkplatz A (Lehrkräfte)', 64), (2, 'Parkplatz C (Schüler*innen)', 180)a) Für die neue Tabelle ausbildungsbetrieb hat Thomas Becker (Projektleitung) einen ersten Entwurf auf dem Testsystem ausprobiert:
CREATE TABLE ausbildungsbetrieb_test (
betrieb_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
plz INT NOT NULL,
ort VARCHAR(60) NOT NULL,
ust_id BIGINT NULL,
foerderung_aktiv VARCHAR(4) NOT NULL DEFAULT 'ja',
vertrag_seit VARCHAR(10) NULL,
bonus_je_fahrt INT NOT NULL DEFAULT 0
);
INSERT INTO ausbildungsbetrieb_test
(name, plz, ort, foerderung_aktiv, vertrag_seit, bonus_je_fahrt)
VALUES ('Elektro Brinkmann KG', '33689', 'Bielefeld', 'ja', '01.08.2026', 1.50),
('Logistik Auensee GmbH', '04109', 'Leipzig', 'nein', '2026-09-15', 0.75),
('Tischlerei Vogt', '33602', 'Bielefeld', 'Ja', '1.10.26', 0.40);Danach hat er drei Dinge ausprobiert:
SELECT betrieb_id, plz, foerderung_aktiv, vertrag_seit, bonus_je_fahrt FROM ausbildungsbetrieb_test;liefert:
| betrieb_id | plz | foerderung_aktiv | vertrag_seit | bonus_je_fahrt |
|---|---|---|---|---|
| 1 | 33689 | ja | 01.08.2026 | 2 |
| 2 | 4109 | nein | 2026-09-15 | 1 |
| 3 | 33602 | Ja | 1.10.26 | 0 |
Die Abfrage „Betriebe mit Vertrag vor dem 1. September 2026“ (
… WHERE vertrag_seit < '2026-09-01') liefert die Betriebe 1 und 3.UPDATE ausbildungsbetrieb_test SET ust_id = 'DE813245679' WHERE betrieb_id = 1;(Umsatzsteuer-Identifikationsnummer) bricht mit Fehler 1366 („Incorrect integer value“) ab.
Werte die Tests aus: Benenne für fünf Spalten, welches Problem der Entwurf verursacht, und schlage jeweils einen besser geeigneten Datentyp vor. Begründe bei plz und ust_id außerdem, warum eine Zeichenkette fester Länge passt. (5 Punkte)
Je Spalte 1 Punkt:
| Spalte | Problem im Entwurf | besser |
|---|---|---|
plz |
Als Zahl fällt die führende Null weg (04109 → 4109); mit Postleitzahlen wird nie gerechnet. |
CHAR(5) |
ust_id |
Die USt-IdNr. beginnt mit dem Länderkürzel (DE) – Buchstaben passen in keine Ganzzahl; gerechnet wird mit ihr ohnehin nicht. |
CHAR(11) für deutsche USt-IdNr. (DE + 9 Ziffern), VARCHAR(14) für ausländische |
foerderung_aktiv |
Freier Text erlaubt ja, Ja, jo …; Abfragen wie WHERE foerderung_aktiv = 'ja' übersehen Varianten. |
BOOLEAN NOT NULL DEFAULT TRUE |
vertrag_seit |
Datumsangaben als Text in verschiedenen Schreibweisen; der Vergleich läuft Zeichen für Zeichen ('1.10.26' < '2026-09-01', weil '1' < '2'), deshalb erscheint Betrieb 3 fälschlich im Ergebnis. |
DATE (Eingabe als '2026-08-01') |
bonus_je_fahrt |
Ganzzahl: Die Beträge werden gerundet, die Cent gehen verloren (1,50 € → 2, 0,40 € → 0). | DECIMAL(4,2) (Festkomma, keine Rundungsfehler wie bei FLOAT) |
Feste Länge: Deutsche Postleitzahlen haben immer fünf, deutsche USt-IdNr. immer elf Zeichen. CHAR(n) braucht dann kein zusätzliches Längenfeld, und VARCHAR brächte keinen Vorteil. (Die Begründung ist Teil der Punkte für plz und ust_id.)
b) Gib die SQL-Anweisung an, die die Tabelle ausbildungsbetrieb mit den Feldern aus a) anlegt. Dabei gilt: Die ID wird automatisch vergeben; Name, PLZ und Ort sind Pflichtangaben; eine USt-IdNr. darf fehlen (z. B. bei Kleinunternehmen), kommt aber höchstens einmal vor; neue Betriebe werden standardmäßig als „Förderung aktiv“ angelegt; der Bonus liegt zwischen 0 und 5 € und ist ohne Angabe 0. (6 Punkte)
CREATE TABLE ausbildungsbetrieb (
betrieb_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
plz CHAR(5) NOT NULL,
ort VARCHAR(60) NOT NULL,
ust_id CHAR(11) NULL,
foerderung_aktiv BOOLEAN NOT NULL DEFAULT TRUE,
vertrag_seit DATE NULL,
bonus_je_fahrt DECIMAL(4,2) NOT NULL DEFAULT 0.00,
CONSTRAINT pk_betrieb PRIMARY KEY (betrieb_id),
CONSTRAINT uq_betrieb_ust_id UNIQUE (ust_id),
CONSTRAINT chk_bonus CHECK (bonus_je_fahrt BETWEEN 0 AND 5)
);Bewertung: CREATE TABLE mit Spalten und Datentypen 1 Punkt, Primärschlüssel mit AUTO_INCREMENT 1 Punkt, NOT NULL für die Pflichtfelder 1 Punkt, UNIQUE für die USt-IdNr. (mehrere NULL bleiben erlaubt) 1 Punkt, zwei DEFAULT-Werte 1 Punkt, CHECK 1 Punkt. Constraints können auch direkt an der Spalte stehen.
Getestet mit zwei Beispielbetrieben; ein Bonus von 7,00 € wird abgewiesen (Fehler 4025 „CONSTRAINT chk_bonus failed“).
c) Jede*r Auszubildende gehört zu genau einem Ausbildungsbetrieb, ein Betrieb kann viele Auszubildende haben; Lehrkräfte haben keinen Betrieb. Gib die SQL-Anweisungen in der richtigen Reihenfolge an, mit denen die Tabelle nutzer unter Einhaltung der 3. Normalform mit ausbildungsbetrieb verknüpft wird. Wird ein Betrieb gelöscht, sollen die Nutzerkonten erhalten bleiben. (5 Punkte)
-- 1. Tabelle ausbildungsbetrieb anlegen (siehe b)
-- 2. Fremdschlüsselspalte ergänzen (gleicher Typ wie der Primärschlüssel!)
ALTER TABLE nutzer
ADD COLUMN betrieb_id INT UNSIGNED NULL AFTER klasse_id;
-- 3. Fremdschlüssel festlegen
ALTER TABLE nutzer
ADD CONSTRAINT fk_nutzer_betrieb FOREIGN KEY (betrieb_id)
REFERENCES ausbildungsbetrieb (betrieb_id)
ON DELETE SET NULL;Bewertung: Spalte mit passendem Typ INT UNSIGNED und NULL erlaubt (Lehrkräfte) 2 Punkte, Fremdschlüssel mit REFERENCES 2 Punkte, richtige Reihenfolge und ON DELETE SET NULL 1 Punkt. Beide Änderungen dürfen auch in einer ALTER TABLE-Anweisung stehen. Nicht in der 3. Normalform wäre es, Name oder Ort des Betriebs als Spalten in nutzer zu speichern.
d) Oksana Melnyk schlägt vor, die Tabelle nutzer einfach mit CREATE TABLE neu anzulegen, „dann steht betrieb_id gleich an der richtigen Stelle“. Nimm Stellung zu ihrem Vorschlag. Gehe dabei darauf ein, was mit den zehn Nutzerkonten und den Tabellen geschähe, die auf nutzer verweisen, wie sich Deine Anweisungen aus c) dagegen auf den Bestand auswirken und was danach noch zu erledigen ist. (4 Punkte)
Oksanas Vorschlag (2 Punkte): CREATE TABLE nutzer scheitert, solange die Tabelle existiert (Fehler 1050 „Table ‘nutzer’ already exists“). Um sie neu anzulegen, müsste sie vorher mit DROP TABLE gelöscht werden – das verweigert MariaDB aber, solange angebot, buchung usw. per Fremdschlüssel auf nutzer verweisen. Und selbst wenn es gelänge, wären alle zehn Nutzerkonten weg. Die gewünschte Position erreicht man auch mit ALTER TABLE … ADD COLUMN … AFTER klasse_id; für die Datenbank ist die Spaltenreihenfolge ohnehin bedeutungslos.
Wirkung von c) (1 Punkt): ALTER TABLE ändert nur die Struktur. Alle zehn Datensätze bleiben erhalten und haben in der neuen Spalte betrieb_id den Wert NULL, da kein DEFAULT angegeben ist. Der Fremdschlüssel lässt sich sofort anlegen, weil NULL-Werte die referenzielle Integrität nicht verletzen.
Nacharbeit (1 Punkt): Für jede*n Auszubildende*n wird der passende Betrieb per UPDATE nachgetragen, z. B. UPDATE nutzer SET betrieb_id = 1 WHERE nutzer_id = 1;. Lehrkräfte behalten NULL.
e) Für die Ansprechpersonen in den Betrieben hat ein Mitschüler folgende Anweisung geschrieben. MariaDB lehnt sie ab. Benenne sechs Fehler und gib eine korrigierte Anweisung an. (6 Punkte)
CREATE TABLE betrieb-ansprechperson (
ansprech-id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(80) NOT NULL,
email VARCHAR(100),
betrieb_id INT NOT NULL,
PRIMARY KEY (ansprech-id),
FOREIGN KEY betrieb_id REFERENCES ausbildungsbetrieb,
);Fehler (je 0,5 Punkte):
- Bindestrich im Tabellennamen
betrieb-ansprechperson(wird als Minus gelesen) → Unterstrich oder Name in Backticks. - Bindestrich im Spaltennamen
ansprech-id(in der Spaltendefinition und imPRIMARY KEY). betrieb_idistINT, der Primärschlüssel inausbildungsbetriebistINT UNSIGNED: Fremdschlüssel und referenzierte Spalte müssen gleichen Typ und gleiches Vorzeichen haben.- Nach
FOREIGN KEYfehlen die Klammern um die Spalte:FOREIGN KEY (betrieb_id). - Nach
REFERENCES ausbildungsbetriebfehlt die referenzierte Spalte(betrieb_id). - Komma nach der letzten Tabellenbedingung vor der schließenden Klammer.
Korrigiert (3 Punkte):
CREATE TABLE betrieb_ansprechperson (
ansprech_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(80) NOT NULL,
email VARCHAR(100),
betrieb_id INT UNSIGNED NOT NULL,
PRIMARY KEY (ansprech_id),
FOREIGN KEY (betrieb_id) REFERENCES ausbildungsbetrieb (betrieb_id)
);f) An den Einfahrten der Schulparkplätze zählen Induktionsschleifen die ein- und ausfahrenden Autos. Ihre Steuerung meldet alle fünf Minuten, wie viele Stellplätze belegt sind; die App soll damit anzeigen, ob sich die Suche nach einem Platz lohnt. Svenja Krämer (Schul-IT) hat dafür bereits eine Tabelle angelegt:
CREATE TABLE belegung (
belegung_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
parkplatz_id TINYINT UNSIGNED NOT NULL,
gemeldet_um DATETIME NOT NULL,
belegt SMALLINT NOT NULL,
CONSTRAINT fk_belegung_parkplatz FOREIGN KEY (parkplatz_id)
REFERENCES parkplatz (parkplatz_id)
);fa) In der ersten Testwoche kamen nach Funkaussetzern Meldungen doppelt an (gleicher Parkplatz, gleicher Zeitpunkt), und nach einem Stromausfall meldete eine Steuerung negative Belegungen. Ergänze die Tabelle mit einer ALTER TABLE-Anweisung so, dass die Datenbank beides künftig abweist. Erläutere, was passiert, wenn noch fehlerhafte Testzeilen in der Tabelle stehen. (4 Punkte)
fb) Die App soll Meldungen wie „Parkplatz C (Schüler*innen): noch 23 von 180 Plätzen frei, 87 % belegt“ anzeigen. Jonas möchte dazu in belegung eine generierte Spalte frei mit dem Ausdruck stellplaetze - belegt ergänzen. Erkläre, warum MariaDB das ablehnt, und erstelle stattdessen eine View v_belegung, die zu jeder Meldung Bezeichnung des Parkplatzes, Zeitpunkt, belegte und freie Plätze sowie die Auslastung in ganzen Prozent liefert. (5 Punkte)
fc) Svenja schlägt vor, die Zahl der Stellplätze zusätzlich in jede Zeile von belegung zu schreiben – „dann klappt die generierte Spalte doch“. Nimm Stellung. (2 Punkte)
fa)
ALTER TABLE belegung
ADD CONSTRAINT uq_belegung_zeitpunkt UNIQUE (parkplatz_id, gemeldet_um),
ADD CONSTRAINT chk_belegung_belegt CHECK (belegt >= 0);Bewertung: UNIQUE über beide Spalten (nicht nur gemeldet_um, denn beide Parkplätze melden zur selben Zeit) 1 Punkt, CHECK (belegt >= 0) 1 Punkt, beides in einer ALTER TABLE-Anweisung mit ADD CONSTRAINT 1 Punkt. Gleichwertig für den zweiten Teil: MODIFY belegt SMALLINT UNSIGNED NOT NULL – dann weist MariaDB negative Werte im strikten Modus als „Out of range“ (Fehler 1264) ab.
Vorhandene Fehlzeilen (1 Punkt): MariaDB prüft beim Hinzufügen der Constraints alle vorhandenen Zeilen. Gibt es noch ein doppeltes Paar, bricht die Anweisung mit Fehler 1062 („Duplicate entry“) ab, bei einer negativen Belegung mit Fehler 4025 („CONSTRAINT chk_belegung_belegt failed“). Die ganze Anweisung wird dann nicht ausgeführt; vorher müssen die Fehlzeilen gesucht (GROUP BY parkplatz_id, gemeldet_um HAVING COUNT(*) > 1 bzw. WHERE belegt < 0) und bereinigt werden.
fb)
Begründung (1 Punkt): Der Ausdruck einer generierten Spalte darf nur Spalten derselben Zeile derselben Tabelle verwenden. stellplaetze steht in parkplatz; MariaDB meldet deshalb eine unbekannte Spalte. Unterabfragen sind in generierten Spalten ebenfalls nicht erlaubt. Werte, die Daten aus mehreren Tabellen verbinden, berechnet eine View.
CREATE VIEW v_belegung AS
SELECT p.bezeichnung,
b.gemeldet_um,
b.belegt,
p.stellplaetze - b.belegt AS frei,
ROUND(b.belegt * 100 / p.stellplaetze) AS auslastung_prozent
FROM belegung AS b
JOIN parkplatz AS p ON p.parkplatz_id = b.parkplatz_id;Bewertung: CREATE VIEW … AS SELECT 1 Punkt, Verknüpfung über parkplatz_id 1 Punkt, freie Plätze 1 Punkt, Auslastung in ganzen Prozent 1 Punkt.
Beispiel: Meldet Parkplatz C um 7:55 Uhr 157 belegte Plätze, liefert die View frei = 23 und auslastung_prozent = 87 (157 · 100 / 180 ≈ 87,2). Die App fragt dann nur die jüngste Meldung je Parkplatz ab, z. B. mit ORDER BY gemeldet_um DESC LIMIT 1.
fc) (2 Punkte) In der Regel abzulehnen: stellplaetze hängt nur vom Parkplatz ab, nicht von der einzelnen Meldung. Die Kopie in jeder Zeile wäre redundant (Verstoß gegen die 3. Normalform), kostet bei 288 Meldungen je Parkplatz und Tag unnötig Speicher und führt zu Widersprüchen, sobald sich die Zahl der Stellplätze ändert, aber nicht überall nachgetragen wird. Die View aus fb) liefert dasselbe ohne Redundanz. Vertretbar ist der Vorschlag nur, wenn bewusst die Kapazität zum Meldezeitpunkt festgehalten werden soll (z. B. weil eine Baustelle zeitweise Plätze sperrt) – dann ist es kein redundanter Wert, sondern ein eigener Messwert und sollte auch so heißen (z. B. stellplaetze_verfuegbar).
g) Die Steuerung der Zählschleifen soll sich mit einem eigenen Datenbankbenutzer anmelden, der ausschließlich Belegungsmeldungen eintragen darf. Die Steuerungen stehen im Netz 10.50.30.0/24. Gib die SQL-Anweisungen an, die den Benutzer zaehldienst mit Passwort anlegen und ihm nur dieses Recht geben. (3 Punkte)
CREATE USER 'zaehldienst'@'10.50.30.%' IDENTIFIED BY 'Vh7!pQ2#rT9w';
GRANT INSERT ON ratzfahrt.belegung TO 'zaehldienst'@'10.50.30.%';Bewertung: Benutzer mit Passwort 1 Punkt, Einschränkung auf das Netz der Steuerungen 1 Punkt, nur INSERT auf genau diese Tabelle 1 Punkt. Weil die Spalte belegung_id automatisch vergeben wird, genügt das INSERT-Recht; die View v_belegung braucht der Zähldienst nicht. Mehr zu Benutzern und Rechten: SQL-DCL.
Anlage: SQL-Kurzreferenz DDL (MariaDB)
| Syntax | Beschreibung |
|---|---|
CREATE DATABASE [IF NOT EXISTS] name; / USE name; |
Datenbank anlegen / auswählen (SCHEMA gleichbedeutend mit DATABASE) |
CREATE TABLE tabelle (spalte TYP [optionen], …, [tabellenbedingungen]); |
Tabelle anlegen |
CREATE TABLE neu LIKE alt; |
leere Tabelle mit gleicher Struktur (ohne Fremdschlüssel) |
CREATE TABLE neu AS SELECT …; |
Tabelle aus einem Abfrageergebnis (ohne Schlüssel und Indizes) |
Spaltenoptionen: NOT NULL, NULL, DEFAULT wert, AUTO_INCREMENT, UNIQUE, PRIMARY KEY |
Pflichtfeld, optional, Standardwert, fortlaufende Nummer, eindeutig, Primärschlüssel |
[CONSTRAINT name] PRIMARY KEY (spalte1[, spalte2 …]) |
(zusammengesetzter) Primärschlüssel |
[CONSTRAINT name] UNIQUE (spalte1[, spalte2 …]) |
Werte(kombination) eindeutig, NULL mehrfach erlaubt |
[CONSTRAINT name] CHECK (bedingung) |
nur Werte erlauben, für die die Bedingung nicht FALSE ist |
[CONSTRAINT name] FOREIGN KEY (spalte[, …]) REFERENCES tabelle (spalte[, …]) [ON DELETE \| ON UPDATE RESTRICT \| CASCADE \| SET NULL] |
Fremdschlüssel; ohne Angabe gilt RESTRICT |
ALTER TABLE tabelle ADD [COLUMN] spalte TYP [optionen] [AFTER spalte]; |
Spalte hinzufügen |
ALTER TABLE tabelle DROP [COLUMN] spalte; |
Spalte entfernen |
ALTER TABLE tabelle MODIFY [COLUMN] spalte TYP [optionen]; |
Datentyp/Optionen einer Spalte ändern (vollständige Definition) |
ALTER TABLE tabelle CHANGE [COLUMN] alt neu TYP [optionen]; / RENAME COLUMN alt TO neu |
Spalte umbenennen (und ändern) |
ALTER TABLE tabelle ADD [CONSTRAINT name] PRIMARY KEY \| UNIQUE \| CHECK \| FOREIGN KEY …; |
Bedingung nachträglich hinzufügen |
ALTER TABLE tabelle DROP CONSTRAINT name; / DROP FOREIGN KEY name; / DROP INDEX name; |
Bedingung, Fremdschlüssel, Index entfernen |
RENAME TABLE alt TO neu; |
Tabelle umbenennen |
DROP TABLE [IF EXISTS] tabelle[, …]; |
Tabelle mit Inhalt löschen |
TRUNCATE TABLE tabelle; |
alle Datensätze löschen, Struktur bleibt |
CREATE [UNIQUE] INDEX name ON tabelle (spalte1[, spalte2 …]); / DROP INDEX name ON tabelle; |
Index anlegen / löschen |
DESCRIBE tabelle; / SHOW CREATE TABLE tabelle; / SHOW INDEX FROM tabelle; |
Struktur, CREATE-Anweisung, Indizes anzeigen |
spalte TYP AS (ausdruck) [STORED \| VIRTUAL] |
generierte (berechnete) Spalte; Ausdruck nur aus Spalten derselben Zeile |
CREATE [OR REPLACE] VIEW name AS SELECT …; / DROP VIEW name; |
View (gespeicherte Abfrage) anlegen / löschen |
CREATE USER 'name'@'host' IDENTIFIED BY 'passwort'; / GRANT recht ON db.tabelle TO 'name'@'host'; |
Benutzer anlegen / Recht vergeben (DCL) |
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT [UNSIGNED] |
Ganzzahlen (1, 2, 3, 4, 8 Byte) |
DECIMAL(p, s) |
Festkommazahl mit p Stellen, davon s Nachkommastellen |
FLOAT, DOUBLE |
Gleitkommazahlen (4, 8 Byte) |
CHAR(n), VARCHAR(n), TEXT |
Zeichenkette fester / variabler Länge, langer Text |
DATE, TIME, DATETIME |
Datum ('2026-10-19'), Uhrzeit ('08:30:00'), beides |
BOOLEAN |
Wahrheitswert (Alias für TINYINT(1), TRUE = 1, FALSE = 0) |
ENUM('a', 'b', …) |
genau einer der aufgezählten Werte |
SUBSTRING_INDEX(text, trennzeichen, n) |
Teil von text vor dem n-ten Trennzeichen; bei negativem n Teil nach dem n-ten Trennzeichen von rechts |
SUBSTRING(text, start[, laenge]) |
Teil ab Position start (erstes Zeichen = 1) |
LEFT(text, n), RIGHT(text, n) |
die ersten / letzten n Zeichen |
LOCATE(suchtext, text) |
Position von suchtext in text (0, wenn nicht enthalten) |
TRIM(text) |
entfernt Leerzeichen am Anfang und Ende |
CONCAT(text1, text2, …) |
Texte aneinanderhängen |
CASE WHEN bedingung THEN wert [ELSE wert] END |
Fallunterscheidung |
text LIKE muster |
Mustervergleich, % beliebig viele Zeichen, _ genau ein Zeichen |
Links und weitere Informationen
Grundlagen der SQL-DDL, Datentypen und Constraints auf oer-informatik.de
Der logische Datenbankentwurf mit dem Relationenmodell und die Übungsaufgaben zum Relationenmodell
als Nachschlagewerk: MariaDB Knowledge Base: CREATE TABLE
als Nachschlagewerk: MariaDB Knowledge Base: ALTER TABLE
als Nachschlagewerk: MariaDB Knowledge Base: Foreign Keys
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 zum Anlegen und Ändern von Tabellen (SQL-DDL)” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/sql-ddl-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]
