sql/01_Datenbanken.sql, ausgeführt mit SQLite; MySQL-Funktionen wie YEAR(), MONTH(), DATEDIFF(), LEFT() wurden für den Test in SQLite-Funktionen übersetzt). Die angegebenen Ergebnistabellen sind die tatsächlichen Testergebnisse. Bei DML-Aufgaben gilt jeweils der Ausgangsbestand der Anlagen.
| Nr. | Kardinalität | Begründung |
|---|---|---|
| 1 | 1:n (Station 1 : n Fahrrad) | Eine Station hat mehrere Fahrräder, ein Fahrrad gehört zu genau einer Station. |
| 2 | 1:1 | Eine Station hat genau einen Leiter, ein Mitarbeiter leitet höchstens eine Station. |
| 3 | m:n | Eine Ausleihe umfasst mehrere Zubehörteile, ein Zubehörteil wird bei vielen Ausleihen gebucht. |
Kategorie(KategorieNr, Bezeichnung, Tagespreis)
Fahrrad(FahrradNr, Modell, Kaufpreis, ↑KategorieNr)
Der Fremdschlüssel kommt bei einer 1:n-Beziehung in die Tabelle der n-Seite (Fahrrad): Jedes Fahrrad hat genau eine Kategorie, also steht im Feld KategorieNr genau ein Wert. Würde man umgekehrt die FahrradNr in Kategorie speichern, bräuchte eine Kategorie mehrere Werte in einem Feld (Verstoß gegen die 1NF).
Bewertung: a) je Zeile 1 P (Kardinalität mit stimmiger Begründung; Kardinalität ohne Begründung ½ P). b) Kategorie mit PK 1 P, Fahrrad mit PK und FK 1 P, Begründung 1 P. Schreibweise „1:n“, „n:1“ oder „Station 1 – n Fahrrad“ gleichwertig.
Erklärung: Immer beide Richtungen fragen. Bei Nr. 2 kommt der FK in eine der beiden Tabellen (z. B. Station.LeiterPersNr) und wird zusätzlich UNIQUE; bei Nr. 3 braucht man eine Zwischentabelle (vgl. DB-08.3 d).
Zur Kontrolle die daraus folgende relationale Umsetzung (nicht verlangt):
Mitarbeiter(PersNr, Name, Telefon, ↑VorgesetzterPersNr)
Station(StationNr, Name) · Kunde(KundenNr, Name, EMail)
Tour(TourNr, Titel, Datum, LaengeKm, Schwierigkeit, ↑StationNr, ↑PersNr)
Teilnahme(↑TourNr, ↑KundenNr, bezahlt, Bewertung)
Bewertung: je Entitätstyp mit Attributen und unterstrichenem Schlüssel 1 P (4 P). Je Beziehung mit korrekter Kardinalität 2 P (8 P): „startet an“ (Station 1 : n Tour), „leitet“ (Mitarbeiter 1 : n Tour), „nimmt teil an“ (Tour n : m Kunde) mit den Attributen bezahlt und Bewertung an der Raute (hängen sie an einer Entität: nur 1 P), „ist Vorgesetzter von“ als rekursive 1:n-Beziehung (zwei Linien zwischen Mitarbeiter und Raute). Andere sinnvolle Beziehungsnamen werden akzeptiert.
Erklärung: „bezahlt“ und „Bewertung“ beschreiben weder den Kunden allein (er nimmt an vielen Touren teil) noch die Tour allein (sie hat viele Teilnehmer) – sie gehören zur Teilnahme, also zur Beziehung. Typischer Fehler bei der rekursiven Beziehung: eine zweite Entität „Vorgesetzter“ anlegen. Der Vorgesetzte ist aber selbst ein Mitarbeiter.
Kunde(KundenNr, Vorname, Nachname, Email)
Station(StationNr, Name, Ort)
Gutschein(Code, RabattProzent, GueltigBis)
Buchung(BuchungsNr, Buchungsdatum, Abholdatum, Rueckgabedatum, ↑KundenNr, ↑AbholStationNr, ↑RueckgabeStationNr, ↑GutscheinCode)
Zubehoer(ZubehoerNr, Bezeichnung, Tagespreis)
Buchung_Zubehoer(↑BuchungsNr, ↑ZubehoerNr, Anzahl)
Bewertung (18 P): je Tabelle mit allen Attributen und PK 1 P (6 P); je Beziehung mit Kardinalität 1 P (6 P: Kunde–Buchung 1:n, Station–Buchung „Abholung“ 1:n, Station–Buchung „Rückgabe“ 1:n, Gutschein–Buchung 1:1, Buchung–Buchung_Zubehoer 1:n, Zubehoer–Buchung_Zubehoer 1:n); je FK 1 P (6 P: KundenNr, AbholStationNr, RueckgabeStationNr, GutscheinCode, BuchungsNr und ZubehoerNr in der Zwischentabelle).
Ebenfalls volle Punktzahl: Surrogatschlüssel in Buchung_Zubehoer (dann BuchungsNr + ZubehoerNr als FKs); FK zum Gutschein auf der anderen Seite (Gutschein.BuchungsNr, UNIQUE); Kardinalität Gutschein–Buchung als „1:c“. Wird die m:n-Beziehung nicht aufgelöst, gibt es für diese Beziehung und die zugehörigen FKs keine Punkte.
Der Fremdschlüssel GutscheinCode in Buchung erhält zusätzlich die Einschränkung UNIQUE. Damit kann jeder Code höchstens in einer Buchung vorkommen. Er darf NULL sein, weil nicht jede Buchung einen Gutschein hat (mehrere NULL-Werte sind bei UNIQUE erlaubt). Alternativ: FK BuchungsNr in Gutschein mit UNIQUE.
Bewertung: UNIQUE auf dem Fremdschlüssel 2 P; nur „Fremdschlüssel“ ohne Eindeutigkeit 1 P.
Die Anzahl hängt von der Kombination aus Buchung und Zubehör ab (voll funktional abhängig von BuchungsNr und ZubehoerNr). In Buchung müsste eine Buchung mit mehreren Zubehörteilen mehrere Anzahlen in einem Feld speichern (Verstoß gegen 1NF); in Zubehör gälte die Anzahl für alle Buchungen gleichzeitig, obwohl jede Buchung eine andere Anzahl hat. Sie gehört deshalb in die Zwischentabelle Buchung_Zubehoer.
Bewertung: je stimmige Begründung (Buchung / Zubehör) 1 P.
Erklärung: Die Abhol- und die Rückgabestation sind zwei verschiedene Beziehungen zwischen denselben Tabellen – das gibt zwei FKs mit sprechenden Namen und zwei Linien. Wer nur einen StationNr-Fremdschlüssel einträgt, verliert eine Beziehung und einen FK. Die 1:1-Beziehung zum Gutschein wird wie eine 1:n umgesetzt, nur dass der FK eindeutig sein muss.
Ein Attribut B ist funktional abhängig von A (A → B), wenn zu jedem Wert von A genau ein Wert von B gehört. Beispiel aus Kunde: KundenNr → Nachname – zur KundenNr 1001 gehört immer der Nachname „Hoffmann“ (ebenso KundenNr → Email).
Bewertung: Erläuterung 1 P, passendes Beispiel 1 P (z. B. auch KundenNr → Ort).
| Nr. | verletzte NF | Grund |
|---|---|---|
| 1 | 1NF | Feld nicht atomar (zwei Werte in einem Feld) |
| 2 | 3NF | transitive Abhängigkeit AusleiheNr → KundenNr → Nachname |
| 3 | 2NF | Bezeichnung hängt nur von einem Teil des zusammengesetzten Schlüssels ab |
Bewertung: je richtige Zuordnung 1 P.
Die 2NF ist verletzt, wenn ein Nichtschlüsselattribut nur von einem Teil des Primärschlüssels abhängt. Ein einspaltiger Schlüssel hat keinen echten Teil – jedes Nichtschlüsselattribut ist also automatisch voll funktional vom gesamten Schlüssel abhängig.
Bewertung: 1 P.
Erklärung: Merkhilfe für b): Mehrere Werte in einem Feld → immer 1NF. Hängt ein Attribut nur von einem Teil des zusammengesetzten Schlüssels ab → 2NF. Hängt es von einem anderen Nichtschlüsselattribut ab → 3NF. Die Verwechslung von 2NF und 3NF ist der häufigste Fehler.
Redundanz bedeutet, dass dieselbe Information mehrfach gespeichert ist. Beispiel: Name und Telefonnummer von Tom Weber (PersNr 12) stehen in drei Zeilen; „Hauptbahnhof, Freiburg“ zur StationNr 1 steht in vier Zeilen; die Dauer „6,0“ steht bei jeder Frühschicht.
Bewertung: Erläuterung 1 P, konkretes Beispiel aus Anlage 5 1 P.
Bewertung: je Anomalie 2 P (1 P Erläuterung, 1 P konkretes Beispiel aus Anlage 5).
Mitarbeiter(PersNr, Vorname, Nachname, Telefon)
Station(StationNr, Name, Ort)
Schicht(Schichtart, Stunden)
Einsatz(Datum, ↑PersNr, ↑StationNr, ↑Schichtart)
Bewertung: je Tabelle mit korrektem PK/FK 1 P. Volle Punktzahl für Mitarbeiter nur, wenn der Name in Vor- und Nachname zerlegt ist. Statt Schichtart als PK auch SchichtNr möglich; Stunden dürfen alternativ aus Beginn/Ende der Schicht berechnet werden.
Erklärung: Schlüssel der Liste ist (Datum, PersNr), da ein Mitarbeiter höchstens eine Schicht pro Tag hat. PersNr → Name, Telefon ist eine Teilabhängigkeit (2NF-Verstoß); StationNr → Station, Ort und Schicht → Stunden sind transitive Abhängigkeiten (3NF-Verstöße). „Weber, Tom“ ist nicht atomar (1NF).
Nicht gespeichert werden die berechneten Angaben: Positionsbetrag „Gesamt“ (Menge × Einzelpreis), Nettobetrag (Summe der Positionen), Umsatzsteuerbetrag (Netto × 19 %) und Rechnungsbetrag (Netto + USt). Sie lassen sich jederzeit aus den gespeicherten Werten berechnen; speichert man sie, entsteht Redundanz mit der Gefahr widersprüchlicher Werte.
Bewertung: zwei berechnete Angaben 1 P, Begründung 1 P.
Kunde(KundenNr, Anrede, Vorname, Nachname, Strasse, Hausnummer, PLZ, Ort, Telefon)
Kundenfahrrad(KundenfahrradNr, Rahmennummer, Hersteller, Modell, ↑KundenNr)
Mechaniker(MechanikerNr, Vorname, Nachname)
Rechnung(RechnungsNr, Rechnungsdatum, Zahlungsart, ↑KundenfahrradNr, ↑MechanikerNr)
Artikel(ArtikelNr, Bezeichnung, Einheit, Preis)
Rechnungsposition(↑RechnungsNr, PosNr, ↑ArtikelNr, Menge, Einzelpreis)
Bewertung (18 P): je Tabelle mit vollständigen Attributen und PK 1 P (6 P); je Beziehung mit Kardinalität 1 P (5 P); je FK 1 P (5 P); Einzelpreis (Preis zum Rechnungszeitpunkt) in der Positionstabelle 1 P; Name und Anschrift atomar zerlegt 1 P.
Ebenfalls korrekt: PK der Position (RechnungsNr, ArtikelNr); Rahmennummer als PK von Kundenfahrrad; zusätzliche Tabellen Hersteller, Zahlungsart, Einheit oder Ort(PLZ, Ort); USt-Satz als Attribut von Rechnung oder Artikel; KundenNr zusätzlich in Rechnung, wenn begründet (z. B. Rechnungsempfänger kann vom Fahrradbesitzer abweichen).
Erklärung: Aus dem Gespräch folgt: Kunde 1:n Fahrrad, Fahrrad 1:n Rechnung, Mechaniker 1:n Rechnung. Rechnung und Artikel stehen in einer m:n-Beziehung, die über die Rechnungsposition (mit Menge und Einzelpreis) aufgelöst wird. Arbeitsleistungen (L-100, Einheit Std.) sind einfach Artikel. Der Einzelpreis in der Position ist trotz Artikel.Preis keine Redundanz, weil er den historischen Preis festhält.
SELECT KundenNr, Vorname, Nachname FROM Kunde WHERE Email IS NULL;
| KundenNr | Vorname | Nachname |
|---|---|---|
| 1003 | Mira | Schulze |
| 1007 | Emma | Vogel |
SELECT FahrradNr, Modell, Kaufpreis FROM Fahrrad WHERE YEAR(Kaufdatum) = 2024 ORDER BY Kaufpreis DESC, FahrradNr;
| FahrradNr | Modell | Kaufpreis |
|---|---|---|
| 107 | Cargo L2 | 5390.00 |
| 103 | Tour T7 | 3199.00 |
| 104 | Tour T7 | 3199.00 |
SELECT KundenNr, Nachname, PLZ, Ort FROM Kunde WHERE PLZ LIKE '791%';
| KundenNr | Nachname | PLZ | Ort |
|---|---|---|---|
| 1002 | Albrecht | 79102 | Freiburg |
| 1005 | Keller | 79100 | Freiburg |
| 1009 | Krüger | 79194 | Gundelfingen |
| 1010 | Neumann | 79108 | Freiburg |
Bewertung: je Teilaufgabe SELECT/FROM 1 P, WHERE (bei b mit ORDER BY) 1 P. Alternativ b) WHERE Kaufdatum BETWEEN '2024-01-01' AND '2024-12-31'.
Erklärung: Email = NULL liefert nie ein Ergebnis. LIKE funktioniert, weil PLZ als Text gespeichert ist (vgl. Kunde 1008 mit PLZ 01067).
SELECT a.AusleiheNr, a.Beginn, k.Nachname, f.Modell FROM Ausleihe a INNER JOIN Kunde k ON k.KundenNr = a.KundenNr INNER JOIN Fahrrad f ON f.FahrradNr = a.FahrradNr WHERE a.Beginn BETWEEN '2026-07-01' AND '2026-08-31' ORDER BY a.Beginn;
| AusleiheNr | Beginn | Nachname | Modell |
|---|---|---|---|
| 5009 | 2026-07-18 | Hoffmann | Cargo L2 |
| 5010 | 2026-08-08 | Brandt | Trail X9 |
| 5011 | 2026-08-22 | Vogel | Urban C5 |
Bewertung: SELECT-Liste 1 P, Join Kunde 1 P, Join Fahrrad 1 P, Zeitraumbedingung 1 P, ORDER BY 1 P. Alternativ: WHERE YEAR(a.Beginn) = 2026 AND MONTH(a.Beginn) IN (7, 8) oder Join-Bedingungen im WHERE (FROM Ausleihe a, Kunde k, Fahrrad f WHERE …) – getestet, gleiches Ergebnis.
SELECT f.FahrradNr, f.Modell, w.Datum, w.Art FROM Fahrrad f LEFT JOIN Wartung w ON w.FahrradNr = f.FahrradNr ORDER BY f.FahrradNr, w.Datum;
| FahrradNr | Modell | Datum | Art |
|---|---|---|---|
| 101 | Urban C5 | 2025-11-20 | Inspektion |
| 102 | Urban C5 | NULL | NULL |
| 103 | Tour T7 | 2026-02-03 | Inspektion |
| 104 | Tour T7 | 2026-09-15 | Reifen |
| 105 | Trail X9 | 2025-11-25 | Bremsen |
| 105 | Trail X9 | 2026-04-20 | Akku-Check |
| 105 | Trail X9 | 2026-09-30 | Inspektion |
| 106 | Trail X9 | NULL | NULL |
| 107 | Cargo L2 | 2026-01-14 | Inspektion |
| 107 | Cargo L2 | 2026-07-01 | Bremsen |
| 108 | Cargo L2 | NULL | NULL |
| 109 | Kiddy 20 | 2026-05-07 | Reifen |
| 110 | Urban C6 | NULL | NULL |
Bewertung: SELECT 1 P, LEFT JOIN mit Fahrrad als linker Tabelle 2 P (INNER JOIN: 0 P für diesen Teil, da 102, 106, 108 und 110 fehlen), ORDER BY 1 P. Wartung w RIGHT JOIN Fahrrad f ist gleichwertig.
Erklärung: „Auch Fahrräder, die nie gewartet wurden“ → LEFT JOIN. Fahrrad 105 erscheint dreimal, weil es drei Wartungen hat.
SELECT a.AusleiheNr, k.Nachname, f.Modell, kat.Bezeichnung,
DATEDIFF(a.Ende, a.Beginn) + 1 AS Mietdauer,
kat.Tagespreis,
(DATEDIFF(a.Ende, a.Beginn) + 1) * kat.Tagespreis AS Sollbetrag
FROM Ausleihe a
INNER JOIN Kunde k ON k.KundenNr = a.KundenNr
INNER JOIN Fahrrad f ON f.FahrradNr = a.FahrradNr
INNER JOIN Kategorie kat ON kat.KategorieNr = f.KategorieNr
WHERE YEAR(a.Beginn) = 2026
AND a.Ende IS NOT NULL
ORDER BY Sollbetrag DESC;
| AusleiheNr | Nachname | Modell | Bezeichnung | Mietdauer | Tagespreis | Sollbetrag |
|---|---|---|---|---|---|---|
| 5008 | Brandt | Tour T7 | Trekking-E-Bike | 7 | 30.00 | 210.00 |
| 5006 | Keller | Cargo L2 | E-Lastenrad | 3 | 45.00 | 135.00 |
| 5007 | Albrecht | Trail X9 | E-Mountainbike | 3 | 39.00 | 117.00 |
| 5010 | Brandt | Trail X9 | E-Mountainbike | 2 | 39.00 | 78.00 |
| 5011 | Vogel | Urban C5 | City-E-Bike | 3 | 25.00 | 75.00 |
| 5004 | Hoffmann | Tour T7 | Trekking-E-Bike | 2 | 30.00 | 60.00 |
| 5009 | Hoffmann | Cargo L2 | E-Lastenrad | 1 | 45.00 | 45.00 |
| 5005 | Schulze | Kiddy 20 | Kinderrad | 1 | 12.00 | 12.00 |
Bewertung: SELECT-Liste mit Aliasen 1 P, Mietdauer mit +1 1 P, Sollbetrag 1 P, Joins über vier Tabellen 1 P, WHERE (2026 und abgeschlossen) 1 P, ORDER BY 1 P. Mit IHK-Belegsatz-Syntax ebenso richtig: DATEDIFF(DAY, a.Beginn, a.Ende) + 1. Ohne „+ 1“ 0 P für die Mietdauer.
Erklärung: DATEDIFF liefert die Differenz (Ausleihe 5009: 0 Tage), deshalb +1. Die Kontrolle ergibt: Alle Sollbeträge stimmen mit den gespeicherten Beträgen überein. Die laufende Ausleihe 5012 fällt durch Ende IS NOT NULL heraus (ohne die Bedingung stünde sie mit Mietdauer NULL im Ergebnis).
SELECT k.KundenNr, k.Nachname, a.AusleiheNr, a.Beginn
FROM Kunde k
LEFT JOIN Ausleihe a ON a.KundenNr = k.KundenNr
AND YEAR(a.Beginn) = 2026
ORDER BY k.KundenNr, a.Beginn;
| KundenNr | Nachname | AusleiheNr | Beginn |
|---|---|---|---|
| 1001 | Hoffmann | 5004 | 2026-03-21 |
| 1001 | Hoffmann | 5009 | 2026-07-18 |
| 1002 | Albrecht | 5007 | 2026-05-30 |
| 1003 | Schulze | 5005 | 2026-04-11 |
| 1004 | Wagner | NULL | NULL |
| 1005 | Keller | 5006 | 2026-05-01 |
| 1005 | Keller | 5012 | 2026-10-08 |
| 1006 | Brandt | 5008 | 2026-06-14 |
| 1006 | Brandt | 5010 | 2026-08-08 |
| 1007 | Vogel | 5011 | 2026-08-22 |
| 1008 | Roth | NULL | NULL |
| 1009 | Krüger | NULL | NULL |
| 1010 | Neumann | NULL | NULL |
Bewertung: SELECT/FROM Kunde 1 P, LEFT JOIN 2 P, Jahresbedingung in der ON-Klausel 1 P, ORDER BY 1 P. Statt YEAR auch AND a.Beginn >= '2026-01-01' AND a.Beginn < '2027-01-01'.
Es fehlen 1004 Wagner, 1008 Roth, 1009 Krüger und 1010 Neumann. Die WHERE-Klausel wird erst nach dem Join ausgewertet: Für 1008, 1009 und 1010 erzeugt der LEFT JOIN eine Zeile mit NULL in den Ausleihe-Spalten; YEAR(NULL) = 2026 ist nicht wahr, die Zeile wird verworfen. Für 1004 entsteht eine Zeile mit der Ausleihe 5003 aus 2025, die die Bedingung ebenfalls nicht erfüllt. Der LEFT JOIN wirkt dadurch wie ein INNER JOIN.
| KundenNr | Nachname | AusleiheNr | Beginn |
|---|---|---|---|
| 1001 | Hoffmann | 5004 | 2026-03-21 |
| 1001 | Hoffmann | 5009 | 2026-07-18 |
| 1002 | Albrecht | 5007 | 2026-05-30 |
| 1003 | Schulze | 5005 | 2026-04-11 |
| 1005 | Keller | 5006 | 2026-05-01 |
| 1005 | Keller | 5012 | 2026-10-08 |
| 1006 | Brandt | 5008 | 2026-06-14 |
| 1006 | Brandt | 5010 | 2026-08-08 |
| 1007 | Vogel | 5011 | 2026-08-22 |
Bewertung: fehlende Kunden genannt 1 P (alle vier; auch Wagner, der nur 2025 ausgeliehen hat), Begründung (WHERE nach dem Join, NULL bzw. anderes Jahr erfüllt Bedingung nicht → wie INNER JOIN) 2 P.
SELECT COUNT(*) AS Anzahl, SUM(Kilometer) AS SummeKm, AVG(Kilometer) AS DurchschnittKm FROM Ausleihe WHERE Ende IS NOT NULL;
| Anzahl | SummeKm | DurchschnittKm |
|---|---|---|
| 11 | 885 | 80.4545 |
Bewertung: Aggregatfunktionen 1 P, Bedingung „abgeschlossen“ 1 P. Statt COUNT(*) mit WHERE auch COUNT(Kilometer) ohne WHERE richtig.
Erklärung: Ohne WHERE liefert COUNT(*) 12 (inklusive der laufenden Ausleihe 5012), SUM und AVG bleiben gleich, weil sie NULL ignorieren. MySQL zeigt AVG mit vier Nachkommastellen (80.4545).
WHERE filtert einzelne Datensätze vor der Gruppierung und darf keine Aggregatfunktionen enthalten. HAVING filtert die Gruppen nach dem GROUP BY und kann sich auf Aggregatfunktionen beziehen (z. B. HAVING COUNT(*) >= 2).
Bewertung: je Klausel 1 P.
| COUNT(*) | COUNT(Kosten) |
|---|---|
| 9 | 8 |
COUNT(*) zählt alle 9 Zeilen, COUNT(Kosten) nur die Zeilen, in denen Kosten nicht NULL ist. Die Wartung 9 (noch nicht abgerechnet) hat Kosten = NULL.
Bewertung: Ergebnis 1 P, Begründung 1 P.
SELECT kat.Bezeichnung, COUNT(*) AS Anzahl, SUM(a.Betrag) AS Umsatz FROM Ausleihe a INNER JOIN Fahrrad f ON f.FahrradNr = a.FahrradNr INNER JOIN Kategorie kat ON kat.KategorieNr = f.KategorieNr GROUP BY kat.KategorieNr, kat.Bezeichnung ORDER BY Umsatz DESC;
| Bezeichnung | Anzahl | Umsatz |
|---|---|---|
| E-Lastenrad | 3 | 315.00 |
| Trekking-E-Bike | 3 | 270.00 |
| E-Mountainbike | 3 | 234.00 |
| City-E-Bike | 2 | 125.00 |
| Kinderrad | 1 | 12.00 |
Bewertung: Joins 2 P, COUNT und SUM 1 P, GROUP BY 1 P, ORDER BY 1 P.
Erklärung: Tandem hat keine Ausleihen und erscheint wegen des INNER JOIN nicht – genau so verlangt. Bei Trekking zählt die laufende Ausleihe 5012 mit (Anzahl 3), ihr Betrag NULL wird von SUM ignoriert.
SELECT k.KundenNr, k.Vorname, k.Nachname, COUNT(*) AS Anzahl FROM Kunde k INNER JOIN Ausleihe a ON a.KundenNr = k.KundenNr GROUP BY k.KundenNr, k.Vorname, k.Nachname HAVING COUNT(*) >= 2 ORDER BY Anzahl DESC, k.Nachname;
| KundenNr | Vorname | Nachname | Anzahl |
|---|---|---|---|
| 1001 | Lena | Hoffmann | 3 |
| 1002 | Jonas | Albrecht | 2 |
| 1006 | Tim | Brandt | 2 |
| 1005 | Sofia | Keller | 2 |
Bewertung: Join 1 P, GROUP BY mit allen Nicht-Aggregat-Spalten 1 P, HAVING 2 P (Bedingung im WHERE: 0 P), ORDER BY 1 P.
SELECT s.StationNr, s.Name, COUNT(f.FahrradNr) AS AnzahlFahrraeder FROM Station s LEFT JOIN Fahrrad f ON f.StationNr = s.StationNr GROUP BY s.StationNr, s.Name ORDER BY s.StationNr;
| StationNr | Name | AnzahlFahrraeder |
|---|---|---|
| 1 | Hauptbahnhof | 3 |
| 2 | Messe | 2 |
| 3 | Titisee | 3 |
| 4 | Kaiserstuhl | 2 |
| 5 | Schauinsland | 0 |
Bewertung: LEFT JOIN mit Station links 2 P, COUNT(f.FahrradNr) 2 P (mit COUNT(*) nur 1 P, weil Schauinsland dann 1 statt 0 erhält), GROUP BY 1 P. ORDER BY nicht verlangt.
SELECT f.FahrradNr, f.Modell, COUNT(*) AS AnzahlWartungen, SUM(w.Kosten) AS Kosten FROM Fahrrad f INNER JOIN Wartung w ON w.FahrradNr = f.FahrradNr WHERE YEAR(w.Datum) = 2026 GROUP BY f.FahrradNr, f.Modell HAVING SUM(w.Kosten) > 50 ORDER BY Kosten DESC;
| FahrradNr | Modell | AnzahlWartungen | Kosten |
|---|---|---|---|
| 107 | Cargo L2 | 2 | 287.00 |
| 103 | Tour T7 | 1 | 89.00 |
| 104 | Tour T7 | 1 | 55.00 |
Bewertung: Join 1 P, WHERE mit Jahr 1 P, COUNT und SUM 1 P, GROUP BY 1 P, HAVING 1 P, ORDER BY 1 P.
Erklärung: Ohne HAVING kämen noch 109 (42.90) und 105 (2 Wartungen, Summe 35.00 – die Inspektion vom 30.09. hat Kosten NULL) hinzu. Die Jahresbedingung betrifft Einzelzeilen → WHERE; „Summe über 50 €“ betrifft die Gruppe → HAVING.
Fehler 1: Die Aggregatfunktion COUNT steht in der WHERE-Klausel. WHERE wird vor der Gruppierung ausgewertet; die Bedingung gehört in HAVING.
Fehler 2: k.Nachname steht im SELECT, aber nicht im GROUP BY (Verstoß gegen die GROUP-BY-Regel).
SELECT k.KundenNr, k.Nachname, COUNT(a.AusleiheNr) AS Anzahl FROM Kunde k LEFT JOIN Ausleihe a ON k.KundenNr = a.KundenNr GROUP BY k.KundenNr, k.Nachname HAVING COUNT(a.AusleiheNr) = 0;
| KundenNr | Nachname | Anzahl |
|---|---|---|
| 1008 | Roth | 0 |
| 1009 | Krüger | 0 |
| 1010 | Neumann | 0 |
Mit COUNT(*) wäre das Ergebnis leer: Nach dem LEFT JOIN hat jeder Kunde ohne Ausleihe trotzdem genau eine Zeile (mit NULL in den Ausleihe-Spalten). COUNT(*) zählt diese Zeile, liefert also 1 und nie 0.
Bewertung: je Fehler 1 P, korrigierte Abfrage 2 P, Erläuterung zu COUNT(*) 1 P. Ebenso richtig: Lösung mit WHERE NOT EXISTS oder WHERE KundenNr NOT IN (SELECT KundenNr FROM Ausleihe).
Dialekt-Hinweis: MySQL 8 lässt Fehler 2 hier ausnahmsweise durchgehen, weil Nachname funktional vom Primärschlüssel KundenNr abhängt. SQL Server, Oracle und die IHK-Lösungen erwarten aber alle Nicht-Aggregat-Spalten im GROUP BY.
INSERT INTO Kunde (KundenNr, Vorname, Nachname, PLZ, Ort, Email) VALUES (1011, 'Clara', 'Fischer', '79104', 'Freiburg', 'c.fischer@mail.de');
Bewertung: INSERT INTO mit Tabelle und Spaltenliste 1 P, Werte vollständig und in passender Reihenfolge (Texte und PLZ in Anführungszeichen) 1 P. Ohne Spaltenliste richtig, wenn die Werte in Tabellenreihenfolge stehen.
UPDATE Kunde SET Email = 'paul.wagner@posteo.de' WHERE KundenNr = 1004;
Bewertung: 1 P (ohne WHERE 0 P – dann hätten alle Kunden diese Adresse).
Auf den Kunden 1004 verweist noch die Ausleihe 5003 über den Fremdschlüssel Ausleihe.KundenNr. Die referenzielle Integrität verhindert das Löschen (Standardverhalten RESTRICT), sonst entstünde eine Ausleihe zu einem nicht existierenden Kunden.
Möglichkeiten: zuerst die abhängigen Ausleihen archivieren bzw. löschen und danach den Kunden löschen; oder den Fremdschlüssel mit ON DELETE CASCADE definieren (löscht die Ausleihhistorie mit); oder den Kunden statt zu löschen anonymisieren bzw. als inaktiv markieren (Soft Delete).
Bewertung: Grund (FK/referenzielle Integrität mit Bezug auf Ausleihe 5003) 1 P, eine sinnvolle Möglichkeit 1 P.
Erklärung: Die PLZ ist Text und steht deshalb in Anführungszeichen ('79104'). In c) reicht „der Kunde wird noch verwendet“ nicht – erwartet wird der Bezug zum Fremdschlüssel bzw. zur referenziellen Integrität. Ob die Ausleihhistorie gelöscht werden darf, ist eine fachliche Frage (Aufbewahrungspflichten); Archivieren oder Anonymisieren ist deshalb meist besser als CASCADE.
UPDATE Kategorie
SET Tagespreis = Tagespreis * 1.10
WHERE KategorieNr IN (SELECT f.KategorieNr
FROM Ausleihe a
INNER JOIN Fahrrad f ON f.FahrradNr = a.FahrradNr
WHERE YEAR(a.Beginn) = 2026
GROUP BY f.KategorieNr
HAVING COUNT(*) >= 2);
Ausleihen 2026 je Kategorie: City 1, Trekking 3, E-Mountainbike 2, E-Lastenrad 2, Kinderrad 1. Ergebnis nach dem UPDATE (Tabelle Kategorie):
| KategorieNr | Bezeichnung | Tagespreis |
|---|---|---|
| 1 | City-E-Bike | 25.00 |
| 2 | Trekking-E-Bike | 33.00 |
| 3 | E-Mountainbike | 42.90 |
| 4 | E-Lastenrad | 49.50 |
| 5 | Kinderrad | 12.00 |
| 6 | Tandem | 35.00 |
Bewertung: UPDATE … SET mit Berechnung (+10 %) 2 P, Unterabfrage mit Join und Jahresbedingung 2 P, GROUP BY/HAVING in der Unterabfrage 1 P.
UPDATE Fahrrad SET StationNr = (SELECT StationNr FROM Station WHERE Name = 'Messe') WHERE KategorieNr = (SELECT KategorieNr FROM Kategorie WHERE Bezeichnung = 'E-Lastenrad'); -- gleichwertig in MySQL: UPDATE mit JOIN UPDATE Fahrrad f INNER JOIN Kategorie k ON k.KategorieNr = f.KategorieNr INNER JOIN Station s ON s.Name = 'Messe' SET f.StationNr = s.StationNr WHERE k.Bezeichnung = 'E-Lastenrad';
| FahrradNr | Modell | KategorieNr | StationNr |
|---|---|---|---|
| 107 | Cargo L2 | 4 | 2 |
| 108 | Cargo L2 | 4 | 2 |
Bewertung: SET mit Unterabfrage auf Station 2 P, WHERE mit Unterabfrage auf Kategorie 2 P, vollständige Syntax 1 P. Feste Werte (SET StationNr = 2 WHERE KategorieNr = 4) entsprechen nicht der Aufgabenstellung: max. 2 P.
Erklärung: Betroffen sind die Fahrräder 107 (bisher Station 1) und 108 (stand schon an der Messe). Die skalaren Unterabfragen sind zulässig, weil Name bzw. Bezeichnung jeweils genau eine Zeile liefern.
START TRANSACTION; INSERT INTO Ausleihe_Archiv (AusleiheNr, KundenNr, FahrradNr, Beginn, Ende, Kilometer, Betrag) SELECT AusleiheNr, KundenNr, FahrradNr, Beginn, Ende, Kilometer, Betrag FROM Ausleihe WHERE Ende < '2026-01-01'; DELETE FROM Ausleihe WHERE Ende < '2026-01-01'; COMMIT;
Ergebnis: Die Ausleihen 5001, 5002 und 5003 stehen jetzt in Ausleihe_Archiv (11 Zeilen), Ausleihe enthält noch 5004–5012 (9 Zeilen). Die laufende Ausleihe 5012 bleibt erhalten, weil NULL < '2026-01-01' nicht wahr ist.
| AusleiheNr | KundenNr | FahrradNr | Beginn | Ende | Kilometer | Betrag |
|---|---|---|---|---|---|---|
| 4001 | 1001 | 101 | 2024-05-10 | 2024-05-11 | 55 | 50.00 |
| 4002 | 1009 | 105 | 2024-06-22 | 2024-06-23 | 81 | 78.00 |
| 4003 | 1002 | 102 | 2024-07-06 | 2024-07-06 | 31 | 25.00 |
| 4004 | 1004 | 109 | 2024-08-15 | 2024-08-17 | 20 | 36.00 |
| 4005 | 1009 | 107 | 2024-09-28 | 2024-09-28 | 18 | 45.00 |
| 4006 | 1006 | 103 | 2024-10-12 | 2024-10-13 | 104 | 60.00 |
| 4007 | 1003 | 104 | 2025-04-19 | 2025-04-20 | 66 | 60.00 |
| 4008 | 1001 | 101 | 2025-06-07 | 2025-06-08 | 64 | 50.00 |
| 5001 | 1001 | 101 | 2025-08-02 | 2025-08-03 | 64 | 50.00 |
| 5002 | 1002 | 105 | 2025-09-13 | 2025-09-13 | 38 | 39.00 |
| 5003 | 1004 | 107 | 2025-10-04 | 2025-10-06 | 41 | 135.00 |
Bewertung: INSERT INTO … SELECT (ohne VALUES) mit passenden Spalten 2 P, korrekte Bedingung 2 P, DELETE mit derselben Bedingung 2 P. WHERE YEAR(Ende) < 2026 ist gleichwertig; INSERT INTO Ausleihe_Archiv SELECT * FROM Ausleihe … ist wegen gleicher Struktur ebenfalls richtig.
Transaktion: Beide Befehle bilden eine logische Einheit. Bricht das System nach dem INSERT ab, würde ohne Transaktion das DELETE fehlen (Datensätze doppelt); mit Transaktion wird alles zurückgesetzt (ROLLBACK) bzw. nur gemeinsam bestätigt (COMMIT).
Reihenfolge: Erst kopieren, dann löschen. Umgekehrt wären die Daten nach dem DELETE weg und könnten nicht mehr ins Archiv übernommen werden.
Bewertung: Transaktion 1 P, Reihenfolge 1 P.
INSERT INTO Fahrrad_Archiv (FahrradNr, Rahmennummer, Modell, Kategorie, Kaufdatum, Ausmusterung, GesamtKm)
SELECT f.FahrradNr, f.Rahmennummer, f.Modell, k.Bezeichnung, f.Kaufdatum, '2026-10-10',
(SELECT SUM(a.Kilometer) FROM Ausleihe a WHERE a.FahrradNr = f.FahrradNr)
+ (SELECT SUM(aa.Kilometer) FROM Ausleihe_Archiv aa WHERE aa.FahrradNr = f.FahrradNr)
FROM Fahrrad f
INNER JOIN Kategorie k ON k.KategorieNr = f.KategorieNr
WHERE f.FahrradNr = 105;
| FahrradNr | Rahmennummer | Modell | Kategorie | Kaufdatum | Ausmusterung | GesamtKm |
|---|---|---|---|---|---|---|
| 95 | WAW-21-0004 | Urban C4 | City-E-Bike | 2021-04-01 | 2025-12-31 | 4120 |
| 98 | MTN-21-0102 | Trail X8 | E-Mountainbike | 2021-06-15 | 2026-01-15 | 2875 |
| 105 | MTN-22-0456 | Trail X9 | E-Mountainbike | 2022-05-20 | 2026-10-10 | 195 |
GesamtKm = 38 + 76 (Ausleihe 5002, 5010) + 81 (Archiv 4002) = 195. Das Ergebnis ist dasselbe, wenn a) vorher ausgeführt wurde (dann liegt 5002 im Archiv) – getestet.
Bewertung: INSERT INTO … SELECT mit Spaltenliste 1 P, Join auf Kategorie (Bezeichnung statt Nummer) 1 P, Ausmusterungsdatum 1 P, Kilometersumme aus beiden Tabellen 2 P (nur aus einer Tabelle: 1 P). Statt '2026-10-10' auch CURDATE().
Robuster: Hat ein Fahrrad in einer der Tabellen keine Ausleihe, liefert SUM dort NULL und die Summe wird NULL. Abhilfe: COALESCE((SELECT SUM(…) …), 0) + COALESCE((SELECT SUM(…) …), 0) oder die Kilometer beider Tabellen per UNION ALL in einer Unterabfrage zusammenfassen und einmal summieren.
DELETE FROM Wartung WHERE FahrradNr = 105; DELETE FROM Ausleihe WHERE FahrradNr = 105; DELETE FROM Fahrrad WHERE FahrradNr = 105;
Wartung und Ausleihe verweisen über Fremdschlüssel auf Fahrrad. Solange dort noch Datensätze zu Fahrrad 105 existieren, verhindert die referenzielle Integrität das Löschen des Fahrrads (getestet: DELETE FROM Fahrrad WHERE FahrradNr = 105 zuerst → Fehler „FOREIGN KEY constraint failed“). Daher zuerst die abhängigen Datensätze (Reihenfolge Wartung/Ausleihe beliebig), dann das Fahrrad. Ausleihe_Archiv hat keinen Fremdschlüssel und bleibt unverändert.
Bewertung: drei DELETE-Anweisungen 1 P, Reihenfolge 1 P, Begründung 1 P.
Bewertung: je Eigenschaft (Begriff und Erläuterung) 1 P.
Vorteil: schnelleres Suchen, Sortieren und Verknüpfen (kein Durchsuchen der ganzen Tabelle). Nachteil: zusätzlicher Speicherbedarf bzw. langsamere INSERT-, UPDATE- und DELETE-Operationen, weil der Index mitgepflegt werden muss.
Bewertung: je 1 P.
Erklärung: Bei „Nennen und erläutern“ gibt es den Punkt nur mit Erläuterung – die vier Begriffe allein reichen nicht. Merkhilfe: A = alles oder nichts, C = Regeln bleiben eingehalten, I = keine gegenseitige Störung, D = bleibt gespeichert.
Eine Transaktion ist eine Folge von Datenbankoperationen, die als logische Einheit vollständig oder gar nicht ausgeführt wird (START TRANSACTION … COMMIT bzw. ROLLBACK).
Stürzt der Server ohne Transaktion nach Schritt 1 ab, ist die Ausleihe bereits abgeschlossen (Ende, Kilometer, Betrag gesetzt), aber es gibt keine Rechnung – der Kunde wird nicht abgerechnet, der Umsatz fehlt. Außerdem fehlt ggf. der Wartungsauftrag, sodass ein wartungsbedürftiges Fahrrad weiter verliehen wird. Die Daten sind inkonsistent; eine Wiederholung der Rückgabe ist nicht möglich, weil die Ausleihe schon beendet ist. Mit Transaktion würde Schritt 1 beim Neustart zurückgesetzt und die Rückgabe könnte komplett wiederholt werden.
Bewertung: Erläuterung Transaktion 2 P, Folgen des Absturzes 2 P.
Referenzielle Integrität: Jeder Wert im Fremdschlüssel Ausleihe.FahrradNr muss als Primärschlüssel in Fahrrad.FahrradNr existieren. Eine Ausleihe für ein Fahrrad 999 kann nicht eingefügt werden, und das Fahrrad 103 kann nicht einfach gelöscht werden, solange die Ausleihen 5004 und 5012 darauf verweisen.
Reaktionsmöglichkeiten beim Löschen: RESTRICT / NO ACTION – das Löschen wird abgewiesen; CASCADE – die abhängigen Ausleihen werden mitgelöscht; SET NULL – der FK in den Ausleihen wird auf NULL gesetzt (hier nicht möglich, weil FahrradNr NOT NULL ist).
Bewertung: Erläuterung mit Bezug auf Ausleihe/Fahrrad 2 P, je Reaktionsmöglichkeit 1 P (zwei verlangt).
Ausleihe.KundenNr – nach ihr wird gefiltert (WHERE KundenNr = …) und über sie mit Kunde verknüpft. Die Tabelle Ausleihe wächst ständig; ohne Index müsste bei jeder Abfrage die gesamte Tabelle durchsucht werden.
Bewertung: Spalte 1 P, Begründung 1 P.
Erklärung: In a) wird ein Bezug zur Situation erwartet (fehlende Rechnung, fehlender Wartungsauftrag), nicht nur die Definition. Zu c): MySQL (InnoDB) legt für Fremdschlüsselspalten automatisch einen Index an, andere DBMS nicht. Erwartet wird trotzdem die Begründung „häufig in WHERE/JOIN verwendet, Tabelle wächst ständig“.
| Relationale Datenbank | Dokumentenorientierte Datenbank |
|---|---|
| Tabelle | Collection (Sammlung) |
| Datensatz (Zeile) | Document (Dokument) |
| Spalte (Attribut) | Field (Feld bzw. Schlüssel-Wert-Paar) |
Bewertung: je Zuordnung 1 P.
{
"_id": 1001,
"vorname": "Lena",
"nachname": "Hoffmann",
"plz": "79098",
"ort": "Freiburg",
"email": "lena.hoffmann@mail.de",
"ausleihen": [
{ "ausleiheNr": 5001, "beginn": "2025-08-02", "ende": "2025-08-03",
"kilometer": 64, "betrag": 50.00, "modell": "Urban C5" },
{ "ausleiheNr": 5004, "beginn": "2026-03-21", "ende": "2026-03-22",
"kilometer": 92, "betrag": 60.00, "modell": "Tour T7" },
{ "ausleiheNr": 5009, "beginn": "2026-07-18", "ende": "2026-07-18",
"kilometer": 23, "betrag": 45.00, "modell": "Cargo L2" }
]
}
Datengrundlage (getestete Abfrage über Kunde, Ausleihe und Fahrrad):
| KundenNr | Nachname | AusleiheNr | Beginn | Ende | Kilometer | Betrag | Modell |
|---|---|---|---|---|---|---|---|
| 1001 | Hoffmann | 5001 | 2025-08-02 | 2025-08-03 | 64 | 50.00 | Urban C5 |
| 1001 | Hoffmann | 5004 | 2026-03-21 | 2026-03-22 | 92 | 60.00 | Tour T7 |
| 1001 | Hoffmann | 5009 | 2026-07-18 | 2026-07-18 | 23 | 45.00 | Cargo L2 |
Bewertung: gültige JSON-Syntax (Klammern, doppelte Anführungszeichen, Kommas) 2 P, Kundendaten 1 P, Array für die Ausleihen 1 P, drei Ausleihen mit richtigen Werten 2 P, Modell je Ausleihe 1 P. Ebenfalls richtig: Fahrrad als eingebettetes Objekt ("fahrrad": { "fahrradNr": 101, "modell": "Urban C5" }), andere Feldnamen, KundenNr als eigenes Feld statt _id.
Erklärung: Zahlen ohne Anführungszeichen, die PLZ aber als String (führende Null bei z. B. 01067). Datumswerte gibt es in JSON nicht als eigenen Typ – sie werden als String gespeichert.
Bewertung: je Vorteil 2 P, Nachteil 2 P (jeweils 1 P für die Aussage aus dem Text, 1 P für den Bezug zum VeloManager).
SELECT FahrradNr, Modell FROM Fahrrad WHERE FahrradNr NOT IN (SELECT FahrradNr FROM Ausleihe);
| FahrradNr | Modell |
|---|---|
| 110 | Urban C6 |
Bewertung: äußere Abfrage 1 P, NOT IN 1 P, Unterabfrage 1 P.
Erklärung: NOT IN ist hier sicher, weil Ausleihe.FahrradNr NOT NULL ist. Enthielte die Unterabfrage einen NULL-Wert, wäre das Ergebnis leer – dann besser NOT EXISTS.
Eine nicht korrelierte Unterabfrage ist unabhängig von der äußeren Abfrage, wird einmal ausgewertet und ihr Ergebnis weiterverwendet (wie in a). Eine korrelierte Unterabfrage verweist auf eine Spalte der äußeren Abfrage und wird deshalb für jede Zeile der äußeren Abfrage erneut ausgewertet.
Bewertung: je Begriff 1 P.
SELECT AusleiheNr, KundenNr, Kilometer FROM Ausleihe WHERE Kilometer > (SELECT AVG(Kilometer) FROM Ausleihe) ORDER BY AusleiheNr;
| AusleiheNr | KundenNr | Kilometer |
|---|---|---|
| 5004 | 1001 | 92 |
| 5007 | 1002 | 118 |
| 5008 | 1006 | 315 |
Bewertung: Unterabfrage mit AVG 2 P, Vergleich in WHERE 1 P, SELECT 1 P. Durchschnitt: 885 / 11 = 80,45 km (NULL der laufenden Ausleihe wird ignoriert).
SELECT KundenNr, Vorname, Nachname
FROM Kunde
WHERE KundenNr IN (SELECT a.KundenNr
FROM Ausleihe a
WHERE a.FahrradNr IN (SELECT f.FahrradNr
FROM Fahrrad f
WHERE f.KategorieNr = (SELECT KategorieNr
FROM Kategorie
WHERE Bezeichnung = 'E-Lastenrad')))
ORDER BY KundenNr;
| KundenNr | Vorname | Nachname |
|---|---|---|
| 1001 | Lena | Hoffmann |
| 1004 | Paul | Wagner |
| 1005 | Sofia | Keller |
Gleichwertig mit EXISTS (getestet, gleiches Ergebnis):
SELECT k.KundenNr, k.Vorname, k.Nachname
FROM Kunde k
WHERE EXISTS (SELECT *
FROM Ausleihe a
INNER JOIN Fahrrad f ON f.FahrradNr = a.FahrradNr
INNER JOIN Kategorie kat ON kat.KategorieNr = f.KategorieNr
WHERE a.KundenNr = k.KundenNr
AND kat.Bezeichnung = 'E-Lastenrad');
Bewertung: äußere Abfrage 1 P, Unterabfrage auf Ausleihe 1 P, Unterabfrage auf Fahrrad 1 P, Kategorie über die Bezeichnung ermittelt 2 P. Eine reine JOIN-Lösung mit DISTINCT liefert dasselbe Ergebnis, erfüllt aber die Vorgabe „mit Unterabfragen“ nicht: max. 3 P.
Erklärung: Häufiger Fehler in a): WHERE Kilometer > AVG(Kilometer) – Aggregatfunktionen sind in WHERE nicht erlaubt, deshalb die Unterabfrage. In b) liefert die mittlere Unterabfrage mehrere Fahrräder, dort ist IN nötig; die innerste liefert genau eine Kategorie, dort genügt =.
SELECT f.FahrradNr, f.Modell, f.Kaufpreis
FROM Fahrrad f
WHERE f.Kaufpreis > (SELECT AVG(f2.Kaufpreis)
FROM Fahrrad f2
WHERE f2.KategorieNr = f.KategorieNr)
ORDER BY f.FahrradNr;
| FahrradNr | Modell | Kaufpreis |
|---|---|---|
| 106 | Trail X9 | 4490.00 |
| 108 | Cargo L2 | 5590.00 |
| 110 | Urban C6 | 2799.00 |
Durchschnittlicher Kaufpreis je Kategorie zur Kontrolle:
| KategorieNr | Schnitt |
|---|---|
| 1 | 2599.00 |
| 2 | 3199.00 |
| 3 | 4390.00 |
| 4 | 5490.00 |
| 5 | 649.00 |
Bewertung: zwei Aliase für dieselbe Tabelle 1 P, Unterabfrage mit AVG 2 P, Korrelation f2.KategorieNr = f.KategorieNr 2 P, SELECT 1 P. Alternativ: Join mit einer Unterabfrage in FROM (SELECT KategorieNr, AVG(Kaufpreis) AS Schnitt FROM Fahrrad GROUP BY KategorieNr).
Erklärung: Ohne Korrelation würde mit dem Gesamtdurchschnitt aller Fahrräder (3460,40 €) verglichen – das Ergebnis wären dann die Fahrräder 105 bis 108 (alle E-Mountainbikes und Lastenräder), das Fahrrad 110 fehlte. In Kategorie 2 kosten beide Räder gleich viel, Kategorie 5 hat nur ein Rad – dort liegt keins über dem Durchschnitt.
DELETE FROM Kunde
WHERE NOT EXISTS (SELECT * FROM Ausleihe a
WHERE a.KundenNr = Kunde.KundenNr)
AND NOT EXISTS (SELECT * FROM Ausleihe_Archiv aa
WHERE aa.KundenNr = Kunde.KundenNr);
Gelöscht werden 1008 Roth und 1010 Neumann (2 Zeilen). 1009 Krüger hat keine aktuelle Ausleihe, aber zwei archivierte (4002, 4005) und bleibt deshalb erhalten.
Bewertung: DELETE FROM Kunde 1 P, NOT EXISTS auf Ausleihe mit Korrelation 1 P, NOT EXISTS auf Ausleihe_Archiv 1 P, Verknüpfung mit AND 1 P, gelöschte Kunden 1 P.
SELECT AVG(Anzahl) AS AusleihenProKunde
FROM (SELECT KundenNr, COUNT(*) AS Anzahl
FROM Ausleihe
GROUP BY KundenNr) AS tbl;
| AusleihenProKunde |
|---|
| 1.7143 |
Bewertung: Unterabfrage mit COUNT und GROUP BY 1 P, Alias der Unterabfrage 1 P, äußeres AVG 1 P. Ergebnis: 12 Ausleihen / 7 Kunden ≈ 1,71.
| DDL | DML | DCL |
|---|---|---|
| CREATE TABLE, ALTER TABLE | UPDATE, DELETE | GRANT, REVOKE |
Bewertung: alle sechs richtig 2 P, vier oder fünf richtig 1 P.
| Spalte | Datentyp |
|---|---|
| Kunde.PLZ | CHAR(5) (oder VARCHAR(5)) |
| Fahrrad.Kaufpreis | DECIMAL(8,2) |
| Ausleihe.Beginn | DATE |
Begründung PLZ: Postleitzahlen können mit 0 beginnen (Kunde 1008: 01067 Dresden) – als Zahl ginge die führende Null verloren. Außerdem wird mit PLZ nicht gerechnet, und Mustersuchen wie LIKE '791%' setzen Text voraus.
Bewertung: drei passende Datentypen 2 P (Kaufpreis als DOUBLE: ½ P Abzug), Begründung PLZ 1 P.
Erklärung: INSERT, SELECT, UPDATE und DELETE (die CRUD-Befehle) sind DML, die Strukturbefehle CREATE, ALTER und DROP sind DDL, GRANT und REVOKE sind DCL. Für Geldbeträge wird DECIMAL statt DOUBLE erwartet, weil Gleitkommazahlen Rundungsfehler haben.
CREATE TABLE Wartung ( WartungNr INTEGER NOT NULL, FahrradNr INTEGER NOT NULL, Datum DATE NOT NULL, Art VARCHAR(30) NOT NULL DEFAULT 'Inspektion', Kosten DECIMAL(8,2) CHECK (Kosten >= 0), PRIMARY KEY (WartungNr), FOREIGN KEY (FahrradNr) REFERENCES Fahrrad (FahrradNr) );
Getestet: Ein INSERT ohne Art erhält 'Inspektion'; negative Kosten werden mit „CHECK constraint failed“, eine nicht existierende FahrradNr mit „FOREIGN KEY constraint failed“ abgewiesen.
Bewertung: Spalten mit sinnvollen Datentypen 1 P, PRIMARY KEY 1 P, FOREIGN KEY … REFERENCES 1 P, NOT NULL für die Pflichtfelder 1 P, DEFAULT 1 P, CHECK 1 P. AUTO_INCREMENT für WartungNr ist zulässig.
CREATE INDEX idx_kunde_nachname ON Kunde (Nachname);
Nachteil: zusätzlicher Speicherbedarf und langsamere Schreibzugriffe (INSERT, UPDATE, DELETE auf Kunde), weil der Index jeweils mit aktualisiert werden muss.
Bewertung: CREATE INDEX 2 P, Nachteil 1 P.
Erklärung: Die Tabelle Fahrrad muss vor Wartung existieren, sonst schlägt der FOREIGN KEY fehl. CHECK-Bedingungen prüft MySQL erst ab Version 8.0.16 (ältere Versionen ignorieren sie). Der Index auf Nachname lohnt sich, weil oft danach gesucht wird und die Kundentabelle stetig wächst.
ALTER TABLE Fahrrad ADD COLUMN Hersteller CHAR(3);
Bewertung: ALTER TABLE … ADD 1 P, Spaltenname und Datentyp 1 P.
UPDATE Fahrrad SET Hersteller = LEFT(Rahmennummer, 3); -- oder SUBSTRING(Rahmennummer, 1, 3)
| FahrradNr | Rahmennummer | Hersteller |
|---|---|---|
| 101 | WAW-23-0017 | WAW |
| 102 | WAW-23-0018 | WAW |
| 103 | KTX-24-1120 | KTX |
| 104 | KTX-24-1121 | KTX |
| 105 | MTN-22-0456 | MTN |
| 106 | MTN-25-0811 | MTN |
| 107 | CRG-24-0033 | CRG |
| 108 | CRG-25-0090 | CRG |
| 109 | KID-23-0301 | KID |
| 110 | WAW-26-0002 | WAW |
Bewertung: UPDATE … SET 1 P, Textfunktion mit richtigen Parametern 2 P. Ein WHERE ist nicht nötig, da alle Fahrräder betroffen sind.
ALTER TABLE Kunde ADD CONSTRAINT uq_kunde_email UNIQUE (Email); -- alternativ: CREATE UNIQUE INDEX uq_kunde_email ON Kunde (Email);
Die Kunden 1003 und 1007 sind kein Problem: UNIQUE verbietet nur doppelte Werte; NULL bedeutet „kein Wert“ und darf (in MySQL) mehrfach vorkommen. Getestet: Eine doppelte E-Mail wird abgewiesen, ein weiterer Kunde ohne E-Mail ist erlaubt.
Bewertung: Constraint 1 P, Erläuterung NULL 1 P.
Dialekt-Hinweis: SQL Server lässt bei einem UNIQUE-Constraint nur einen NULL-Wert zu; MySQL, PostgreSQL und SQLite erlauben mehrere.
CREATE TABLE Zubehoer ( ZubehoerNr INTEGER NOT NULL, Bezeichnung VARCHAR(40) NOT NULL, Tagespreis DECIMAL(5,2) NOT NULL, PRIMARY KEY (ZubehoerNr) ); CREATE TABLE Ausleihe_Zubehoer ( AusleiheNr INTEGER NOT NULL, ZubehoerNr INTEGER NOT NULL, Anzahl INTEGER NOT NULL DEFAULT 1, PRIMARY KEY (AusleiheNr, ZubehoerNr), FOREIGN KEY (AusleiheNr) REFERENCES Ausleihe (AusleiheNr), FOREIGN KEY (ZubehoerNr) REFERENCES Zubehoer (ZubehoerNr) );
Bewertung: Zubehoer mit Datentypen und PK 2 P; Ausleihe_Zubehoer: Spalten inkl. DEFAULT 1 1 P, zusammengesetzter PK 1 P, je FK 1 P (2 P). Zubehoer muss vor Ausleihe_Zubehoer angelegt werden. Getestet: Ein zweiter Eintrag mit gleicher AusleiheNr und ZubehoerNr wird wegen des PK abgewiesen.
Beide verbinden die Ergebnismengen zweier SELECT-Anweisungen untereinander. UNION entfernt dabei doppelte Zeilen (wie DISTINCT), UNION ALL behält alle Zeilen einschließlich Duplikaten und ist deshalb schneller.
Bewertung: 2 P (je Operator 1 P).
Beide SELECT-Anweisungen müssen dieselbe Anzahl Spalten mit kompatiblen Datentypen in gleicher Reihenfolge liefern.
Bewertung: 1 P.
Erklärung: Merkhilfe: UNION = Menge (ohne Doppelte), UNION ALL = Liste (alles). Die Spaltennamen des Gesamtergebnisses kommen aus dem ersten SELECT.
SELECT AusleiheNr, FahrradNr, Beginn, Betrag FROM Ausleihe WHERE KundenNr = 1001 UNION ALL SELECT AusleiheNr, FahrradNr, Beginn, Betrag FROM Ausleihe_Archiv WHERE KundenNr = 1001 ORDER BY Beginn;
| AusleiheNr | FahrradNr | Beginn | Betrag |
|---|---|---|---|
| 4001 | 101 | 2024-05-10 | 50.00 |
| 4008 | 101 | 2025-06-07 | 50.00 |
| 5001 | 101 | 2025-08-02 | 50.00 |
| 5004 | 103 | 2026-03-21 | 60.00 |
| 5009 | 108 | 2026-07-18 | 45.00 |
Bewertung: zwei SELECTs mit Bedingung 2 P, UNION ALL 1 P (UNION ist hier ebenfalls richtig, weil die AusleiheNr jede Zeile eindeutig macht), ORDER BY nur am Ende 1 P.
Mit UNION: 8 Zeilen (die KundenNr 1001–1007 und 1009), mit UNION ALL: 20 Zeilen (12 aus Ausleihe + 8 aus Ausleihe_Archiv). UNION entfernt die doppelten KundenNr – z. B. kommt 1001 dreimal in Ausleihe und zweimal im Archiv vor –, UNION ALL hängt alle Zeilen unverändert aneinander.
Bewertung: je Anzahl 1 P, Begründung 1 P.
Erklärung: In a) steht ORDER BY nur einmal am Ende und sortiert das Gesamtergebnis; ein ORDER BY im ersten SELECT wäre ein Syntaxfehler. In b) entfernt UNION Duplikate sowohl innerhalb einer Tabelle als auch über beide Tabellen hinweg.
SELECT Jahr, COUNT(*) AS Anzahl, SUM(Betrag) AS Umsatz, SUM(Kilometer) AS Kilometer
FROM (SELECT YEAR(Beginn) AS Jahr, Betrag, Kilometer
FROM Ausleihe
UNION ALL
SELECT YEAR(Beginn), Betrag, Kilometer
FROM Ausleihe_Archiv) AS alle
GROUP BY Jahr
ORDER BY Jahr;
| Jahr | Anzahl | Umsatz | Kilometer |
|---|---|---|---|
| 2024 | 6 | 294.00 | 309 |
| 2025 | 5 | 334.00 | 273 |
| 2026 | 9 | 732.00 | 742 |
Bewertung: innere SELECTs mit YEAR 2 P, UNION ALL 1 P, Unterabfrage in FROM mit Alias 2 P, COUNT/SUM 1 P, GROUP BY Jahr 1 P, ORDER BY 1 P.
Erklärung: 2026 enthält auch die laufende Ausleihe 5012 (Anzahl 9); deren NULL-Werte werden bei SUM ignoriert. Das Jahr 2025 kommt in beiden Tabellen vor (Archiv: 4007, 4008; aktuell: 5001–5003) – deshalb muss erst vereinigt und dann gruppiert werden.
UNION würde Zeilen entfernen, die in allen ausgewählten Spalten gleich sind, obwohl es verschiedene Ausleihen sind. Beispiel: Archiv 4008 und Ausleihe 5001 ergeben beide (2025; 50.00; 64 km) – mit UNION bliebe nur eine Zeile übrig, 2025 hätte dann 4 statt 5 Ausleihen, 284 € statt 334 € und 209 statt 273 km (getestet):
| Jahr | Anzahl | Umsatz | Kilometer |
|---|---|---|---|
| 2024 | 6 | 294.00 | 309 |
| 2025 | 4 | 284.00 | 209 |
| 2026 | 9 | 732.00 | 742 |
Bewertung: Begründung 1 P, konkretes Beispiel 1 P.
Der Kollege gruppiert jede Tabelle getrennt und hängt die Gruppenergebnisse aneinander. Da das Jahr 2025 in beiden Tabellen vorkommt, erscheint 2025 zweimal (einmal mit 3 Ausleihen aus Ausleihe, einmal mit 2 aus dem Archiv) – die Anforderung „jedes Jahr genau einmal“ ist verletzt. Richtig ist, erst beide Tabellen zu vereinigen und dann zu gruppieren (wie in a).
| Jahr | Anzahl | Umsatz | Kilometer |
|---|---|---|---|
| 2024 | 6 | 294.00 | 309 |
| 2025 | 3 | 224.00 | 143 |
| 2025 | 2 | 110.00 | 130 |
| 2026 | 9 | 732.00 | 742 |
Bewertung: Erläuterung (getrennte Gruppierung → doppelte Gruppe) 2 P, Bezug auf 2025 bzw. Lösungsweg 1 P.
Hinweis: In der Originalaufgabe S24 T2 4d funktionierte die Variante des Kollegen, weil dort alle älteren Jahre ausschließlich im Archiv lagen. Prüfe also immer, ob sich die Gruppen der beiden Tabellen überschneiden.
Jeder Benutzer bzw. jede Anwendung erhält nur die Rechte, die für die jeweilige Aufgabe unbedingt erforderlich sind. Dadurch werden Schäden durch Fehlbedienung, Missbrauch oder kompromittierte Zugangsdaten begrenzt und personenbezogene Daten nur Berechtigten zugänglich gemacht.
Bewertung: Erläuterung 1 P, Zweck/Nutzen 1 P.
REVOKE (z. B. REVOKE UPDATE ON velomanager.Kunde FROM benutzer;)
Bewertung: 1 P.
Erklärung: Das Prinzip gilt auch für Anwendungen: Der Datenbankbenutzer des Kundenportals erhält nur die DML-Rechte, die das Portal braucht – keine DDL- oder Administrationsrechte.
-- a) CREATE USER werkstatt IDENTIFIED BY 'Passwort'; -- b) GRANT SELECT ON velomanager.Fahrrad TO werkstatt; GRANT SELECT ON velomanager.Kategorie TO werkstatt; GRANT SELECT, INSERT, UPDATE ON velomanager.Wartung TO werkstatt; -- c) REVOKE UPDATE ON velomanager.Wartung FROM werkstatt;
Bewertung: a) 1 P; b) je GRANT-Anweisung 1 P (3 P); c) 1 P. In MySQL wird der Benutzer genauer als 'werkstatt'@'localhost' (oder @'%') angegeben – beide Schreibweisen sind richtig. Pro GRANT ist in MySQL nur ein Objekt erlaubt, daher zwei getrennte Anweisungen für Fahrrad und Kategorie.
Nach dem Prinzip der minimalen Rechte: Die Werkstatt muss Wartungen erfassen, aber keine Historie löschen. Gelöschte Wartungen würden Nachweise (Sicherheit, Gewährleistung, Kostenauswertung) vernichten; versehentliches oder missbräuchliches Löschen wird so ausgeschlossen.
Bewertung: 1 P.
Erklärung: Häufige Fehler: mehrere Tabellen in einer GRANT-Anweisung (in MySQL nicht erlaubt), REVOKE mit TO statt FROM, Passwort ohne Anführungszeichen. Maßgeblich für die Syntax ist in der Prüfung der Belegsatz.
| Rolle | Kunde | Fahrrad | Ausleihe | Wartung |
|---|---|---|---|---|
| Stationspersonal | S, I, U | S | S, I, U | – |
| Werkstatt | – | S | – | S, I, U |
| Controlling | S | S | S | S |
Bewertung: Stationspersonal 2 P, Werkstatt 1 P, Controlling 1 P.
CREATE ROLE stationspersonal; GRANT SELECT, INSERT, UPDATE ON velomanager.Kunde TO stationspersonal; GRANT SELECT, INSERT, UPDATE ON velomanager.Ausleihe TO stationspersonal; GRANT SELECT ON velomanager.Fahrrad TO stationspersonal; GRANT stationspersonal TO mueller;
Bewertung: CREATE ROLE 1 P, Rechte-Vergabe an die Rolle 2 P, Rolle dem Benutzer zuweisen 1 P. (In MySQL 8 wird die Rolle zusätzlich mit SET DEFAULT ROLE stationspersonal TO mueller; beim Anmelden aktiviert – nicht verlangt.)
Bewertung: je erläutertem Vorteil 1 P.
Erklärung: Zum Anlegen und Ändern gehört praktisch immer das Leserecht (sonst kann der Datensatz nicht gesucht werden) – deshalb steht S überall dabei. DELETE erhält keine Rolle, weil es in den Anforderungen nicht vorkommt.
Eine Stored Procedure ist eine in der Datenbank gespeicherte, benannte Folge von SQL-Anweisungen (ggf. mit Parametern, Variablen und Kontrollstrukturen), die mit CALL name(…) aufgerufen wird. Sie bündelt wiederkehrende Abläufe zentral im DBMS, verringert den Datenverkehr zwischen Anwendung und Datenbank und erlaubt, Benutzern nur das Ausführungsrecht statt Tabellenrechte zu geben.
Bewertung: Begriff 1 P, Aufruf/Zweck 1 P.
Ein Trigger ist SQL-Code, der automatisch ausgeführt wird, wenn auf einer bestimmten Tabelle ein Ereignis (INSERT, UPDATE oder DELETE) eintritt – vor (BEFORE) oder nach (AFTER) der Änderung, meist für jede betroffene Zeile. Einsatz z. B. für Protokollierung, Plausibilitätsprüfungen oder das Aktualisieren abgeleiteter Werte.
Bewertung: Begriff/automatische Auslösung durch Ereignis 1 P, Zeitpunkt oder Einsatzzweck 1 P.
Erklärung: Abgrenzung: Eine Stored Procedure wird ausdrücklich aufgerufen, ein Trigger läuft automatisch beim Ereignis. Beide sind in der Datenbank gespeichert – im Unterschied zu SQL-Anweisungen im Anwendungscode.
CREATE VIEW v_laufende_ausleihen AS SELECT a.AusleiheNr, a.Beginn, k.Nachname, f.Modell, s.Name AS Station FROM Ausleihe a INNER JOIN Kunde k ON k.KundenNr = a.KundenNr INNER JOIN Fahrrad f ON f.FahrradNr = a.FahrradNr INNER JOIN Station s ON s.StationNr = f.StationNr WHERE a.Ende IS NULL;
Ergebnis von SELECT * FROM v_laufende_ausleihen;:
| AusleiheNr | Beginn | Nachname | Modell | Station |
|---|---|---|---|---|
| 5012 | 2026-10-08 | Keller | Tour T7 | Messe |
Bewertung: CREATE VIEW … AS 1 P, Joins über vier Tabellen 1 P, WHERE a.Ende IS NULL 1 P, Ergebniszeile 1 P.
Vorteile (zwei genügen): komplexe Abfragen werden vereinfacht und wiederverwendet; Zugriffsschutz, weil Benutzer nur die freigegebenen Spalten/Zeilen sehen; die Anwendung bleibt stabil, wenn sich die darunterliegenden Tabellen ändern; immer aktuelle Daten, da die View keine Kopie speichert.
Bewertung: je Vorteil 1 P.
Erklärung: Die View speichert keine Daten, nur die Abfrage. Wird die Ausleihe 5012 zurückgegeben (Ende gesetzt), taucht sie beim nächsten Aufruf automatisch nicht mehr auf.
| Tabelle | Ereignis | Zeitpunkt |
|---|---|---|
| Ausleihe | INSERT (neue Ausleihe) | BEFORE – nur vor dem Einfügen kann die Änderung noch verhindert werden |
| Bedingung | Aktion | |
Es existiert bereits eine Ausleihe mit derselben FahrradNr (NEW.FahrradNr), deren Ende NULL ist. | Einfügen abbrechen und Fehlermeldung ausgeben („Fahrrad ist bereits verliehen“). | |
Bewertung: Tabelle und Ereignis 1 P, Zeitpunkt 1 P, Bedingung 1 P, Aktion 1 P.
Umsetzung in MySQL (nicht verlangt):
CREATE TRIGGER trg_keine_doppelausleihe
BEFORE INSERT ON Ausleihe
FOR EACH ROW
BEGIN
IF EXISTS (SELECT * FROM Ausleihe
WHERE FahrradNr = NEW.FahrradNr AND Ende IS NULL) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Fahrrad ist bereits verliehen';
END IF;
END;
Getestet (gleichwertiger SQLite-Trigger mit RAISE(ABORT, …)): Eine neue Ausleihe für Fahrrad 103 (laufende Ausleihe 5012) wird abgewiesen, eine für Fahrrad 101 wird angenommen.
Die Prozedur schließt eine laufende Ausleihe ab: Für die Ausleihe mit der übergebenen Nummer – aber nur, wenn sie noch kein Ende hat – setzt sie das Rückgabedatum auf das heutige Datum, speichert die gefahrenen Kilometer und berechnet den Betrag als Anzahl der Miettage (einschließlich Abholtag) × Tagespreis der Kategorie des Fahrrads. Bereits abgeschlossene Ausleihen bleiben unverändert.
CALL rueckgabe(5012, 84);
Ausleihe 5012: Beginn 08.10.2026, Fahrrad 103 (Trekking-E-Bike, 30,00 € je Tag). DATEDIFF('2026-10-10', '2026-10-08') + 1 = 3 Tage → 3 × 30,00 € = 90,00 €.
| AusleiheNr | KundenNr | FahrradNr | Beginn | Ende | Kilometer | Betrag |
|---|---|---|---|---|---|---|
| 5012 | 1005 | 103 | 2026-10-08 | 2026-10-10 | 84 | 90.00 |
Bewertung: Beschreibung 2 P, Aufruf 1 P, Betrag 1 P.
Vorteil: Die Regel gilt zentral für alle Anwendungen (Webportal, App, Stationsterminal) und kann nicht umgangen werden; weniger Datenübertragung. Nachteil: Herstellerspezifische Syntax (Bindung an das DBMS); Logik ist schwerer zu testen, zu debuggen und zu versionieren und für Entwickler „versteckt“.
Bewertung: je 1 P.
Erklärung: Zu a): Ein AFTER-Trigger käme zu spät – die doppelte Ausleihe wäre dann schon gespeichert. Zu b): Die Bedingung a.Ende IS NULL verhindert, dass eine bereits abgeschlossene Ausleihe durch einen zweiten Aufruf überschrieben wird. Typischer Rechenfehler: 2 statt 3 Tage, weil das „+ 1“ übersehen wird.
Vier aus: Vollständigkeit, Korrektheit (Richtigkeit), Konsistenz (Widerspruchsfreiheit), Einheitlichkeit, Eindeutigkeit (keine Dubletten), Atomarität, Gültigkeit/Plausibilität, Aktualität.
Bewertung: je Kriterium ½ P.
Bewertung: je Schritt 1 P.
Erklärung: „Nennen“ heißt: Der Begriff genügt. Bei „Erläutern“ (b) gehört zu jedem Schritt, was dort passiert.
Drei der folgenden Probleme mit je einer Folge:
| Problem (mit Beispiel aus Anlage 6) | Folge beim Import |
|---|---|
| Uneinheitliche, unvollständige oder ungültige Datumsangaben: „03.02.2025“, „4.2.“ (ohne Jahr), „2025-02-07“, „11.02.25“, „Feb 25“ (ohne Tag), „14.13.2025“ (Monat 13 existiert nicht). | Der Import in die Spalte Datum (DATE, NOT NULL) schlägt fehl oder es werden falsche Daten gespeichert (welches Jahr hat „4.2.“?); zeitliche Auswertungen werden falsch. |
| Fahrrad nicht eindeutig gekennzeichnet: mal FahrradNr („101“), mal Modell und Farbe („Urban C5 blau“), mal Rahmennummer, mal nur das Modell („Tour T7“ gibt es zweimal), mal zwei Räder in einem Feld („103/104“). | Der Fremdschlüssel FahrradNr kann nicht oder nur falsch zugeordnet werden → FK-Verletzung oder Wartungen beim falschen Fahrrad; die Wartungshistorie ist unbrauchbar. |
| Kosten nicht numerisch bzw. uneinheitlich: „89,00 €“, „145,5“, „ca. 130“, „inkl.“, „2x 89 €“, „29.90 EUR“ (Währungsangaben, Text, Schätzwerte, Sammelbeträge, unterschiedliche Dezimaltrennzeichen). | Der Import in DECIMAL schlägt fehl oder liefert falsche Beträge (z. B. „29.90“ als 2990 interpretiert); Kostenauswertungen je Fahrrad sind falsch. |
| Mehrere Werte in einem Feld / uneinheitliche Schreibweisen: Mechaniker „Weber, Sanchez“, Arbeiten „Insp. + Reifen“; „Weber“ / „T. Weber“, „Inspektion“ / „Insp.“. | Keine eindeutige Zuordnung zu einer Wartungsart bzw. einem Mitarbeiter; Auswertungen je Art oder Mechaniker zersplittern. |
| Fehlende Werte: Mechaniker leer. | NOT NULL-Verletzung bzw. unvollständige Datensätze. |
| Dubletten: Die Zeile „11.02.25 – 105 – Akku geprüft“ steht doppelt. | Die Wartung wird doppelt gespeichert; Anzahl und Kosten der Wartungen werden zu hoch ausgewiesen. |
Bewertung: je Problem 3 P (Beschreibung mit Beispiel aus der Anlage 2 P, Folge 1 P); maximal 9 P.
Erklärung: Der Operator „beschreiben“ verlangt den konkreten Bezug zur Anlage: „Datum uneinheitlich“ allein reicht nicht für die volle Punktzahl, „4.2. ohne Jahr, 14.13.2025 ungültig“ schon. Die Folge muss sich auf den Import in die Tabelle Wartung beziehen (Datentyp, Fremdschlüssel, Auswertungen).
| Spalte | Problem | Transformation |
|---|---|---|
| Name | Vor- und Nachname in einem Feld (nicht atomar), uneinheitliche Reihenfolge („Hoffmann, Lena“, „Jonas Albrecht“, „Schulze Mira“), abgekürzte Vornamen („Hoffmann, L.“). | Aufteilen in Vorname und Nachname: bei Komma ist der Teil davor der Nachname; ohne Komma Abgleich mit einer Vornamensliste; nicht eindeutige Fälle (Zeilen 3, 7) in eine Klärungsliste; Abkürzungen bei der Dublettenprüfung auflösen. |
| Adresse | Straße, PLZ und Ort in einem Feld, unterschiedliche Trennzeichen, teilweise fehlt die PLZ (Zeilen 3–6) oder nur ein Ortsteil („Wiehre“). | Per Muster zerlegen (fünfstellige Zahl = PLZ, Rest danach = Ort, davor = Straße/Hausnummer); fehlende PLZ über ein PLZ-Verzeichnis ergänzen bzw. nachfragen; die Straße wird in der Zieltabelle Kunde nicht gespeichert (oder Zieltabelle erweitern). |
| Ungültige Adressen („j.albrecht@web,de“, „s.keller(at)posteo.de“), fehlende Werte. | Syntaxprüfung (genau ein @, gültige Domain); bekannte Muster korrigieren („(at)“ → „@“, „,de“ → „.de“); fehlende Adressen als NULL übernehmen (Spalte erlaubt NULL); nicht reparierbare Adressen in die Klärungsliste. |
Bewertung: je Spalte 2 P (Problem 1 P, Transformation 1 P).
Bewertung: Vergleichsmerkmale/Erkennung 2 P, Zusammenführen/Bereinigen 1 P.
Beispiel für eine Entscheidung (Import mit Bereinigung): Die Kartei umfasst rund 950 Kunden, darunter Stammkunden. Eine Neuerfassung wäre sehr aufwendig (mehrere Personentage) und selbst fehleranfällig. Die meisten Mängel (Namensformat, Adressaufteilung, E-Mail-Tippfehler, Datumsformate) lassen sich regelbasiert per Skript beheben; nur Restfälle müssen manuell geklärt werden. Daher: Import über ETL mit Staging-Tabelle, Fehlerliste und Testimport – ggf. nur Kunden, die in den letzten Jahren aktiv waren (Datenminimierung nach DSGVO).
Ebenso vertretbar ist eine begründete Entscheidung gegen den Import, z. B.: hohe Fehlerquote, viele unvollständige Datensätze, Kunden registrieren sich für die Online-Buchung ohnehin neu, alte Daten inaktiver Kunden dürften nach DSGVO ohnehin nicht weiter gespeichert werden.
Bewertung: nachvollziehbare Entscheidung 1 P, mindestens zwei Argumente mit Bezug zur Situation 2 P.
Erklärung: Welche Transformationen nötig sind, bestimmt die Zieltabelle Kunde (Anlage 2): Sie verlangt Vor- und Nachname getrennt sowie PLZ und Ort – für Straße und Telefon gibt es dort keine Spalte. In c) gibt es Punkte für Argumente aus der Situation (rund 950 Datensätze, Art der Fehler, Nutzen der Historie), nicht für die Richtung der Entscheidung.