Der stille Killer: NULLs Auswirkungen auf die Datenbankleistung verstehen
James Reed
Infrastructure Engineer · Leapcell

Einleitung
In der Welt der Datenbanken ist das Konzept „NULL“ allgegenwärtig. Es steht für das Fehlen eines Wertes, ein Unbekanntes oder einen nicht anwendbaren Zustand. Obwohl scheinbar harmlos, unterschätzen Entwickler oft die tieferen Auswirkungen von NULL. Es ist nicht nur ein weiterer Datenpunkt; seine einzigartigen Eigenschaften können grundlegend verändern, wie eine Datenbank-Engine Abfragen optimiert und ausführt. Insbesondere können NULL-Werte die Effizienz kritischer Datenbankoperationen wie Indizierung, Aggregatfunktionen wie COUNT() und relationale JOIN-Operationen erheblich beeinträchtigen. Das Verständnis dieser Auswirkungen ist entscheidend für das Schreiben performanter und robuster Datenbankanwendungen. Dieser Artikel befasst sich damit, wie NULL diese Schlüsselbereiche stillschweigend beeinflusst und bietet ein klareres Bild seiner oft unterschätzten Macht.
Kernkonzepte
Bevor wir ins Detail gehen, definieren wir kurz einige Kernkonzepte, die für unsere Diskussion zentral sind:
- NULL: In SQL steht NULL für das Fehlen eines beliebigen Datenwertes. Es ist nicht gleichbedeutend mit Null, einem leeren String oder Falsch. Es ist eine spezielle Markierung, die anzeigt, dass Daten fehlen oder unbekannt sind.
 - Index: Ein Datenbankindex ist eine Datenstruktur, typischerweise ein B-Tree oder B+-Tree, die die Geschwindigkeit von Datenabrufoperationen auf einer Datenbanktabelle verbessert. Er ermöglicht es dem Datenbanksystem, die Zeilen schnell zu lokalisieren, die den Kriterien einer Abfrage entsprechen, ohne die gesamte Tabelle zu durchsuchen.
 - COUNT(): Eine Aggregatfunktion, die die Anzahl der Elemente in einer Gruppe zurückgibt. 
COUNT(*)zählt alle Zeilen, währendCOUNT(spaltenname)Nicht-NULL-Werte in der angegebenen Spalte zählt. - JOIN: Eine SQL-Klausel, die verwendet wird, um Zeilen aus zwei oder mehr Tabellen basierend auf einer zugehörigen Spalte dazwischen zu kombinieren. Häufige Typen sind 
INNER JOIN,LEFT JOIN,RIGHT JOINundFULL OUTER JOIN. - Kardinalität: Die Anzahl der eindeutigen Werte in einer Spalte. Hohe Kardinalität bedeutet viele eindeutige Werte, niedrige Kardinalität bedeutet wenige eindeutige Werte.
 - Selektivität: Das Verhältnis von eindeutigen Werten zur Gesamtzahl der Zeilen. Hohe Selektivität bedeutet, dass die Werte einer Spalte gut zum Filtern geeignet sind.
 
NULL und Indizes
Die Interaktion zwischen NULL-Werten und Indizes ist einer der kritischsten Bereiche, in denen die Leistung beeinträchtigt werden kann. Die meisten Datenbanksysteme behandeln NULLs spezifisch beim Erstellen und Traversieren von Indizes.
Wie Indizes NULLs behandeln:
Im Allgemeinen speichern B-Tree-Indizes, die der häufigste Typ sind, NULL-Werte nicht explizit in ihren Blattknoten für Spalten, die NULLs zulassen. Dies liegt daran, dass NULL keine inhärente Reihenfolge relativ zu anderen Werten hat und ihre Einbeziehung die Indexstruktur und die Traversierungslogik komplizieren würde. Das genaue Verhalten kann jedoch variieren:
- Die meisten Datenbanken (z.B. MySQL, PostgreSQL, SQL Server): Standardmäßig schließen Single-Column-B-Tree-Indizes im Allgemeinen keine Zeilen ein, bei denen die indizierte Spalte NULL ist. Das bedeutet, dass eine Abfrage wie 
SELECT * FROM meine_tabelle WHERE meine_spalte IS NULLtypischerweise zu einem vollständigen Tabellenscan führt, auch wennmeine_spalteeinen Index hat. Der Index wird einfach nicht verwendet, da er keine Einträge für NULLs enthält. - Zusammengesetzte Indizes: In einem zusammengesetzten (mehrspaltigen) Index kann eine Zeile, wenn eine der Spalten im Index für eine bestimmte Zeile NULL ist, nicht im Index enthalten sein oder ihr Eintrag wird auf besondere Weise gespeichert. Zum Beispiel werden in MySQL Zeilen indiziert, wenn alle Spalten im zusammengesetzten Index NICHT NULL sind. Wenn auch nur eine NULL ist, erscheint die Zeile möglicherweise überhaupt nicht im Index.
 - Spezialisierte NULL-Behandlung: Einige Datenbanken bieten spezifische Möglichkeiten, NULLs zu indizieren. PostgreSQL beispielsweise erlaubt „partielle Indizes“ oder die Verwendung von Ausdrücken in Indizes (
CREATE INDEX ON meine_tabelle ((meine_spalte IS NULL))), was NULLs erzwingen kann, indiziert zu werden. SQL Servers gefilterte Indizes können dies ebenfalls erreichen, indem sie eineWHERE-Klausel einschließen (CREATE INDEX ix ON MeineTabelle (Spalte1) WHERE Spalte1 IS NOT NULL). 
Beispiel: Auswirkung auf die Indexnutzung
Betrachten Sie die Tabelle auftraege mit einem Index auf lieferdatum.
CREATE TABLE auftraege ( auftrags_id INT PRIMARY KEY, kunden_id INT, auftragsdatum DATE NOT NULL, lieferdatum DATE -- Kann NULL sein, wenn noch nicht geliefert ); CREATE INDEX idx_lieferdatum ON auftraege (lieferdatum); -- Abfrage 1: Effizient, verwendet Index EXPLAIN ANALYZE SELECT * FROM auftraege WHERE lieferdatum = '2023-10-26'; -- Abfrage 2: Kann je nach DB zu einem vollständigen Tabellenscan führen, -- da NULLs normalerweise nicht indiziert werden EXPLAIN ANALYZE SELECT * FROM auftraege WHERE lieferdatum IS NULL; -- Abfrage 3: Beispiel für die Verwendung eines zusammengesetzten Index CREATE INDEX idx_kunde_lieferung ON auftraege (kunden_id, lieferdatum); -- Diese Abfrage könnte den zusammengesetzten Index für kunden_id verwenden, aber dann -- für NULL-Lieferdatum filtern, wenn die DB NULLs in zusammengesetzten Indizes nicht indiziert. EXPLAIN ANALYZE SELECT * FROM auftraege WHERE kunden_id = 123 AND lieferdatum IS NULL;
Praktische Auswirkungen:
- Ineffiziente 
IS NULL-Abfragen: Abfragen, die aufIS NULLfiltern, umgehen oft Standard-B-Tree-Indizes, was zu vollständigen Tabellenscans und schlechter Leistung führt, insbesondere bei großen Tabellen. - Indexgröße und Overhead: Wenn eine Spalte viele NULL-Werte enthält, aber Teil eines Indexes ist, kann der Index immer noch größer als nötig oder weniger effektiv sein, wenn viele kritische Abfragen 
IS NULL-Prädikate beinhalten. - Lösungen:
- Partielle/Gefilterte Indizes: Wenn Ihre Datenbank sie unterstützt, erstellen Sie einen Index speziell für NULL-Werte (
CREATE INDEX idx_null_lieferung ON auftraege ((lieferdatum IS NULL))) oder für Nicht-NULL-Werte (CREATE INDEX idx_nicht_null_lieferung ON auftraege (lieferdatum) WHERE lieferdatum IS NOT NULL). - Standardwerte: Wenn möglich und semantisch korrekt, weisen Sie einen Standard-Nicht-NULL-Wert zu (z. B. ein bestimmtes Datum 
9999-12-31für nicht geliefert oder0für ein ganzzahliges Feld). Dies kann die Anwendungslogik jedoch komplizieren. - Separate Tabellen: Für Spalten mit sehr hohem NULL-Prozentsatz und unterschiedlichen Abfragemustern ziehen Sie in Betracht, die Tabelle zu teilen oder einen Sparse-Column-Ansatz zu verwenden, was jedoch Komplexität hinzufügt.
 
 - Partielle/Gefilterte Indizes: Wenn Ihre Datenbank sie unterstützt, erstellen Sie einen Index speziell für NULL-Werte (
 
NULL und COUNT()-Leistung
Die Aggregatfunktion COUNT() verhält sich unterschiedlich, je Nachdem, ob es sich um COUNT(*) oder COUNT(spaltenname) handelt, und diese Unterscheidung hängt direkt mit NULL-Werten zusammen.
COUNT(*): Dies zählt alle Zeilen im Ergebnis-Set, unabhängig davon, ob eine Spalte NULLs enthält. Es ist im Allgemeinen die effizienteste Form vonCOUNT(), da viele Datenbanksysteme sie durch die Verwendung von Primärschlüssel-Indizes (die immer NICHT NULL sind) optimieren können oder einfach Zeilenanzahlen aus Metadaten abrufen.COUNT(spaltenname): Diese Form zählt nur die Zeilen, in denenspaltennameNICHT NULL ist. Wenn die angegebene Spalte NULL zulässt und viele NULL-Werte hat, kannCOUNT(spaltenname)langsamer sein alsCOUNT(*), da es für jede Zeile eine zusätzliche Prüfung auf NULL-Verhalten durchführen muss. Wenn die Spalte indiziert und NICHT NULL ist, kann auchCOUNT(spaltenname)sehr schnell sein.
Beispiel: COUNT()-Verhalten
SELECT COUNT(*) FROM auftraege; -- Zählt alle Zeilen SELECT COUNT(lieferdatum) FROM auftraege; -- Zählt Zeilen, bei denen lieferdatum NICHT NULL ist -- Angenommen, wir haben 100.000 Aufträge, von denen 50.000 ein NULL-Lieferdatum haben. -- COUNT(*) gibt schnell 100.000 zurück. -- COUNT(lieferdatum) gibt 50.000 zurück und könnte langsamer sein, -- da es die Spalte lieferdatum für jede Zeile untersuchen muss -- oder einen Index verwendet, der speziell NULLs ausschließt.
Leistungsauswirkungen:
COUNT(*)vs.COUNT(spaltenname): Wenn Sie die Gesamtzahl der Zeilen benötigen, bevorzugen Sie aus Leistungsgründen immerCOUNT(*). Wenn Sie gezielt die Nicht-NULL-Werte in einer Spalte zählen müssen, istCOUNT(spaltenname)angemessen, aber seien Sie sich seiner potenziellen Leistungseigenschaften im Vergleich zuCOUNT(*)bewusst.- Indexauswirkungen auf 
COUNT(spaltenname): Wennspaltennameindiziert und NICHT NULL ist, kannCOUNT(spaltenname)sehr schnell sein, da der Optimizer den Index zum Zählen der Einträge verwenden kann. Wennspaltennamejedoch NULL zulässt, wird der Index möglicherweise nicht vollständig genutzt, oder die Datenbank muss möglicherweise immer noch die tatsächlichen Zeilen scannen, um NULLs für jede Zählung zu prüfen. 
Praktische Auswirkungen:
- Wählen Sie 
COUNT(*)strategisch: Verwenden SieCOUNT(*), wenn Sie die Gesamtzahl der Zeilen wünschen, nicht, wenn Sie sich speziell für Nicht-NULL-Werte in einer Spalte interessieren. - Betrachten Sie NOT NULL-Constraints: Wenn eine Spalte niemals NULL sein darf, erzwingen Sie einen 
NOT NULL-Constraint. Dies verbessert nicht nur die Datenintegrität, sondern ermöglicht es der Datenbank auch, bessere Annahmen für aggregierte Zählungen und die Indexnutzung zu treffen. 
NULL und JOIN-Leistung
NULL-Werte haben aufgrund der Drei-Werte-Logik von SQL (WAHR, FALSCH, UNBEKANNT) einen besonderen Einfluss auf JOIN-Operationen. Beim Vergleichen eines Wertes mit NULL ist das Ergebnis immer UNBEKANNT.
- 
INNER JOIN: Zeilen werden nur zurückgegeben, wenn es in beiden Tabellen eine Übereinstimmung gibt. Wenn die Join-Spalte in einer der Tabellen NULL enthält, wird sie niemals mit einem anderen NULL oder einem Nicht-NULL-Wert übereinstimmen, noch wird sie mit einem anderen NULL verbunden. Daher werden Zeilen mit NULL in der Join-Spalte vomINNER JOIN-Ergebnis ausgeschlossen. Dies ist im Allgemeinen performant, da weniger Zeilen abgeglichen werden müssen.SELECT o.auftrags_id, c.kunden_name FROM auftraege o INNER JOIN kunden c ON o.kunden_id = c.kunden_id; -- Wenn o.kunden_id für einen Auftrag NULL ist, erscheint dieser Auftrag hier nicht. - 
LEFT JOIN(undRIGHT JOIN): EinLEFT JOINgibt alle Zeilen aus der linken Tabelle und übereinstimmende Zeilen aus der rechten Tabelle zurück. Wenn es keine Übereinstimmung für eine Zeile in der linken Tabelle gibt, enthalten die Spalten aus der rechten Tabelle NULLs. Wenn die Join-Spalte in der rechten Tabelle NULL ist, wird sie nichts abgleichen, aber die Zeile aus der linken Tabelle wird trotzdem einbezogen. Wenn die Join-Spalte in der linken Tabelle NULL ist, verhält es sich effektiv wie einINNER JOINfür diesen spezifischen Wert, was bedeutet, dass es keine Übereinstimmung in den Nicht-NULL-Werten der rechten Tabelle findet, was zu NULLs für die Spalten der rechten Tabelle führt.SELECT o.auftrags_id, c.kunden_name FROM auftraege o LEFT JOIN kunden c ON o.kunden_id = c.kunden_id; -- Wenn o.kunden_id für einen Auftrag NULL ist, erscheint er trotzdem. -- c.kunden_name ist für diese Aufträge NULL. -- Wenn c.kunden_id für einen Kunden NULL ist, würde dieser Kunde niemals mit einem Auftrag verbunden. - 
FULL OUTER JOIN: Gibt alle Zeilen zurück, wenn es in einer der Tabellen eine Übereinstimmung gibt. Wenn einige Zeilen keine Übereinstimmung haben, treten NULLs auf der Seite der Tabelle auf, die keine Übereinstimmung hat. Auch hier erzeugen NULLs in den Join-Spalten selbst keine Übereinstimmung. 
Leistungsauswirkungen:
- Keine Gleichheit mit NULL: Die grundlegende Regel 
NULL = NULList FALSCH (oder genauer gesagt UNBEKANNT in der Drei-Werte-Logik von SQL). Daher können Sie nicht direkt auf NULL-Werte mit Standard-Gleichheitsoperatoren joinen. Wenn Sie viele NULLs in Ihren Join-Spalten haben und sie beabsichtigen, darauf zu joinen (z. B. „joinen, wenn beide unbekannt sind“), benötigen Sie eine spezielle Behandlung. - Komplexität der Join-Prädikate: Wenn Ihre 
JOIN-BedingungenOR-Klauseln beinhalten, umIS NULL-Szenarien zu behandeln (z. B.ON a.spalte = b.spalte OR (a.spalte IS NULL AND b.spalte IS NULL)), sind diese Prädikate generell viel schwieriger für den Optimizer, Indizes effizient zu nutzen. Dies kann zu weniger optimalen Join-Plänen führen, wie z. B. Hash-Joins oder Nested-Loop-Joins, die größere Teile der Tabellen scannen. - Indexverwendung nicht möglich: Wenn ein Join-Prädikat wie 
a.spalte = b.spalteSpalten beinhaltet, die häufig NULL-Werte enthalten, und der gewählte Index NULLs nicht ordnungsgemäß behandelt (wie oben erläutert), kann der Optimizer wählen, den Index nicht zu verwenden, da er weiß, dass er nicht alle relevanten Schlüssel enthält. 
Beispiel: Joinen mit NULLs
Nehmen wir an, wir möchten die Tabellen mitarbeiter und abteilungen verbinden.
CREATE TABLE mitarbeiter ( mitarbeiter_id INT PRIMARY KEY, mitarbeiter_name VARCHAR(50), abteilungs_id INT -- Kann NULL sein für nicht zugewiesene Mitarbeiter ); CREATE TABLE abteilungen ( abteilungs_id INT PRIMARY KEY, abteilungs_name VARCHAR(50) ); -- Stellen Sie sich vor, 'Max Mustermann' hat mitarbeiter.abteilungs_id = NULL. -- Stellen Sie sich vor, die Abteilung 'F&E' hat abteilungs_id = 1. -- Stellen Sie sich einen weiteren Mitarbeiter 'Erika Mustermann' vor, mitarbeiter.abteilungs_id = 1. -- INNER JOIN schließt Max Mustermann aus: SELECT e.mitarbeiter_name, a.abteilungs_name FROM mitarbeiter e INNER JOIN abteilungen a ON e.abteilungs_id = a.abteilungs_id; -- Ergebnisse: Erika Mustermann, F&E -- LEFT JOIN schließt Max Mustermann ein, mit NULL-Abteilung: SELECT e.mitarbeiter_name, a.abteilungs_name FROM mitarbeiter e LEFT JOIN abteilungen a ON e.abteilungs_id = a.abteilungs_id; -- Ergebnisse: Erika Mustermann, F&E -- Max Mustermann, NULL -- Versuch, NULLs zu verbinden (funktioniert aufgrund von Gleichheit nicht wie erwartet): -- Dies wird NICHT die NULL-Abteilungs-ID von Max Mustermann abgleichen, wenn eine andere Tabelle eine NULL zum Verbinden hätte. -- Zum Beispiel, wenn es eine 'unassigned_dept_id'-Spalte in 'abteilungen' gäbe, die NULL wäre. -- Die folgende Abfrage wird niemals übereinstimmen, wenn beide Seiten NULL sind: SELECT e.mitarbeiter_name, a.abteilungs_name FROM mitarbeiter e INNER JOIN abteilungen a ON e.abteilungs_id = a.abteilungs_id OR (e.abteilungs_id IS NULL AND a.abteilungs_id IS NULL); -- Diese `OR`-Bedingung erzwingt oft einen vollständigen Scan/teuren Join.
Praktische Auswirkungen:
- Verstehen Sie das NULL-Verhalten bei JOINs: Seien Sie sich explizit bewusst, dass 
NULL = NULLnicht wahr ist. Das überrascht Entwickler oft. - Vermeiden Sie komplexe JOIN-Prädikate: Wenn möglich, strukturieren Sie Ihre Daten oder bereiten Sie sie vor, um 
JOIN-Bedingungen zu vermeiden, dieIS NULL-Logik beinhalten, daOR-Bedingungen mit NULLs sich sehr negativ auf die Join-Leistung und die Indexnutzung auswirken können. - Daten bereinigen: Wenn NULL in einer Join-Spalte tatsächlich einen unbekannten oder nicht anwendbaren Zustand darstellt, ist es möglicherweise besser, diese Zeilen vor dem Join zu filtern oder sie explizit mit 
LEFT JOINzu behandeln. Wenn NULL etwas Spezifisches impliziert (z. B. „Standardabteilung“), sollten Sie erwägen, es durch einen gut definierten Nicht-NULL-Wert zu ersetzen, um Joins zu erleichtern. 
Fazit
NULLs in einer Datenbank sind mehr als nur leere Platzhalter; sie sind ein grundlegender Aspekt des SQL-Datenmodells mit tiefgreifenden Leistungsauswirkungen. Bei Indizes führen NULLs oft zu deren Ausschluss, was IS NULL-Abfragen zu vollständigen Tabellenscans zwingt. Bei COUNT() ist COUNT(*) in Bezug auf die Effizienz COUNT(spaltenname) überlegen, was auf den Ausschluss von NULLs zurückzuführen ist. Bei JOIN-Operationen stellen NULLs eine einzigartige Herausforderung dar, da sie Standard-Gleichheitsvergleiche verweigern, was Join-Prädikate potenziell verkompliziert und die Indexnutzung behindert. Indem Sie diese Verhaltensweisen verstehen, können Entwickler effizientere Schemata entwerfen, intelligentere Abfragen schreiben und letztendlich performantere Datenbankanwendungen erstellen. NULLs als die stillen Performance-Killer zu behandeln, die sie sein können, ist der Schlüssel zur Datenbankoptimierung.