NOT IN, NULL und Duplikate in der Kursdatenbank
Die Oberstufe verwaltet ihre Kurse in einer relationalen Datenbank mit den Tabellen Schueler(SchuelerID, Name, Klasse), Kurs(KursID, Titel, Lehrkraft) und Belegung(SchuelerID, KursID, Note). Die Note darf NULL sein, solange keine Leistungserhebung stattgefunden hat; es gibt 240 Schülerinnen und Schüler, 18 Kurse und 512 Belegungen, davon 37 ohne Note. Die Lehrkraft möchte alle Schülerinnen und Schüler ausgeben lassen, die keinen Kurs mit der Note 1,0 belegen. Eine Schülerin schlägt vor:
SELECT Name FROM Schueler WHERE SchuelerID NOT IN (SELECT SchuelerID FROM Belegung WHERE Note = 1.0);
Die Abfrage liefert 0 Zeilen, obwohl 12 Personen tatsächlich keinen Einser-Kurs haben. Eine zweite Abfrage mit einem Join über Schueler und Belegung liefert dagegen Namen doppelt, sobald jemand mehrere Kurse belegt. Im Protokoll steht der Satz: „NULL ist doch einfach ein leerer Wert, das kann man wie 0 behandeln.“ Die Fachlehrkraft verlangt eine Erklärung und eine korrigierte Abfrage.
1. Erklären Sie, warum die NOT-IN-Abfrage 0 Zeilen liefert, obwohl es 12 passende Personen gibt. Gehen Sie auf die dreiwertige Logik ein.
Read the worked answer
Semantik von NOT IN. x NOT IN (Teilabfrage) ist definiert als x <> ALL (Teilabfrage), also als Konjunktion von Vergleichen über alle Zeilen der Teilabfrage. Enthält die Teilabfrage mindestens einen NULL-Wert, dann ist jeder Vergleich x <> NULL nach den SQL-Regeln UNKNOWN – nicht FALSE, nicht TRUE. Eine Konjunktion, die UNKNOWN enthält und sonst TRUE ist, ergibt UNKNOWN. Ein WHERE filtert aber nur Zeilen, für die die Bedingung TRUE ist; UNKNOWN zählt wie FALSE.
Folge. Sobald nur eine Belegung ohne Note in der Teilabfrage steht, ist das Ergebnis für jeden Schüler UNKNOWN. Deshalb kommen 0 Zeilen zurück – unabhängig davon, ob die Person einen Einser-Kurs hat. Das ist kein Implementierungsfehler des DBMS, sondern die vorgeschriebene Semantik.
Zweiter Denkfehler. „NULL wie 0 behandeln“ ist falsch: NULL steht für „unbekannt/nicht vorhanden“, nicht für den Wert 0 oder den leeren String. Note = NULL ist niemals TRUE, sondern UNKNOWN; korrekt wäre Note IS NULL. Aus demselben Grund würde auch Note <> 1.0 die 37 Belegungen ohne Note nicht zurückgeben – ein häufiger stiller Datenverlust in Auswertungen. Die Lehrkraft sollte diese beiden Fälle getrennt prüfen lassen.
2. Geben Sie eine korrigierte SQL-Abfrage an und begründen Sie, warum sie ohne DISTINCT auskommt.
Read the worked answer
Korrekte Abfrage mit NOT EXISTS:
SELECT s.Name
FROM Schueler s
WHERE NOT EXISTS (
SELECT 1 FROM Belegung b
WHERE b.SchuelerID = s.SchuelerID
AND b.Note = 1.0
);
Begründung. EXISTS prüft nur, ob mindestens eine Zeile existiert; es gibt nie NULL als Vergleichsergebnis an den äußeren Filter weiter. Die Bedingung b.Note = 1.0 ist für Belegungen ohne Note UNKNOWN und selektiert sie damit nicht – das ist hier genau gewünscht, denn „keine Note“ ist kein Einser-Kurs. Die Korrelation über b.SchuelerID = s.SchuelerID sorgt dafür, dass pro Schüler entschieden wird.
Warum kein DISTINCT? Die äußere Abfrage liest ausschließlich aus Schueler, und SchuelerID ist dort Primärschlüssel. Jede Schülerzeile wird also höchstens einmal ausgewertet, unabhängig davon, wie viele Belegungen existieren. Ein Join mit anschließendem DISTINCT wäre ebenfalls korrekt, würde aber zuerst ein kartesisches Zwischenergebnis mit 512 Zeilen erzeugen und danach wieder deduplizieren – mehr Arbeit und eine Fehlerquelle, wenn man DISTINCT vergisst. Wer aus didaktischen Gründen den Join zeigt, sollte SELECT DISTINCT s.Name schreiben und erklären, warum die Duplikate entstehen.
Sonderfall. Sollen auch Personen erscheinen, die gar keine Belegung haben, ist NOT EXISTS die richtige Wahl; ein INNER JOIN würde sie verlieren.
3. Die Fachschaft überlegt, die Spalte Note künftig mit DEFAULT 0 und NOT NULL zu belegen. Bewerten Sie diesen Vorschlag aus Datenmodell-Sicht.
Read the worked answer
Vorteile. NOT NULL verhindert die dreiwertige Logik an dieser Stelle, Abfragen werden vorhersehbar, und Indizes sowie Aggregationen (AVG, COUNT) verhalten sich einfacher. Die 0 könnte als „noch nicht bewertet“ gelesen werden.
Probleme. 0 ist im bayerischen Punktesystem ein gültiger Wert (ungenügend, 0 Punkte). Die Bedeutung „nicht bewertet“ und „mit 0 bewertet“ wäre nicht mehr unterscheidbar; eine Durchschnittsberechnung würde unbenotete Belegungen als Nullleistung einrechnen und das Ergebnis systematisch verfälschen. Das ist ein Semantikfehler im Datenmodell, kein Schönheitsfehler.
Bessere Alternativen.
- NULL beibehalten und in Abfragen konsequent mit
IS NULLbzw.COALESCEarbeiten; die Bedeutung im Datenmodell dokumentieren. - Eine eigene Spalte
bewertet_am DATEeinführen; Note ist genau dann nicht NULL, wenn ein Datum gesetzt ist (Invariante, per CHECK oder Trigger prüfbar). - Eine separate Tabelle Leistung(BelegungID, Punkte, Datum) modellieren, sodass eine fehlende Zeile „nicht bewertet“ bedeutet und Mehrfachbewertungen möglich werden.
Urteil. DEFAULT 0 wäre nur akzeptabel, wenn 0 im System eindeutig „nicht bewertet“ hieße und das Punktesystem keine 0 kennt – beides ist hier nicht der Fall. Empfehlenswert ist Variante 2, weil sie die Unterscheidung ohne Schemawechsel in der Anwendung erlaubt.