Übungsaufgaben zu SQL-Abfragen über mehrere Tabellen (JOIN)
https://bildung.social/@oerinformatik/112633708392604185
https://oer-informatik.de/sql-join-uebungsaufgaben
SQL, JOIN, INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN, Anti-Join, Kartesisches Produkt, MariaDB, Übungsaufgaben, Prüfungsvorbereitung
tl/dr; (Bearbeitungszeit insgesamt 154 min, mehrere Aufgaben): Fünf Übungsaufgaben zu Verbünden über mehrere Tabellen – vom einfachen INNER JOIN über LEFT, RIGHT und nachgebauten FULL JOIN bis zu Tabellen in mehreren Rollen, zusammengesetzten Join-Bedingungen, Anti-Joins, Vervielfachung beim Zählen sowie UPDATE und DELETE mit JOIN, außerdem Zeitspannen-Auswertungen, DISTINCT-Abfragen und Verbünde mit pandas. Alle Beispiellösungen sind ausgeblendet und mit Punkteverteilung versehen [AI-Assisted]. (Zuletzt geändert am 26.10.2026)
Dieser Artikel ist Teil einer Artikelreihe über SQL-Abfragen: Selektion und Projektion / Sortieren und Begrenzen (ORDER BY, LIMIT) / Funktionen im SELECT (Single-Row-Functions) / Fallunterscheidungen und NULL (CASE, COALESCE) / Aggregatfunktionen und Gruppierung / Abfragen über mehrere Tabellen (JOIN) / Übungsaufgaben: einfache Abfragen, Aggregatfunktionen, JOIN, Rechnungsdatenbank (gemischt)
Diese Übungsaufgaben gehören zum Artikel SQL: Abfragen über mehrere Tabellen mit JOIN. Wie die Tabellen über Primär- und Fremdschlüssel zusammenhängen, erklärt der Artikel Der logische Datenbankentwurf mit dem Relationenmodell; Gruppieren und Zählen findest Du im Artikel SQL: Aggregierende Funktionen.
Hinweise zur Bearbeitung: Die Aufgaben sind bewusst umfangreich, damit alle typischen Fragestellungen zum Thema vorkommen; die Angabe hinter dem Aufgabentitel nennt die Kompetenzstufe (Stufe 1 Basiskompetenz bis Stufe 4 Expertise). 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 (zum Beispiel JOIN statt INNER JOIN oder Tabellennamen statt Aliasen). In den Datenmodellen sind Primärschlüssel unterstrichen und Fremdschlüssel mit # gekennzeichnet. Heute ist in allen Szenarien der 18.10.2026 (CURDATE()). Wo sich die Schreibweise der Datenbanksysteme unterscheidet, zeigen Tabs die Beispiellösung zusätzlich für SQL Server, PostgreSQL, Oracle und SQLite; in Oracle entfällt außerdem das Schlüsselwort AS vor Tabellenaliasen (FROM artikel a). Am Ende findest Du eine SQL-Kurzreferenz.
Aufgabe 1: Bestell- und Fertigungsübersicht im Fahrradshop (Stufe 2 – Kernkompetenz)
(ca. 34 min Bearbeitungszeit)
Du bist Auszubildende*r bei der CSB-Rad GmbH in Bielefeld, die Fahrräder und Komponenten über einen Onlineshop verkauft und Rahmen und Schlösser bei Partnerbetrieben individuell fertigen lässt. Werkstattleiter Bastian Ludwig steuert die Fertigung und braucht für seine Planung einige Listen aus der Shop-Datenbank. Sie enthält unter anderem folgende Tabellen:
kunde (kunde_id, kundentyp, firma, vorname, nachname, email, …)
adresse (adresse_id, #kunde_id, typ, strasse, hausnr, plz, ort, land)
bestellung (bestellung_id, #kunde_id, bestellt_am, status, zahlungsart, #rechnungsadresse_id, #lieferadresse_id, …)
bestellposition (#bestellung_id, pos_nr, #artikel_id, menge, einzelpreis_netto, rabatt_prozent, #konfiguration_id)
artikel (artikel_id, artikelnr, bezeichnung, #kategorie_id, netto_preis, lagerbestand, …)
kategorie (kategorie_id, bezeichnung, #ober_kategorie_id)
fertigungspartner (partner_id, name, ort, leistung, schnittstelle, …)
fertigungsauftrag (auftrag_id, #bestellung_id, #pos_nr, #partner_id, uebermittelt_am, status, liefertermin, fehlermeldung)
Die Fremdschlüssel rechnungsadresse_id und lieferadresse_id verweisen beide auf adresse. Der Fremdschlüssel (bestellung_id, pos_nr) in fertigungsauftrag verweist auf den zusammengesetzten Primärschlüssel von bestellposition. Derzeit sind 7 Kund*innen und 8 Bestellungen gespeichert.
| bestellung_id | pos_nr | artikel_id | menge | einzelpreis_netto | rabatt_prozent |
|---|---|---|---|---|---|
| 1 | 1 | 3 | 1 | 3780.67 | 0.0 |
| 1 | 2 | 5 | 2 | 62.18 | 0.0 |
| 1 | 3 | 7 | 1 | 75.63 | 0.0 |
| 2 | 1 | 2 | 1 | 2436.13 | 0.0 |
| … | … | … | … | … | … |
| 6 | 1 | 4 | 1 | 840.34 | 0.0 |
| auftrag_id | bestellung_id | pos_nr | partner_id | status | liefertermin |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | in_produktion | 2026-10-30 |
| 2 | 1 | 2 | 2 | geliefert | 2026-09-05 |
| 3 | 3 | 1 | 1 | geliefert | 2026-09-24 |
| 4 | 5 | 1 | 2 | geliefert | 2026-05-20 |
| 5 | 6 | 1 | 1 | fehler | NULL |
a) Zeige alle Bestellungen mit Bestellnummer, Bestellzeitpunkt, Status sowie Nachname, Vorname und Firma der Kund*innen an. Die neueste Bestellung soll oben stehen. Erstelle dazu die SQL-Abfrage. (4 Punkte)
SELECT b.bestellung_id, b.bestellt_am, b.status,
k.nachname, k.vorname, k.firma
FROM bestellung AS b
INNER JOIN kunde AS k ON k.kunde_id = b.kunde_id
ORDER BY b.bestellt_am DESC;Ergebnis:
| bestellung_id | bestellt_am | status | nachname | vorname | firma |
|---|---|---|---|---|---|
| 8 | 2026-10-16 16:20:00 | offen | Kühn | Hans-Jürgen | NULL |
| 6 | 2026-10-12 19:03:00 | bezahlt | Berg | Felix | NULL |
| 7 | 2026-10-01 07:45:00 | storniert | Schäfer | Ingrid | NULL |
| 4 | 2026-09-15 08:30:00 | geliefert | Nowak | Katarzyna | Radhaus Bielefeld-Süd GmbH |
| 3 | 2026-09-02 22:55:00 | versandt | Engel | Mira | NULL |
| 2 | 2026-08-20 10:12:00 | geliefert | Kühn | Hans-Jürgen | NULL |
| 1 | 2026-08-11 21:40:00 | in_fertigung | Okafor-Schmidt | Chiamaka | NULL |
| 5 | 2026-05-02 14:00:00 | geliefert | Hartmann | Michael | Zweckverband Radwege OWL |
Bewertung: Spaltenauswahl 1 P, Verbund der beiden Tabellen 1 P, Join-Bedingung Fremdschlüssel = Primärschlüssel 1 P, ORDER BY … DESC 1 P.
b) Bastian Ludwig hat sich in einem Forum eine Abfrage für dieselbe Liste abgeschaut und angepasst:
ba) MariaDB bricht mit der Meldung Column 'kunde_id' in field list is ambiguous ab. Erläutere die Meldung und korrigiere die Abfrage so, dass sie die Schreibweise mit Komma in der FROM-Klausel beibehält und jede Bestellung genau einmal mit dem passenden Nachnamen ausgibt. (3 Punkte)
bb) Ermittle, wie viele Zeilen die Abfrage geliefert hätte, wenn in der SELECT-Liste nur eindeutige Spaltennamen gestanden hätten, und erläutere, wie diese Zeilen zustande kommen. (2 Punkte)
ba) Die Spalte kunde_id gibt es in beiden Tabellen. Ohne Tabellenname oder Alias davor weiß MariaDB nicht, welche gemeint ist, die Angabe ist mehrdeutig (ambiguous). Außerdem fehlt die Verknüpfungsbedingung. Korrigiert mit qualifizierten Spaltennamen und der Join-Bedingung in WHERE (sogenannter impliziter Join):
SELECT b.bestellung_id, b.kunde_id, k.nachname
FROM bestellung AS b, kunde AS k
WHERE b.kunde_id = k.kunde_id;Das Ergebnis hat 8 Zeilen (eine je Bestellung). Gleichwertig in expliziter Schreibweise: FROM bestellung AS b INNER JOIN kunde AS k ON b.kunde_id = k.kunde_id.
Bewertung: Erläuterung der Mehrdeutigkeit 1 P, qualifizierte Spaltennamen 1 P, Join-Bedingung in WHERE 1 P.
bb) Ohne Verknüpfungsbedingung bildet die Datenbank das kartesische Produkt (Kreuzprodukt): Jede der 8 Bestellungen wird mit jeder der 7 Kund*innen kombiniert, also 8 \cdot 7 = 56 Zeilen. Die meisten davon sind sinnlos, weil Bestellung und Kund*in nicht zusammengehören.
Bewertung: 56 Zeilen 1 P, Erläuterung Kreuzprodukt 1 P.
c) Chiamaka Okafor-Schmidt fragt nach dem Stand ihrer Bestellungen. Bastian kennt ihre Kundennummer nicht, nur den Nachnamen. Erstelle eine SQL-Abfrage, die alle von ihr bestellten Positionen mit Bestellnummer, Positionsnummer, Artikelnummer, Artikelbezeichnung, Kategorie und Menge ausgibt, sortiert nach Bestellung und Position. (6 Punkte)
| bestellung_id | pos_nr | artikelnr | artikel | kategorie | menge |
|---|---|---|---|---|---|
| 1 | 1 | RAD-LR-03 | Lastenrad „Long John“ individuell | Lastenräder | 1 |
| … | … | … | … | … | … |
SELECT b.bestellung_id, bp.pos_nr, a.artikelnr,
a.bezeichnung AS artikel, kat.bezeichnung AS kategorie, bp.menge
FROM kunde AS k
INNER JOIN bestellung AS b ON b.kunde_id = k.kunde_id
INNER JOIN bestellposition AS bp ON bp.bestellung_id = b.bestellung_id
INNER JOIN artikel AS a ON a.artikel_id = bp.artikel_id
INNER JOIN kategorie AS kat ON kat.kategorie_id = a.kategorie_id
WHERE k.nachname = 'Okafor-Schmidt'
ORDER BY b.bestellung_id, bp.pos_nr;Ergebnis:
| bestellung_id | pos_nr | artikelnr | artikel | kategorie | menge |
|---|---|---|---|---|---|
| 1 | 1 | RAD-LR-03 | Lastenrad „Long John“ individuell | Lastenräder | 1 |
| 1 | 2 | SCH-BUE-01 | Bügelschloss, gleichschließend | Schlösser | 2 |
| 1 | 3 | LIC-SET-01 | Lichtset Nabendynamo StVZO | Beleuchtung | 1 |
Bewertung: vier Join-Bedingungen je 0,5 P (2 P), Filter über den Namen statt über die ID 1 P, Spaltenaliase für die beiden Spalten bezeichnung 1 P, Spaltenauswahl 1 P, Sortierung nach zwei Spalten 1 P. Wer kunde_id mit einer Unterabfrage ermittelt, erhält die Punkte für den Filter ebenfalls.
d) Für die Wochenplanung braucht Bastian alle Fertigungsaufträge, die noch nicht geliefert sind.
da) Erstelle eine SQL-Abfrage, die für diese Aufträge die Auftragsnummer, die Bestellnummer, die Bezeichnung des gefertigten Artikels, den Namen des Fertigungspartners, den Status und den Liefertermin ausgibt, sortiert nach Liefertermin. (6 Punkte)
db) Erläutere, was sich am Ergebnis ändert, wenn fertigungsauftrag und bestellposition nur über die Bestellnummer verknüpft werden. (2 Punkte)
da)
SELECT fa.auftrag_id, fa.bestellung_id, a.bezeichnung AS artikel,
fp.name AS partner, fa.status, fa.liefertermin
FROM fertigungsauftrag AS fa
INNER JOIN bestellposition AS bp ON bp.bestellung_id = fa.bestellung_id
AND bp.pos_nr = fa.pos_nr
INNER JOIN artikel AS a ON a.artikel_id = bp.artikel_id
INNER JOIN fertigungspartner AS fp ON fp.partner_id = fa.partner_id
WHERE fa.status <> 'geliefert'
ORDER BY fa.liefertermin;Ergebnis:
| auftrag_id | bestellung_id | artikel | partner | status | liefertermin |
|---|---|---|---|---|---|
| 5 | 6 | Rahmen nach Maß (Stahl, pulverbeschichtet) | Rahmenwerk OWL GmbH | fehler | NULL |
| 1 | 1 | Lastenrad „Long John“ individuell | Rahmenwerk OWL GmbH | in_produktion | 2026-10-30 |
MariaDB sortiert NULL bei aufsteigender Sortierung nach vorn; der fehlerhafte Auftrag steht damit oben, was für die Planung sogar praktisch ist.
Bewertung: Join-Bedingung über beide Schlüsselspalten 2 P, Verbund mit artikel und fertigungspartner je 1 P, Statusfilter 1 P, Spaltenauswahl und Sortierung 1 P.
db) Bestellung 1 hat drei Positionen. Wird nur bestellung_id verglichen, passt Fertigungsauftrag 1 zu allen drei Positionen; er erscheint dreimal, einmal davon mit dem richtigen Lastenrad und zweimal mit Bügelschloss und Lichtset, die gar nicht bei Rahmenwerk OWL gefertigt werden. Bei einem zusammengesetzten Schlüssel müssen in der Join-Bedingung alle Schlüsselspalten verglichen werden.
Bewertung: Vervielfachung der Zeilen 1 P, Begründung mit dem zusammengesetzten Schlüssel 1 P.
e) Für die Versandplanung sollen alle Bestellungen aufgelistet werden, bei denen Rechnungs- und Lieferadresse verschieden sind. Erstelle eine SQL-Abfrage, die die Bestellnummer, den Nachnamen sowie Straße und Ort der Rechnungsadresse und Straße und Ort der Lieferadresse ausgibt. (5 Punkte)
| bestellung_id | nachname | rechnung_strasse | rechnung_ort | liefer_strasse | liefer_ort |
|---|---|---|---|---|---|
| 3 | Engel | Feilenstraße | Bielefeld | Kurierzentrale, Herforder Straße | Bielefeld |
| … | … | … | … | … | … |
Die Tabelle adresse wird zweimal mit unterschiedlichen Aliasen eingebunden, einmal in der Rolle „Rechnungsadresse“ und einmal in der Rolle „Lieferadresse“:
SELECT b.bestellung_id, k.nachname,
ra.strasse AS rechnung_strasse, ra.ort AS rechnung_ort,
la.strasse AS liefer_strasse, la.ort AS liefer_ort
FROM bestellung AS b
INNER JOIN kunde AS k ON k.kunde_id = b.kunde_id
INNER JOIN adresse AS ra ON ra.adresse_id = b.rechnungsadresse_id
INNER JOIN adresse AS la ON la.adresse_id = b.lieferadresse_id
WHERE b.rechnungsadresse_id <> b.lieferadresse_id
ORDER BY b.bestellung_id;Ergebnis:
| bestellung_id | nachname | rechnung_strasse | rechnung_ort | liefer_strasse | liefer_ort |
|---|---|---|---|---|---|
| 3 | Engel | Feilenstraße | Bielefeld | Kurierzentrale, Herforder Straße | Bielefeld |
| 5 | Hartmann | Am Bahnhof | Bielefeld | Betriebshof, Eckendorfer Straße | Bielefeld |
Bewertung: adresse zweimal mit verschiedenen Aliasen 2 P, beide Join-Bedingungen 1 P, Bedingung für abweichende Adressen 1 P, Spaltenaliase 1 P.
f) Für die Lagerplanung ermittelt Bastian die Artikel, von denen in nicht stornierten Bestellungen insgesamt mehr als 5 Stück verkauft wurden. Erstelle die SQL-Abfrage mit Artikelnummer, Bezeichnung und verkaufter Stückzahl, absteigend nach Stückzahl sortiert. (5 Punkte)
SELECT a.artikelnr, a.bezeichnung, SUM(bp.menge) AS stueck
FROM artikel AS a
INNER JOIN bestellposition AS bp ON bp.artikel_id = a.artikel_id
INNER JOIN bestellung AS b ON b.bestellung_id = bp.bestellung_id
WHERE b.status <> 'storniert'
GROUP BY a.artikel_id, a.artikelnr, a.bezeichnung
HAVING SUM(bp.menge) > 5
ORDER BY stueck DESC;Ergebnis:
| artikelnr | bezeichnung | stueck |
|---|---|---|
| LIC-SET-01 | Lichtset Nabendynamo StVZO | 22 |
| SCH-RAH-01 | Rahmenschloss, gleichschließend | 12 |
| RAD-CITY-01 | Stadtrad „Promenade“ 7-Gang | 10 |
Bewertung: Verbund über drei Tabellen 1 P (der Status steht nur in bestellung), WHERE gegen Stornierungen 1 P, SUM 1 P, GROUP BY mit allen nicht aggregierten Spalten 1 P, HAVING und Sortierung 1 P.
g) Die Lipper SchließTec KG erhöht ihre Preise. Erstelle eine SQL-Anweisung, die den Nettopreis aller Artikel der Kategorie „Schlösser“ um 4 % erhöht und auf zwei Nachkommastellen rundet. Die kategorie_id der Schlösser ist nicht bekannt. (4 Punkte)
MariaDB erlaubt einen Verbund direkt in der UPDATE-Anweisung (multi-table UPDATE):
Gleichwertig und in allen Datenbanksystemen lauffähig ist die Variante mit Unterabfrage:
UPDATE artikel
SET netto_preis = ROUND(netto_preis * 1.04, 2)
WHERE kategorie_id = (SELECT kategorie_id FROM kategorie WHERE bezeichnung = 'Schlösser');Betroffen sind 2 Zeilen: Bügelschloss 62.18 → 64.67 und Rahmenschloss 41.18 → 42.83.
Bewertung: UPDATE … SET mit Berechnung 1 P, Verbund oder Unterabfrage zu kategorie 2 P, Filter auf die Bezeichnung 1 P.
Aufgabe 2: Sessionplan und Raumbelegung für ein Barcamp (Stufe 2 – Kernkompetenz)
(ca. 32 min Bearbeitungszeit)
Das Ratz und Rübe Berufskolleg in Bielefeld veranstaltet am 13.03.2027 das Barcamp „Lernen.Digital.Leben 2027“. Du arbeitest als Auszubildende*r im Orga-Team von Informatiklehrer Dimitrios Papadopoulos mit. Emre Aksoy, Leiter des Technikteams, hat schon einige Abfragen geschrieben. Die Orga-Datenbank enthält unter anderem:
person (person_id, vorname, nachname, email, organisation, geburtsjahr)
anmeldung (anmeldung_id, #veranstaltung_id, #person_id, ticketart, angemeldet_am, essen, bedarf_barrierefreiheit, foto_einwilligung, …)
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, format, status, eingereicht_am, max_plaetze, #raum_id, #slot_id, livestream, live_untertitel)
session_person (#session_id, #person_id, rolle)
session_buchung (#session_id, #anmeldung_id, gebucht_am)
geraet (geraet_id, typ, bezeichnung, mac, ip, #vlan_id, #raum_id, standort, leistung_w, poe)
ticketart hat einen der Werte 'Schueler', 'Lehrkraft', 'Extern', 'Referent', 'Orga' oder 'Aussteller'; barrierefrei ist ein Wahrheitswert. Sessions, die noch nicht eingeplant sind, haben in raum_id und slot_id den Wert NULL.
| session_id | titel | status | raum_id | slot_id |
|---|---|---|---|---|
| 1 | KI-Assistenten im Unterricht – was geht, was nicht? | angenommen | 1 | 1 |
| 2 | Reparieren statt wegwerfen: Laptops retten | angenommen | 7 | 2 |
| 3 | Barrierefreie Videos: Untertitel und Gebärdensprache | angenommen | 5 | 3 |
| 4 | Escape-Room mit Mikrocontrollern | angenommen | 6 | 4 |
| 5 | Datenschutz im Schulalltag – Mythen und Fakten | eingereicht | NULL | NULL |
| 6 | Barrierefreie Webseiten mit HTML und ARIA | angenommen | 6 | 5 |
| 7 | Krypto-Trading für Einsteiger | abgelehnt | NULL | NULL |
| 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 |
a) Für die Infomonitore braucht Dimitrios den Sessionplan. Erstelle eine SQL-Abfrage, die für alle angenommenen Sessions Beginn und Ende des Slots, die Raumbezeichnung und den Titel ausgibt, sortiert nach Beginn und bei gleichem Beginn nach Raum. (4 Punkte)
SELECT sl.beginn, sl.ende, r.bezeichnung AS raum, s.titel
FROM session AS s
INNER JOIN slot AS sl ON sl.slot_id = s.slot_id
INNER JOIN raum AS r ON r.raum_id = s.raum_id
WHERE s.status = 'angenommen'
ORDER BY sl.beginn, r.bezeichnung;Ergebnis:
| beginn | ende | raum | titel |
|---|---|---|---|
| 10:00:00 | 10:45:00 | A.001 | KI-Assistenten im Unterricht – was geht, was nicht? |
| 11:00:00 | 11:45:00 | C.110 | Reparieren statt wegwerfen: Laptops retten |
| 12:45:00 | 13:30:00 | B.201 | Barrierefreie Videos: Untertitel und Gebärdensprache |
| 13:45:00 | 14:30:00 | B.202 | Escape-Room mit Mikrocontrollern |
| 14:45:00 | 15:30:00 | B.202 | Barrierefreie Webseiten mit HTML und ARIA |
Bewertung: zwei Join-Bedingungen je 1 P, Statusfilter 1 P, Sortierung nach zwei Spalten 1 P.
b) Dimitrios möchte für die Planungsrunde alle Sessions sehen, die nicht abgelehnt wurden – auch die noch nicht eingeplanten. Er wünscht sich folgendes Ergebnis:
| session_id | titel | status | raum | beginn |
|---|---|---|---|---|
| 1 | KI-Assistenten im Unterricht – was geht, was nicht? | angenommen | A.001 | 10:00:00 |
| … | … | … | … | … |
| 5 | Datenschutz im Schulalltag – Mythen und Fakten | eingereicht | noch offen | NULL |
| 6 | Barrierefreie Webseiten mit HTML und ARIA | angenommen | B.202 | 14:45:00 |
ba) Begründe, warum die Abfrage aus a) die Session 5 auch ohne den Statusfilter nicht liefern würde. (2 Punkte)
bb) Erstelle die SQL-Abfrage für das gewünschte Ergebnis. (4 Punkte)
ba) Ein INNER JOIN liefert nur Zeilen, zu denen in beiden Tabellen ein passender Partner existiert. Session 5 hat in raum_id und slot_id den Wert NULL. Ein Vergleich mit NULL ist nie wahr, daher findet die Join-Bedingung keinen Raum und keinen Slot, und die Session fällt aus dem Ergebnis.
Bewertung: Eigenschaft des inneren Verbunds 1 P, Bezug auf NULL im Fremdschlüssel 1 P.
bb)
SELECT s.session_id, s.titel, s.status,
COALESCE(r.bezeichnung, 'noch offen') AS raum, sl.beginn
FROM session AS s
LEFT JOIN raum AS r ON r.raum_id = s.raum_id
LEFT JOIN slot AS sl ON sl.slot_id = s.slot_id
WHERE s.status <> 'abgelehnt'
ORDER BY s.session_id;Ergebnis:
| session_id | titel | status | raum | beginn |
|---|---|---|---|---|
| 1 | KI-Assistenten im Unterricht – was geht, was nicht? | angenommen | A.001 | 10:00:00 |
| 2 | Reparieren statt wegwerfen: Laptops retten | angenommen | C.110 | 11:00:00 |
| 3 | Barrierefreie Videos: Untertitel und Gebärdensprache | angenommen | B.201 | 12:45:00 |
| 4 | Escape-Room mit Mikrocontrollern | angenommen | B.202 | 13:45:00 |
| 5 | Datenschutz im Schulalltag – Mythen und Fakten | eingereicht | noch offen | NULL |
| 6 | Barrierefreie Webseiten mit HTML und ARIA | angenommen | B.202 | 14:45:00 |
Die Bedingung an den Status darf in WHERE stehen, weil sie die linke Tabelle session betrifft. Der Platzhalter „noch offen“ entsteht mit COALESCE (oder IFNULL).
Bewertung: zwei LEFT JOIN mit session als linker Tabelle 2 P, COALESCE/IFNULL 1 P, Statusfilter und Sortierung 1 P.
c) Emre plant die E-Ink-Türschilder und möchte wissen, wie viele Slots in jedem Raum belegt sind, auch wenn es keine sind. Er hat folgenden Entwurf begonnen:
SELECT r.bezeichnung, r.name, COUNT(______①______) AS belegte_slots
FROM session AS s
______②______ JOIN raum AS r ON ______③______
GROUP BY r.raum_id, r.bezeichnung, r.name
ORDER BY r.bezeichnung;ca) Ergänze die Lücken ① bis ③ so, dass alle acht Räume mit der richtigen Anzahl erscheinen. (4 Punkte)
cb) Formuliere die Abfrage so um, dass sie mit einem LEFT JOIN dasselbe Ergebnis liefert. (2 Punkte)
ca) ① s.session_id ② RIGHT ③ r.raum_id = s.raum_id
SELECT r.bezeichnung, r.name, COUNT(s.session_id) AS belegte_slots
FROM session AS s
RIGHT JOIN raum AS r ON r.raum_id = s.raum_id
GROUP BY r.raum_id, r.bezeichnung, r.name
ORDER BY r.bezeichnung;Ergebnis:
| bezeichnung | name | belegte_slots |
|---|---|---|
| A.001 | Aula | 1 |
| A.104 | NULL | 0 |
| A.105 | NULL | 0 |
| B.012 | Mensa | 0 |
| B.201 | IT-Labor 1 | 1 |
| B.202 | IT-Labor 2 | 2 |
| C.020 | Bibliothek | 0 |
| C.110 | Elektrowerkstatt | 1 |
raum steht rechts vom JOIN, alle Räume sollen erscheinen, also RIGHT JOIN. Gezählt werden muss eine Spalte der Tabelle session: Für einen Raum ohne Session erzeugt der äußere Verbund eine Zeile, in der alle Session-Spalten NULL sind. COUNT(*) zählt diese Zeile mit und liefert 1 statt 0, COUNT(s.session_id) zählt nur Nicht-NULL-Werte.
Bewertung: ① 2 P (mit COUNT(*) 0 P), ② 1 P, ③ 1 P.
cb)
SELECT r.bezeichnung, r.name, COUNT(s.session_id) AS belegte_slots
FROM raum AS r
LEFT JOIN session AS s ON s.raum_id = r.raum_id
GROUP BY r.raum_id, r.bezeichnung, r.name
ORDER BY r.bezeichnung;A RIGHT JOIN B ist gleichwertig zu B LEFT JOIN A: Die Tabelle, aus der alle Zeilen erscheinen sollen, wird nach links getauscht.
Bewertung: Tabellen getauscht 1 P, LEFT JOIN 1 P.
d) Für die Auswertung der Schüler*innen-Beteiligung soll für jede angenommene Session angezeigt werden, wie viele Buchungen von Schüler*innen (Ticketart 'Schueler') es gibt – auch wenn es keine gibt. Emre hat dazu geschrieben:
SELECT s.titel, COUNT(sb.anmeldung_id) AS schueler_buchungen
FROM session AS s
LEFT JOIN session_buchung AS sb ON sb.session_id = s.session_id
LEFT JOIN anmeldung AS a ON a.anmeldung_id = sb.anmeldung_id
WHERE s.status = 'angenommen'
AND a.ticketart = 'Schueler'
GROUP BY s.session_id, s.titel;Seine Abfrage liefert nur drei der fünf angenommenen Sessions.
da) Erläutere, warum die Sessions „KI-Assistenten im Unterricht“ und „Barrierefreie Videos“ fehlen. (2 Punkte)
db) Korrigiere die Abfrage. Achte darauf, dass wirklich nur Buchungen von Schüler*innen gezählt werden. (4 Punkte)
da) Die LEFT JOIN erzeugen zwar auch für Sessions ohne (passende) Buchung eine Zeile, aber darin ist a.ticketart NULL. Die Bedingung a.ticketart = 'Schueler' in WHERE wird erst nach dem Verbund ausgewertet und ist für NULL nicht wahr. Sie wirft diese Zeilen wieder hinaus; der äußere Verbund wirkt dadurch wie ein innerer. Die KI-Session hat gar keine Buchungen, bei den barrierefreien Videos haben nur Lehrkräfte gebucht.
Bewertung: WHERE filtert nach dem Verbund 1 P, NULL-Zeilen fallen weg 1 P.
db) Die Bedingung an die rechte Tabelle gehört in die ON-Klausel, und gezählt wird eine Spalte von anmeldung:
SELECT s.titel, COUNT(a.anmeldung_id) AS schueler_buchungen
FROM session AS s
LEFT JOIN session_buchung AS sb ON sb.session_id = s.session_id
LEFT JOIN anmeldung AS a ON a.anmeldung_id = sb.anmeldung_id
AND a.ticketart = 'Schueler'
WHERE s.status = 'angenommen'
GROUP BY s.session_id, s.titel
ORDER BY s.session_id;Ergebnis:
| titel | schueler_buchungen |
|---|---|
| KI-Assistenten im Unterricht – was geht, was nicht? | 0 |
| Reparieren statt wegwerfen: Laptops retten | 2 |
| Barrierefreie Videos: Untertitel und Gebärdensprache | 0 |
| Escape-Room mit Mikrocontrollern | 2 |
| Barrierefreie Webseiten mit HTML und ARIA | 2 |
Typischer Folgefehler: Wird die Bedingung nach ON verschoben, aber weiter COUNT(sb.anmeldung_id) gezählt, erscheinen zwar alle fünf Sessions, gezählt werden aber auch die Buchungen der Lehrkräfte (Laptops retten 3, Barrierefreie Videos 2). Die Zeile aus session_buchung bleibt ja erhalten, nur die Spalten von anmeldung werden NULL.
Bewertung: Ticket-Bedingung in ON 2 P, COUNT(a.anmeldung_id) 1 P, Statusfilter in WHERE belassen 1 P.
e) Gerhard Vogt, der den Repair-Workshop leitet, ist mit dem Rollator unterwegs und braucht einen Aufzug.
ea) Erstelle eine SQL-Abfrage, die alle Referierenden (alle Rollen in session_person) mit Vor- und Nachname, Sessiontitel, Raumbezeichnung und angegebenem Barrierefreiheitsbedarf auflistet, die einen Bedarf angegeben haben und deren Session in einem nicht barrierefreien Raum stattfindet. Den Bedarf findest Du in der Anmeldung der Person zu derselben Veranstaltung. (5 Punkte)
eb) Dimitrios will die Abfrage künftig vor jeder Änderung am Sessionplan laufen lassen. Nimm Stellung, ob sie damit alle Barrierefreiheitsprobleme der Referierenden zuverlässig findet. (3 Punkte)
ea)
SELECT p.vorname, p.nachname, s.titel, r.bezeichnung AS raum,
an.bedarf_barrierefreiheit
FROM session AS s
INNER JOIN raum AS r ON r.raum_id = s.raum_id
INNER JOIN session_person AS sp ON sp.session_id = s.session_id
INNER JOIN person AS p ON p.person_id = sp.person_id
INNER JOIN anmeldung AS an ON an.person_id = p.person_id
AND an.veranstaltung_id = s.veranstaltung_id
WHERE r.barrierefrei = FALSE
AND an.bedarf_barrierefreiheit IS NOT NULL;Ergebnis:
| vorname | nachname | titel | raum | bedarf_barrierefreiheit |
|---|---|---|---|---|
| Gerhard | Vogt | Reparieren statt wegwerfen: Laptops retten | C.110 | Rollator, Aufzug nötig |
Bewertung: Verbund über die Zwischentabelle session_person 2 P, Verbund mit anmeldung (über die Person, gern zusätzlich über die Veranstaltung) 1 P, Verbund mit raum 1 P, beide Filterbedingungen 1 P. an.person_id = sp.person_id ist ebenso richtig.
eb) Die Abfrage findet nur einen Fall: eine Person mit irgendeinem Bedarf in einem Raum, der als nicht barrierefrei markiert ist. Sie ist daher eine gute Warnung, aber keine vollständige Prüfung:
Der Bedarf ist Freitext. Andere Bedarfe passen nicht zum Merkmal
barrierefrei: Lisa Brandt braucht Live-Untertitel; dafür müsste zusätzlichs.live_untertitelgeprüft werden.Wer keinen Bedarf eingetragen hat, wird nicht erfasst, auch wenn er oder sie einen hat.
Teilnehmende, die eine Session gebucht haben, sind nicht berücksichtigt (dafür ein Verbund über
session_buchung).
Fazit: als automatische Warnung sinnvoll, die persönliche Rückfrage bei den Betroffenen ersetzt sie nicht. Die Sofortmaßnahme für Gerhard Vogt: Workshop in einen barrierefreien Raum verlegen.
Bewertung: zwei begründete Einschränkungen je 1 P, nachvollziehbares Fazit 1 P.
f) Jedes E-Ink-Türschild ist in geraet mit dem Typ 'E-Ink-Tuerschild' erfasst. Seit gestern liegt ein Ersatz-Türschild im Orga-Büro, das noch keinem Raum zugeordnet ist:
INSERT INTO geraet (bezeichnung, typ, mac, ip, vlan_id, raum_id, standort, leistung_w, poe)
VALUES ('TS-RES-1', 'E-Ink-Tuerschild', '24:6F:28:40:09:01', '10.120.0.250', 120,
NULL, 'Lager Orga-Büro A.010', 1, FALSE);Liste alle Räume und alle E-Ink-Türschilder in einer Ergebnistabelle auf, sodass sowohl Räume ohne Türschild als auch Türschilder ohne Raum zu sehen sind. Beachte die SQL-Kurzreferenz in der Anlage. (5 Punkte)
Gesucht ist ein vollständiger äußerer Verbund (FULL OUTER JOIN). MariaDB unterstützt ihn in den aktuellen Versionen nicht, deshalb werden ein LEFT JOIN (alle Räume) und ein RIGHT JOIN (alle Türschilder) mit UNION vereinigt. UNION entfernt dabei die doppelten Zeilen der Räume mit Türschild.
SELECT r.bezeichnung AS raum, g.bezeichnung AS tuerschild
FROM raum AS r
LEFT JOIN geraet AS g ON g.raum_id = r.raum_id
AND g.typ = 'E-Ink-Tuerschild'
UNION
SELECT r.bezeichnung, g.bezeichnung
FROM raum AS r
RIGHT JOIN geraet AS g ON g.raum_id = r.raum_id
WHERE g.typ = 'E-Ink-Tuerschild'
ORDER BY raum;Ergebnis:
| raum | tuerschild |
|---|---|
| NULL | TS-RES-1 |
| A.001 | NULL |
| A.104 | TS-A104 |
| A.105 | TS-A105 |
| B.012 | NULL |
| B.201 | TS-B201 |
| B.202 | NULL |
| C.020 | NULL |
| C.110 | NULL |
Beachte die Stellung der Typ-Bedingung: Im ersten Teil steht sie in ON, damit Räume mit anderen Geräten (zum Beispiel die Aula mit Kamera und Encoder) nicht herausfallen. Im zweiten Teil darf sie in WHERE stehen, weil sie die Tabelle betrifft, aus der alle Zeilen erhalten bleiben sollen.
Bewertung: Erkennen des vollständigen äußeren Verbunds 1 P, LEFT-Teil mit Typ-Bedingung in ON 1 P, RIGHT-Teil (oder getauschter LEFT JOIN) 1 P, UNION 1 P, Ergebnis vollständig 1 P. Ein FULL OUTER JOIN mit Hinweis, dass MariaDB ihn nicht kennt, erhält 3 P.
Aufgabe 3: Buchungen, Matching und Statistik einer Mitfahr-App (Stufe 2 – Kernkompetenz)
(ca. 34 min Bearbeitungszeit)
Die Auszubildenden des Ratz und Rübe Berufskollegs organisieren ihre Fahrgemeinschaften zur Berufsschule mit der App „RatzFahrt“. Du arbeitest im FIA-Projektteam mit Oksana Melnyk und Jonas Wolff. Andrea Wolf ist die Datenschutzbeauftragte der Schule und administriert die App. Ausschnitt aus dem Datenmodell:
nutzer (nutzer_id, pseudonym, vorname, nachname, email_schule, geburtsdatum, #klasse_id, rolle, einwilligung_eltern, fuehrerschein, …)
haltepunkt (haltepunkt_id, name, ort, plz, …, entfernung_km)
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)
fahrer_id und mitfahrer_id verweisen beide auf nutzer. abfahrt, zeit_von und zeit_bis sind Uhrzeiten (TIME), angefragt_am und beantwortet_am Zeitpunkte (DATETIME).
| angebot_id | fahrer_id | haltepunkt_id | richtung | datum | abfahrt | freie_plaetze | status |
|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | zur Schule | 2026-10-19 | 07:00:00 | 2 | offen |
| 2 | 1 | 1 | nach Hause | 2026-10-19 | 14:20:00 | 3 | offen |
| 3 | 6 | 3 | zur Schule | 2026-10-19 | 07:10:00 | 0 | voll |
| 4 | 7 | 2 | zur Schule | 2026-10-20 | 06:50:00 | 3 | offen |
| 5 | 9 | 4 | zur Schule | 2026-10-22 | 06:45:00 | 6 | offen |
| 6 | 5 | 1 | nach Hause | 2026-10-15 | 15:30:00 | 3 | durchgefuehrt |
| 7 | 7 | 2 | nach Hause | 2026-10-14 | 15:20:00 | 4 | abgesagt |
| gesuch_id | nutzer_id | haltepunkt_id | richtung | datum | zeit_von | zeit_bis | status |
|---|---|---|---|---|---|---|---|
| 1 | 2 | 1 | zur Schule | 2026-10-20 | 07:00:00 | 08:00:00 | offen |
| 2 | 3 | 3 | zur Schule | 2026-10-19 | 06:45:00 | 07:30:00 | vermittelt |
| 3 | 4 | 1 | zur Schule | 2026-10-19 | 06:30:00 | 07:15:00 | vermittelt |
| 4 | 8 | 4 | zur Schule | 2026-10-23 | 07:00:00 | 08:00:00 | offen |
| 5 | 4 | 1 | nach Hause | 2026-10-15 | 15:00:00 | 16:00:00 | vermittelt |
| buchung_id | angebot_id | mitfahrer_id | gesuch_id | status | angefragt_am | beantwortet_am |
|---|---|---|---|---|---|---|
| 1 | 3 | 3 | 2 | bestaetigt | 2026-10-14 22:10:00 | 2026-10-14 22:31:00 |
| 2 | 1 | 4 | 3 | bestaetigt | 2026-10-15 19:45:00 | 2026-10-15 20:02:00 |
| 3 | 1 | 8 | NULL | abgelehnt | 2026-10-16 07:00:00 | 2026-10-16 07:40:00 |
| 4 | 6 | 4 | 5 | bestaetigt | 2026-10-13 12:30:00 | 2026-10-13 13:05:00 |
| 5 | 4 | 2 | NULL | angefragt | 2026-10-17 20:15:00 | NULL |
a) In der App sehen Fahrende und Mitfahrende nur Pseudonyme. Erstelle eine SQL-Abfrage für eine Buchungsübersicht mit Buchungsnummer, Datum und Abfahrtszeit des Angebots, Ort des Haltepunkts, Pseudonym der Fahrerin oder des Fahrers, Pseudonym der Mitfahrerin oder des Mitfahrers und Buchungsstatus, sortiert nach Datum und Abfahrtszeit. (6 Punkte)
| buchung_id | datum | abfahrt | ort | fahrer | mitfahrer | status |
|---|---|---|---|---|---|---|
| 4 | 2026-10-15 | 15:30:00 | Gütersloh | Grünpfeil | Bassline | bestaetigt |
| … | … | … | … | … | … | … |
Die Tabelle nutzer wird in zwei Rollen gebraucht und daher zweimal mit verschiedenen Aliasen eingebunden:
SELECT b.buchung_id, an.datum, an.abfahrt, h.ort,
f.pseudonym AS fahrer, m.pseudonym AS mitfahrer, b.status
FROM buchung AS b
INNER JOIN angebot AS an ON an.angebot_id = b.angebot_id
INNER JOIN haltepunkt AS h ON h.haltepunkt_id = an.haltepunkt_id
INNER JOIN nutzer AS f ON f.nutzer_id = an.fahrer_id
INNER JOIN nutzer AS m ON m.nutzer_id = b.mitfahrer_id
ORDER BY an.datum, an.abfahrt;Ergebnis:
| buchung_id | datum | abfahrt | ort | fahrer | mitfahrer | status |
|---|---|---|---|---|---|---|
| 4 | 2026-10-15 | 15:30:00 | Gütersloh | Grünpfeil | Bassline | bestaetigt |
| 2 | 2026-10-19 | 07:00:00 | Gütersloh | Funkenflug | Bassline | bestaetigt |
| 3 | 2026-10-19 | 07:00:00 | Gütersloh | Funkenflug | Rotkehlchen | abgelehnt |
| 1 | 2026-10-19 | 07:10:00 | Halle (Westf.) | Spätzünder | Kaffeetasse | bestaetigt |
| 5 | 2026-10-20 | 06:50:00 | Herford | Ostwind | Morgenmuffel | angefragt |
Die Reihenfolge der Buchungen 2 und 3 (gleiche Abfahrt) ist nicht festgelegt.
Bewertung: Verbund mit angebot und haltepunkt je 1 P, nutzer zweimal mit verschiedenen Aliasen 2 P, richtige Fremdschlüssel (fahrer_id über angebot, mitfahrer_id über buchung) 1 P, Spaltenaliase und Sortierung 1 P.
b) Oksana entwickelt die Vermittlung. Ein Angebot passt zu einem Gesuch, wenn Haltepunkt, Richtung und Datum übereinstimmen und die Abfahrt im Zeitfenster des Gesuchs liegt (Grenzen eingeschlossen).
ba) Erstelle eine SQL-Abfrage, die zu jedem Gesuch die Gesuchsnummer, den Ort des Haltepunkts, Datum und Zeitfenster sowie Nummer, Abfahrt und Status der passenden Angebote ausgibt. Gesuche ohne passendes Angebot sollen ebenfalls erscheinen. (7 Punkte)
bb) Aylin Kaya (Gesuch 1) wundert sich, dass ihr kein Angebot vorgeschlagen wird, obwohl Ben Richter (Angebot 1) ebenfalls ab Gütersloh zur Schule fährt. Begründe das Ergebnis und gib an, welche Bedingung ergänzt werden muss, damit nur noch tatsächlich buchbare Angebote vorgeschlagen werden. (2 Punkte)
ba) Alle Vergleiche gehören in die ON-Klausel des LEFT JOIN, sonst würden Gesuche ohne Treffer herausfallen:
SELECT g.gesuch_id, h.ort, g.datum, g.zeit_von, g.zeit_bis,
an.angebot_id, an.abfahrt, an.status AS angebot_status
FROM gesuch AS g
INNER JOIN haltepunkt AS h ON h.haltepunkt_id = g.haltepunkt_id
LEFT JOIN angebot AS an ON an.haltepunkt_id = g.haltepunkt_id
AND an.richtung = g.richtung
AND an.datum = g.datum
AND an.abfahrt BETWEEN g.zeit_von AND g.zeit_bis
ORDER BY g.gesuch_id;Ergebnis:
| gesuch_id | ort | datum | zeit_von | zeit_bis | angebot_id | abfahrt | angebot_status |
|---|---|---|---|---|---|---|---|
| 1 | Gütersloh | 2026-10-20 | 07:00:00 | 08:00:00 | NULL | NULL | NULL |
| 2 | Halle (Westf.) | 2026-10-19 | 06:45:00 | 07:30:00 | 3 | 07:10:00 | voll |
| 3 | Gütersloh | 2026-10-19 | 06:30:00 | 07:15:00 | 1 | 07:00:00 | offen |
| 4 | Bad Salzuflen | 2026-10-23 | 07:00:00 | 08:00:00 | NULL | NULL | NULL |
| 5 | Gütersloh | 2026-10-15 | 15:00:00 | 16:00:00 | 6 | 15:30:00 | durchgefuehrt |
Statt BETWEEN geht auch an.abfahrt >= g.zeit_von AND an.abfahrt <= g.zeit_bis.
Bewertung: LEFT JOIN mit gesuch als linker Tabelle 2 P, Join-Bedingung über Haltepunkt, Richtung und Datum 2 P, Zeitfenster 2 P, Verbund mit haltepunkt und Spaltenauswahl 1 P.
bb) Ben Richter fährt am 19.10., Aylin sucht für den 20.10.; die Datumsbedingung ist nicht erfüllt, daher gibt es keinen Treffer. Für buchbare Vorschläge muss zusätzlich AND an.status = 'offen' (und gegebenenfalls AND an.freie_plaetze > 0) in die ON-Klausel. Dann bleibt nur noch Angebot 1 für Gesuch 3 übrig.
Bewertung: Begründung über das Datum 1 P, Statusbedingung in ON 1 P.
c) Die Schülervertretung möchte wissen, wie schnell Fahrende auf Anfragen reagieren. Erstelle eine SQL-Abfrage, die für jede beantwortete Buchung die Buchungsnummer, das Pseudonym der Fahrerin oder des Fahrers, den Status und die Antwortzeit in Minuten ausgibt, die längste Antwortzeit zuerst. (5 Punkte)
SELECT b.buchung_id, f.pseudonym AS fahrer, b.status,
TIMESTAMPDIFF(MINUTE, b.angefragt_am, b.beantwortet_am) AS antwortzeit_min
FROM buchung AS b
INNER JOIN angebot AS an ON an.angebot_id = b.angebot_id
INNER JOIN nutzer AS f ON f.nutzer_id = an.fahrer_id
WHERE b.beantwortet_am IS NOT NULL
ORDER BY antwortzeit_min DESC;Ergebnis:
| buchung_id | fahrer | status | antwortzeit_min |
|---|---|---|---|
| 3 | Funkenflug | abgelehnt | 40 |
| 4 | Grünpfeil | bestaetigt | 35 |
| 1 | Spätzünder | bestaetigt | 21 |
| 2 | Funkenflug | bestaetigt | 17 |
Die Fahrerin bzw. der Fahrer steht nicht in buchung, sondern im zugehörigen Angebot; deshalb führt der Weg über angebot zu nutzer.
Bewertung: Verbund über angebot zu nutzer 2 P, TIMESTAMPDIFF(MINUTE, …) mit richtiger Reihenfolge 1 P, Filter auf beantwortete Buchungen 1 P, Sortierung 1 P.
d) Ermittle mit einer SQL-Abfrage alle Haltepunkte (Nummer, Name, Ort), die bisher weder in einem Angebot noch in einem Gesuch verwendet wurden. Verwende dazu keine Unterabfrage. (4 Punkte)
Ein Anti-Join: Mit LEFT JOIN bleiben alle Haltepunkte erhalten; wo es keinen Partner gibt, sind die Schlüssel der rechten Tabellen NULL.
SELECT h.haltepunkt_id, h.name, h.ort
FROM haltepunkt AS h
LEFT JOIN angebot AS an ON an.haltepunkt_id = h.haltepunkt_id
LEFT JOIN gesuch AS g ON g.haltepunkt_id = h.haltepunkt_id
WHERE an.angebot_id IS NULL
AND g.gesuch_id IS NULL;Ergebnis:
| haltepunkt_id | name | ort |
|---|---|---|
| 5 | Kirchplatz | Werther (Westf.) |
| 6 | Bahnhof Brackwede | Bielefeld |
Hier muss die Bedingung in WHERE stehen: Gefiltert wird gerade nach den Zeilen, für die der äußere Verbund keinen Partner gefunden hat. Gleichwertig mit Unterabfragen: WHERE NOT EXISTS (SELECT * FROM angebot …) AND NOT EXISTS (SELECT * FROM gesuch …).
Bewertung: zwei LEFT JOIN 2 P, beide IS NULL-Bedingungen auf Schlüsselspalten 2 P.
e) Jonas möchte für die Monatsstatistik je Fahrer*in die Anzahl der Angebote und die Anzahl der bestätigten Mitfahrten ausgeben. Seine Abfrage:
SELECT n.pseudonym,
COUNT(an.angebot_id) AS angebote,
COUNT(b.buchung_id) AS mitfahrten
FROM nutzer AS n
INNER JOIN angebot AS an ON an.fahrer_id = n.nutzer_id
LEFT JOIN buchung AS b ON b.angebot_id = an.angebot_id
GROUP BY n.nutzer_id, n.pseudonym
ORDER BY n.pseudonym;liefert:
| pseudonym | angebote | mitfahrten |
|---|---|---|
| Funkenflug | 3 | 2 |
| Grünpfeil | 1 | 1 |
| Ostwind | 2 | 1 |
| Spätzünder | 1 | 1 |
| Turbo | 1 | 0 |
ea) Funkenflug (Ben Richter) hat aber nur zwei Angebote eingestellt. Erläutere anhand der Zwischenergebnisse des Verbunds, wie die falschen Werte zustande kommen. (3 Punkte)
eb) Korrigiere die Abfrage. (4 Punkte)
ec) Oksana schlägt vor, die beiden Werte stattdessen mit zwei Unterabfragen im SELECT zu ermitteln, zum Beispiel (SELECT COUNT(*) FROM angebot WHERE fahrer_id = n.nutzer_id) AS angebote. Beurteile den Vorschlag. (2 Punkte)
ea) Vor dem Gruppieren erzeugt der Verbund für Funkenflug diese Zeilen:
| pseudonym | angebot_id | buchung_id | status |
|---|---|---|---|
| Funkenflug | 1 | 2 | bestaetigt |
| Funkenflug | 1 | 3 | abgelehnt |
| Funkenflug | 2 | NULL | NULL |
Angebot 1 hat zwei Buchungen und erscheint deshalb zweimal. COUNT(an.angebot_id) zählt Zeilen mit Angebotsnummer, also 3 statt 2 (Vervielfachung durch den 1:n-Verbund, englisch fan-out). Außerdem zählt COUNT(b.buchung_id) alle Buchungen, auch die abgelehnte Buchung 3 und bei Ostwind die nur angefragte Buchung 5.
Bewertung: Vervielfachung der Angebotszeile 2 P, fehlender Statusfilter 1 P.
eb)
SELECT n.pseudonym,
COUNT(DISTINCT an.angebot_id) AS angebote,
COUNT(b.buchung_id) AS mitfahrten
FROM nutzer AS n
INNER JOIN angebot AS an ON an.fahrer_id = n.nutzer_id
LEFT JOIN buchung AS b ON b.angebot_id = an.angebot_id
AND b.status = 'bestaetigt'
GROUP BY n.nutzer_id, n.pseudonym
ORDER BY n.pseudonym;Ergebnis:
| pseudonym | angebote | mitfahrten |
|---|---|---|
| Funkenflug | 2 | 1 |
| Grünpfeil | 1 | 1 |
| Ostwind | 2 | 0 |
| Spätzünder | 1 | 1 |
| Turbo | 1 | 0 |
Die Statusbedingung muss in ON stehen; in WHERE würden Fahrer*innen ohne bestätigte Mitfahrt (Ostwind, Turbo) verschwinden.
Bewertung: COUNT(DISTINCT …) 2 P, Statusbedingung in ON 2 P (in WHERE 1 P).
ec) Der Vorschlag ist richtig und vermeidet die Vervielfachung, weil jede Unterabfrage für sich zählt. Er ist bei mehreren unabhängigen 1:n-Beziehungen oft die übersichtlichere Lösung. Nachteile: Die Unterabfrage wird je Zeile ausgewertet (bei großen Datenmengen ggf. langsamer, moderne Optimierer gleichen das teils aus), und für jede Kennzahl entsteht eine eigene Unterabfrage. Bei zwei Kennzahlen wie hier sind beide Lösungen vertretbar.
Bewertung: Wirkung (keine Vervielfachung) 1 P, begründete Abwägung 1 P.
f) Lina Hartwig (17 Jahre) hat sich ohne Einwilligung ihrer Eltern registriert und eine Mitfahrt angefragt. Andrea Wolf legt fest: Buchungsanfragen von minderjährigen Nutzenden ohne eingetragene Einwilligung werden gelöscht. Erstelle eine SQL-Anweisung, die alle betroffenen Buchungen löscht, ohne die Nutzer-IDs zu kennen. Das Alter am heutigen Tag erhältst Du mit TIMESTAMPDIFF(YEAR, geburtsdatum, CURDATE()). (4 Punkte)
MariaDB erlaubt einen Verbund in der DELETE-Anweisung (multi-table DELETE). Hinter DELETE steht die Tabelle (bzw. ihr Alias), aus der gelöscht wird:
Gleichwertig mit Unterabfrage:
DELETE FROM buchung
WHERE mitfahrer_id IN (SELECT nutzer_id
FROM nutzer
WHERE TIMESTAMPDIFF(YEAR, geburtsdatum, CURDATE()) < 18
AND einwilligung_eltern IS NULL);Gelöscht wird eine Zeile: Buchung 3 von Rotkehlchen (Lina Hartwig). Ben Richter ist seit dem 17.01.2026 volljährig und daher nicht betroffen, Aylin Kaya hat eine Einwilligung. Vor dem Löschen empfiehlt sich dieselbe Abfrage als SELECT b.* zur Kontrolle.
Bewertung: DELETE mit Angabe der Zieltabelle 1 P, Verbund oder Unterabfrage zu nutzer 1 P, Altersbedingung 1 P, IS NULL für die Einwilligung 1 P.
Aufgabe 4: Aktivitäten einer Freizeit-App auswerten (Stufe 3 – Profilkompetenz)
(ca. 29 min Bearbeitungszeit)
Du bist Auszubildende*r bei der Let’s Meet GmbH in Bielefeld. Die App bringt Menschen für gemeinsame Freizeitaktivitäten zusammen. Für das Quartalsgespräch möchte Geschäftsführerin Clara Neumann wissen, wie die Aktivitäten angenommen werden. UX-Designerin Anh Nguyen arbeitet zusätzlich mit exportierten CSV-Dateien in Python. Ausschnitt aus dem Datenmodell:
benutzer (benutzer_id, vorname, nachname, email, geburtsdatum, plz, ort, status, …)
hobby (hobby_id, bezeichnung, kategorie)
treffpunkt (treffpunkt_id, name, strasse, plz, ort, barrierefrei, …)
aktivitaet (aktivitaet_id, titel, #hobby_id, #ersteller_id, #treffpunkt_id, beginn, dauer_min, max_teilnehmende, mindestalter, status, erstellt_am)
teilnahme (#aktivitaet_id, #benutzer_id, angemeldet_am, status)
bewertung (bewertung_id, #aktivitaet_id, #bewerter_id, sterne, kommentar, erstellt_am)
aktivitaet.status ist geplant, abgesagt oder stattgefunden; teilnahme.status ist angemeldet, warteliste, abgemeldet, teilgenommen oder nicht_erschienen. bewerter_id verweist auf benutzer; bewerten kann nur, wer teilgenommen hat. beginn und angemeldet_am sind Zeitpunkte (DATETIME).
| aktivitaet_id | titel | hobby_id | treffpunkt_id | beginn | status |
|---|---|---|---|---|---|
| 1 | Brettspielabend: Kooperative Spiele | 6 | 1 | 2026-09-25 19:00:00 | stattgefunden |
| 2 | Sonntagswanderung Teutoburger Wald (12 km) | 1 | 2 | 2026-10-04 10:00:00 | stattgefunden |
| 3 | Feierabend-Kick | 4 | 3 | 2026-10-21 18:30:00 | geplant |
| 4 | Sprachtandem Deutsch–Arabisch | 5 | 4 | 2026-10-23 17:00:00 | geplant |
| 5 | Bouldern für Einsteiger*innen | 11 | 5 | 2026-10-24 14:00:00 | geplant |
| 6 | Schach im Park | 19 | 6 | 2026-09-27 15:00:00 | abgesagt |
| 7 | Fotospaziergang Sparrenburg | 2 | 4 | 2026-10-31 15:30:00 | geplant |
| 8 | Kochabend: Mezze mit Weinbegleitung | 3 | 7 | 2026-10-17 18:00:00 | stattgefunden |
| bewertung_id | aktivitaet_id | bewerter_id | sterne |
|---|---|---|---|
| 1 | 1 | 9 | 5 |
| 2 | 1 | 5 | 4 |
| 3 | 2 | 6 | 5 |
| 4 | 2 | 10 | 4 |
| 5 | 8 | 2 | 5 |
| 6 | 8 | 6 | 4 |
a) Clara Neumann ist keine Informatikerin. Erkläre ihr das Konzept hinter einem JOIN und den Unterschied zwischen einem inneren und einem linken äußeren Verbund am Beispiel der Tabellen aktivitaet und bewertung. (4 Punkte)
Konzept (2 Punkte): Die Daten stehen in getrennten Tabellen. Ein JOIN fügt die Datensätze mehrerer Tabellen zu einer Ergebniszeile zusammen; welche zusammengehören, bestimmt die Verbundbedingung, hier bewertung.aktivitaet_id = aktivitaet.aktivitaet_id (Fremdschlüssel gleich Primärschlüssel). So steht neben jeder Bewertung der Titel der Aktivität.
Unterschied (2 Punkte): Der innere Verbund (INNER JOIN) liefert nur Aktivitäten, zu denen es mindestens eine Bewertung gibt – hier nur die Aktivitäten 1, 2 und 8, jede zweimal. Der linke äußere Verbund (aktivitaet LEFT JOIN bewertung) liefert zusätzlich alle Aktivitäten ohne Bewertung (3 bis 7) mit NULL in den Spalten von bewertung.
b) Erstelle eine SQL-Abfrage, die für alle nicht abgesagten Aktivitäten den Titel, die Bezeichnung des Hobbys, die Anzahl der Bewertungen und den Durchschnitt der Sterne (eine Nachkommastelle) ausgibt. Aktivitäten ohne Bewertung sollen mit der Anzahl 0 erscheinen. Sortiere nach dem Durchschnitt absteigend und bei Gleichstand nach dem Titel. (6 Punkte)
SELECT a.titel, h.bezeichnung AS hobby,
COUNT(b.bewertung_id) AS bewertungen,
ROUND(AVG(b.sterne), 1) AS durchschnitt
FROM aktivitaet AS a
INNER JOIN hobby AS h ON h.hobby_id = a.hobby_id
LEFT JOIN bewertung AS b ON b.aktivitaet_id = a.aktivitaet_id
WHERE a.status <> 'abgesagt'
GROUP BY a.aktivitaet_id, a.titel, h.bezeichnung
ORDER BY durchschnitt DESC, a.titel;| titel | hobby | bewertungen | durchschnitt |
|---|---|---|---|
| Brettspielabend: Kooperative Spiele | Brettspiele | 2 | 4.5 |
| Kochabend: Mezze mit Weinbegleitung | Kochen | 2 | 4.5 |
| Sonntagswanderung Teutoburger Wald (12 km) | Wandern | 2 | 4.5 |
| Bouldern für Einsteiger*innen | Klettern | 0 | NULL |
| Feierabend-Kick | Fußball | 0 | NULL |
| Fotospaziergang Sparrenburg | Fotografie | 0 | NULL |
| Sprachtandem Deutsch–Arabisch | Sprachtandem | 0 | NULL |
Bewertung: Verbund mit hobby (1 Punkt), LEFT JOIN auf bewertung (1 Punkt), COUNT(b.bewertung_id) statt COUNT(*) (1 Punkt), AVG mit ROUND (1 Punkt), WHERE und GROUP BY (1 Punkt), Sortierung (1 Punkt). Mit COUNT(*) stünde bei Aktivitäten ohne Bewertung fälschlich 1. NULL steht bei absteigender Sortierung in MariaDB am Ende.
c) Für das Jugendschutzkonzept will Clara wissen, an welchen Treffpunkten schon Aktivitäten mit minderjährigen Teilnehmenden stattfanden oder geplant sind. Erstelle eine SQL-Abfrage, die Name und Ort dieser Treffpunkte jeweils nur einmal ausgibt. Berücksichtige Teilnahmen mit dem Status angemeldet oder teilgenommen; minderjährig ist, wer zu Beginn der Aktivität noch keine 18 Jahre alt ist. (5 Punkte)
SELECT DISTINCT tp.name, tp.ort
FROM treffpunkt AS tp
INNER JOIN aktivitaet AS a ON a.treffpunkt_id = tp.treffpunkt_id
INNER JOIN teilnahme AS t ON t.aktivitaet_id = a.aktivitaet_id
INNER JOIN benutzer AS u ON u.benutzer_id = t.benutzer_id
WHERE t.status IN ('angemeldet', 'teilgenommen')
AND TIMESTAMPDIFF(YEAR, u.geburtsdatum, a.beginn) < 18;| name | ort |
|---|---|
| Spielecafé Würfelglück | Bielefeld |
Bewertung: Verbund über vier Tabellen (2 Punkte), Statusbedingung (1 Punkt), Altersbedingung zum Beginn der Aktivität (1 Punkt), DISTINCT (1 Punkt). Ohne DISTINCT erschiene ein Treffpunkt so oft, wie dort Minderjährige teilnehmen. Gleichwertig: WHERE EXISTS (…) mit einer Unterabfrage. In anderen Datenbanksystemen wird das Alter anders berechnet (z. B. DATEDIFF(YEAR, …) in SQL Server zählt nur Jahreswechsel und ist deshalb ungenau).
d) Clara vermutet, dass sich Menschen für Sportangebote kurzfristiger anmelden als für andere Aktivitäten. Erstelle eine SQL-Abfrage, die je Hobby-Kategorie die Anzahl der Anmeldungen (Status angemeldet oder teilgenommen) und den durchschnittlichen Vorlauf in Stunden zwischen Anmeldung und Beginn der Aktivität (eine Nachkommastelle) ausgibt, sortiert nach dem Vorlauf absteigend. (6 Punkte)
SELECT h.kategorie, COUNT(*) AS anmeldungen,
ROUND(AVG(TIMESTAMPDIFF(MINUTE, t.angemeldet_am, a.beginn)) / 60, 1) AS vorlauf_h
FROM teilnahme AS t
INNER JOIN aktivitaet AS a ON a.aktivitaet_id = t.aktivitaet_id
INNER JOIN hobby AS h ON h.hobby_id = a.hobby_id
WHERE t.status IN ('angemeldet', 'teilgenommen')
GROUP BY h.kategorie
ORDER BY vorlauf_h DESC;| kategorie | anmeldungen | vorlauf_h |
|---|---|---|
| Kreativ | 2 | 364.8 |
| Essen | 3 | 361.9 |
| Spiele | 4 | 317.1 |
| Natur | 3 | 313.1 |
| Sprache | 2 | 256.0 |
| Sport | 5 | 237.8 |
Bewertung: Verbund über drei Tabellen (2 Punkte), Zeitdifferenz in Minuten bzw. Stunden (1 Punkt), AVG und Umrechnung mit ROUND (1 Punkt), Statusbedingung und GROUP BY (1 Punkt), Sortierung (1 Punkt). Die Vermutung stimmt für diesen Datenbestand: Sportangebote haben mit rund 238 Stunden (knapp zehn Tagen) den kürzesten Vorlauf. TIMESTAMPDIFF(HOUR, …) ist auch richtig, schneidet aber jede Differenz auf volle Stunden ab.
e) Anh Nguyen hat die Tabellen teilnahme und bewertung als CSV-Dateien exportiert. Sie möchte alle Teilnahmen mit der Bewertung derselben Person für dieselbe Aktivität zusammenführen und anschließend die gut bewerteten Teilnahmen herausfiltern. Ihr Skript:
1 import pandas as pd
2 teilnahme = pd.read_csv("teilnahme.csv") # aktivitaet_id, benutzer_id, angemeldet_am, status
3 bewertung = pd.read_csv("bewertung.csv") # bewertung_id, aktivitaet_id, bewerter_id, sterne, kommentar
4 daten = pd.merge(teilnahme, bewertung, on="aktivitaet_id", how="inner")
5 gut = daten[daten.sterne >= 4 and daten.status == "teilgenommen"]Das Ergebnis von Zeile 4 hat 24 Zeilen, obwohl es nur 23 Teilnahmen gibt; Zeile 5 bricht mit einem ValueError ab.
ea) Erläutere zwei Fehler in Zeile 4. (4 Punkte)
eb) Korrigiere die Zeilen 4 und 5. (4 Punkte)
ec) Gib die SQL-Abfrage an, die der korrigierten Zeile 4 entspricht. (3 Punkte)
ea) Je Fehler 2 Punkte:
Verbunden wird nur über
aktivitaet_id. Damit wird jede Teilnahme mit jeder Bewertung derselben Aktivität kombiniert, auch mit Bewertungen anderer Personen (Aktivität 1: fünf Teilnahmen × zwei Bewertungen = zehn Zeilen). Die Bewertung gehört zur Person: Zusätzlich mussbenutzer_idmitbewerter_idübereinstimmen – die Spalten heißen verschieden, deshalbleft_on/right_on.how="inner"lässt alle Teilnahmen ohne Bewertung wegfallen. Für „alle Teilnahmen“ ist ein linker Verbund (how="left") nötig.
eb) Zeile 4 (2 Punkte), Zeile 5 (2 Punkte):
4 daten = pd.merge(teilnahme, bewertung,
left_on=["aktivitaet_id", "benutzer_id"],
right_on=["aktivitaet_id", "bewerter_id"], how="left")
5 gut = daten[(daten.sterne >= 4) & (daten.status == "teilgenommen")]Bedingungen auf DataFrame-Spalten werden mit & (bzw. |) verknüpft, jede in eigenen Klammern; and versucht, eine ganze Spalte als einen einzigen Wahrheitswert zu deuten, und löst den ValueError aus. daten hat danach 23 Zeilen, gut sechs (alle sechs Bewertungen haben mindestens vier Sterne).
ec) (3 Punkte)
Aufgabe 5: Lieferzeiten eines Fahrrad-Onlineshops analysieren (Stufe 3 – Profilkompetenz)
(ca. 25 min Bearbeitungszeit)
Du bist Auszubildende*r bei der CSB-Rad GmbH in Bielefeld. Kund*innen haben sich über lange Lieferzeiten beschwert. Geschäftsführer Cem Sarıkaya bittet Dich, die Bestell- und Versanddaten auszuwerten, bevor er mit den Dienstleistern spricht. Ausschnitt aus dem Datenmodell:
kunde (kunde_id, kundentyp, firma, vorname, nachname, email, …)
bestellung (bestellung_id, #kunde_id, bestellt_am, status, zahlungsart, …)
bestellposition (#bestellung_id, pos_nr, #artikel_id, menge, einzelpreis_netto, …)
artikel (artikel_id, artikelnr, bezeichnung, …)
rechnung (rechnung_id, rechnungsnr, #bestellung_id, rechnungsdatum, …)
versand (versand_id, #bestellung_id, dienstleister, sendungsnr, versandt_am, zugestellt_am)
kundentyp ist privat, haendler oder oeffentlich; firma ist nur bei Händlern und öffentlichen Auftraggebern gefüllt. bestellt_am, versandt_am und zugestellt_am sind Zeitpunkte (DATETIME); zugestellt_am bleibt leer, solange die Sendung unterwegs ist.
| bestellung_id | kunde_id | bestellt_am | status |
|---|---|---|---|
| 1 | 1 | 2026-08-11 21:40:00 | in_fertigung |
| 2 | 2 | 2026-08-20 10:12:00 | geliefert |
| 3 | 3 | 2026-09-02 22:55:00 | versandt |
| 4 | 4 | 2026-09-15 08:30:00 | geliefert |
| 5 | 5 | 2026-05-02 14:00:00 | geliefert |
| 6 | 6 | 2026-10-12 19:03:00 | bezahlt |
| 7 | 7 | 2026-10-01 07:45:00 | storniert |
| 8 | 2 | 2026-10-16 16:20:00 | offen |
| versand_id | bestellung_id | dienstleister | sendungsnr | versandt_am | zugestellt_am |
|---|---|---|---|---|---|
| 1 | 2 | Spedition | SP-88412-BI | 2026-08-26 13:00:00 | 2026-08-27 10:40:00 |
| 2 | 3 | Paket | 00340434160000123456 | 2026-09-25 15:30:00 | NULL |
| 3 | 4 | Spedition | SP-90133-BI | 2026-09-17 09:00:00 | 2026-09-18 11:20:00 |
| 4 | 5 | Spedition | SP-71002-BI | 2026-05-20 12:00:00 | 2026-05-21 08:55:00 |
a) Erstelle eine SQL-Abfrage, die für alle zugestellten Sendungen die Bestellnummer, den Dienstleister und die Lieferdauer in Tagen vom Bestellzeitpunkt bis zur Zustellung (eine Nachkommastelle) ausgibt, die längste Lieferdauer zuerst. (5 Punkte)
| bestellung_id | dienstleister | lieferdauer_tage |
|---|---|---|
| 5 | Spedition | 18.8 |
| 2 | Spedition | 7.0 |
| 4 | Spedition | 3.1 |
Bewertung: Verbund (1 Punkt), Differenz zwischen den Zeitpunkten aus beiden Tabellen (2 Punkte), Umrechnung in Tage und Runden (1 Punkt), Bedingung und Sortierung (1 Punkt). Die lange Lieferdauer von Bestellung 5 entsteht vor dem Versand (Beschaffung von zwölf gleichschließenden Rahmenschlössern), nicht beim Transport – das zeigt erst der Vergleich mit versandt_am.
b) Erstelle eine SQL-Abfrage, die je Kundentyp die Anzahl der zugestellten Lieferungen und die durchschnittliche Lieferdauer in Tagen (eine Nachkommastelle) ausgibt, die kürzeste zuerst. (5 Punkte)
SELECT k.kundentyp, COUNT(*) AS lieferungen,
ROUND(AVG(TIMESTAMPDIFF(MINUTE, b.bestellt_am, v.zugestellt_am)) / 1440, 1) AS durchschnitt_tage
FROM kunde AS k
INNER JOIN bestellung AS b ON b.kunde_id = k.kunde_id
INNER JOIN versand AS v ON v.bestellung_id = b.bestellung_id
WHERE v.zugestellt_am IS NOT NULL
GROUP BY k.kundentyp
ORDER BY durchschnitt_tage;| kundentyp | lieferungen | durchschnitt_tage |
|---|---|---|
| haendler | 1 | 3.1 |
| privat | 1 | 7.0 |
| oeffentlich | 1 | 18.8 |
Bewertung: Verbund über drei Tabellen (2 Punkte), AVG über die Zeitdifferenz mit Umrechnung (2 Punkte), GROUP BY und Sortierung (1 Punkt). Bei nur einer Lieferung je Kundentyp ist der Durchschnitt wenig aussagekräftig – ein Hinweis darauf, dass für die Auswertung mehr Daten nötig sind.
c) Für das Gespräch braucht Cem eine Übersicht aller nicht stornierten Bestellungen mit Bestellnummer, Kundenname (bei Firmen der Firmenname, sonst der Nachname), Status, Rechnungsnummer, Dienstleister und Zustellzeitpunkt. Bestellungen ohne Rechnung oder ohne Versand sollen ebenfalls erscheinen. Erstelle die SQL-Abfrage, sortiert nach Bestellnummer. (6 Punkte)
SELECT b.bestellung_id, COALESCE(k.firma, k.nachname) AS kunde, b.status,
r.rechnungsnr, v.dienstleister, v.zugestellt_am
FROM bestellung AS b
INNER JOIN kunde AS k ON k.kunde_id = b.kunde_id
LEFT JOIN rechnung AS r ON r.bestellung_id = b.bestellung_id
LEFT JOIN versand AS v ON v.bestellung_id = b.bestellung_id
WHERE b.status <> 'storniert'
ORDER BY b.bestellung_id;| bestellung_id | kunde | status | rechnungsnr | dienstleister | zugestellt_am |
|---|---|---|---|---|---|
| 1 | Okafor-Schmidt | in_fertigung | NULL | NULL | NULL |
| 2 | Kühn | geliefert | RE-2026-00412 | Spedition | 2026-08-27 10:40:00 |
| 3 | Engel | versandt | RE-2026-00431 | Paket | NULL |
| 4 | Radhaus Bielefeld-Süd GmbH | geliefert | RE-2026-00457 | Spedition | 2026-09-18 11:20:00 |
| 5 | Zweckverband Radwege OWL | geliefert | RE-2026-00233 | Spedition | 2026-05-21 08:55:00 |
| 6 | Berg | bezahlt | RE-2026-00499 | NULL | NULL |
| 8 | Kühn | offen | NULL | NULL | NULL |
Bewertung: innerer Verbund mit kunde (1 Punkt), je ein LEFT JOIN für rechnung und versand (2 Punkte), COALESCE (1 Punkt), Bedingung (1 Punkt), Sortierung (1 Punkt). Mit INNER JOIN auf rechnung oder versand fielen die Bestellungen 1, 6 und 8 bzw. 1 und 8 weg. Hätte eine Bestellung mehrere Sendungen, erschiene sie entsprechend oft.
d) Erstelle eine SQL-Abfrage, die die Artikelnummer und Bezeichnung aller Artikel ausgibt, die schon in nicht stornierten Bestellungen öffentlicher Auftraggeber enthalten waren – jeden Artikel nur einmal, sortiert nach Artikelnummer. (4 Punkte)
SELECT DISTINCT a.artikelnr, a.bezeichnung
FROM kunde AS k
INNER JOIN bestellung AS b ON b.kunde_id = k.kunde_id
INNER JOIN bestellposition AS p ON p.bestellung_id = b.bestellung_id
INNER JOIN artikel AS a ON a.artikel_id = p.artikel_id
WHERE k.kundentyp = 'oeffentlich' AND b.status <> 'storniert'
ORDER BY a.artikelnr;| artikelnr | bezeichnung |
|---|---|
| RAD-CITY-01 | Stadtrad „Promenade“ 7-Gang |
| SCH-RAH-01 | Rahmenschloss, gleichschließend |
Bewertung: Verbund über vier Tabellen (2 Punkte), Bedingungen und Sortierung (1 Punkt), DISTINCT (1 Punkt). Gleichwertig ist WHERE EXISTS (…) bzw. WHERE a.artikel_id IN (SELECT …).
e) Erstelle eine SQL-Abfrage, die alle Sendungen ermittelt, die heute (siehe Hinweise zur Bearbeitung) seit mehr als fünf Tagen unterwegs und noch nicht zugestellt sind. Auszugeben sind Bestellnummer, Vor- und Nachname der Kundin bzw. des Kunden, Sendungsnummer und die Anzahl der vollen Tage seit dem Versand. (4 Punkte)
SELECT b.bestellung_id, k.vorname, k.nachname, v.sendungsnr,
TIMESTAMPDIFF(DAY, v.versandt_am, CURDATE()) AS tage_unterwegs
FROM versand AS v
INNER JOIN bestellung AS b ON b.bestellung_id = v.bestellung_id
INNER JOIN kunde AS k ON k.kunde_id = b.kunde_id
WHERE v.zugestellt_am IS NULL
AND v.versandt_am < CURDATE() - INTERVAL 5 DAY;| bestellung_id | vorname | nachname | sendungsnr | tage_unterwegs |
|---|---|---|---|---|
| 3 | Mira | Engel | 00340434160000123456 | 22 |
Bewertung: Verbund über drei Tabellen (1 Punkt), IS NULL (1 Punkt), Tagesdifferenz zum heutigen Datum als Ausgabe und Bedingung (2 Punkte). Ein Vergleich zugestellt_am = NULL wäre nie wahr. Die Ergebnistabelle gilt für den in den Hinweisen festgelegten Stichtag 18.10.2026 (CURDATE()). In SQL Server zählt DATEDIFF(DAY, …) die überschrittenen Tagesgrenzen und liefert deshalb 23.
f) Natalia Wróbel aus der Buchhaltung schlägt vor, die Tabellen künftig als CSV-Dateien zu exportieren und in der Tabellenkalkulation mit einer Verweisfunktion (z. B. XVERWEIS) zusammenzuführen, „dann brauchen wir kein SQL“. Nimm Stellung zu diesem Vorschlag. (3 Punkte)
Für (1 Punkt), z. B.: Für einmalige, kleine Auswertungen ist die Tabellenkalkulation vertraut und schnell; Diagramme lassen sich direkt erstellen.
Gegen (1 Punkt), z. B.: Eine Verweisfunktion liefert je Zeile nur den ersten Treffer – das entspricht einem Verbund nur bei 1:1- bzw. n:1-Beziehungen; 1:n-Beziehungen (mehrere Positionen oder Sendungen je Bestellung) gehen verloren. Exporte veralten sofort, müssen bei jeder Auswertung wiederholt werden und erzeugen Kopien personenbezogener Daten außerhalb des Berechtigungskonzepts der Datenbank.
Begründetes Fazit (1 Punkt), z. B.: Wiederkehrende Auswertungen gehören als Abfrage oder Sicht (View) in die Datenbank; die Tabellenkalkulation kann das Ergebnis darstellen.
Anlage: SQL-Kurzreferenz (MariaDB)
Auszug der in diesen Aufgaben nützlichen Sprachelemente. Eckige Klammern kennzeichnen optionale Teile.
| Syntax | Bedeutung |
|---|---|
SELECT [DISTINCT] ausdruck [AS alias], … |
Spalten bzw. Ausdrücke des Ergebnisses |
FROM tabelle1 [AS t1], tabelle2 [AS t2] |
Kreuzprodukt beider Tabellen; mit Bedingung in WHERE ein (impliziter) Verbund |
[INNER] JOIN tabelle2 ON bedingung |
nur Zeilen mit passendem Partner in beiden Tabellen |
LEFT [OUTER] JOIN tabelle2 ON bedingung |
alle Zeilen der linken Tabelle, fehlende Partner als NULL |
RIGHT [OUTER] JOIN tabelle2 ON bedingung |
alle Zeilen der rechten Tabelle, fehlende Partner als NULL |
FULL [OUTER] JOIN tabelle2 ON bedingung |
alle Zeilen beider Tabellen; in MariaDB nicht verfügbar (Ersatz: LEFT JOIN … UNION … RIGHT JOIN) |
CROSS JOIN tabelle2 |
Kreuzprodukt (jede Zeile mit jeder) |
WHERE bedingung |
filtert Zeilen nach dem Verbund und vor dem Gruppieren |
GROUP BY ausdruck1 [, ausdruck2 …] |
bildet Gruppen gleicher Werte |
HAVING bedingung |
filtert Gruppen nach dem Gruppieren (Bedingungen an Aggregate) |
ORDER BY ausdruck [ASC] [, …] bzw. … DESC |
Sortierung (Standard ASC; NULL steht bei ASC vorn) |
UNION / UNION ALL |
hängt Ergebnisse gleich aufgebauter Abfragen aneinander (UNION entfernt Duplikate) |
(SELECT … ) |
Unterabfrage als Wert, Liste (IN) oder abgeleitete Tabelle |
[NOT] EXISTS (SELECT …) |
wahr, wenn die Unterabfrage mindestens eine (bzw. keine) Zeile liefert |
UPDATE t1 JOIN t2 ON … SET t1.spalte = wert WHERE … |
Ändern mit Verbund |
DELETE t1 FROM t1 JOIN t2 ON … WHERE … |
Löschen mit Verbund (gelöscht wird in t1) |
COUNT(*) / COUNT(spalte) / COUNT(DISTINCT spalte) |
Anzahl Zeilen / Nicht-NULL-Werte / verschiedene Nicht-NULL-Werte |
SUM(ausdruck), AVG(ausdruck), MIN(…), MAX(…) |
Summe, Mittelwert, kleinster, größter Wert |
ROUND(zahl, stellen) |
runden |
COALESCE(wert1, wert2, …) |
erster Wert, der nicht NULL ist (IFNULL(a, b) für zwei Werte) |
CURDATE(), NOW() |
heutiges Datum, aktueller Zeitpunkt |
TIMESTAMPDIFF(einheit, start, ende) |
Differenz in vollen Einheiten (MINUTE, HOUR, DAY, MONTH, YEAR) |
ausdruck BETWEEN a AND b |
Bereich einschließlich der Grenzen |
ausdruck IS [NOT] NULL |
Test auf NULL |
Links und weitere Informationen
Artikel „SQL: Abfragen über mehrere Tabellen mit JOIN“ auf oer-informatik.de
als Nachschlagewerk: MariaDB Knowledge Base: JOIN Syntax
als Nachschlagewerk: MariaDB Knowledge Base: UPDATE (mehrere Tabellen)
als Nachschlagewerk: MariaDB Knowledge Base: DELETE (mehrere Tabellen)
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 SQL-Abfragen über mehrere Tabellen (JOIN)” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/sql-join-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/db-sql/dml-select-queries. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 26.10.2026.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]
