NULL SQL ist nicht dasselbe wie leer, false oder 0 — und genau darin liegt die Quelle von Bugs, leeren Ergebnismengen und stillen Fehlern in Produktionsdatenbanken.
Inhaltsverzeichnis
- Einführung: Was NULL wirklich bedeutet
- Denkfehler 1: NULL = NULL
- Denkfehler 2: NOT IN mit NULL in Subquery
- Denkfehler 3: COUNT(*) vs. COUNT(Spalte)
- Denkfehler 4: NULL in Aggregatfunktionen
- Denkfehler 5: COALESCE vs. IFNULL vs. NVL
- Denkfehler 6: NULL in ORDER BY
- Denkfehler 7: NULL in booleschen Bedingungen
- Denkfehler 8: CONCAT mit NULL
- Denkfehler 9: UNIQUE erlaubt mehrere NULLs
- Denkfehler 10: Drei-wertige Logik
- Unterstützung
- Zusammenfassung
- FAQ
Einführung: Was NULL wirklich bedeutet
NULL SQL ist kein Wert — es ist die Abwesenheit eines Wertes. Das klingt philosophisch, hat aber sehr konkrete Auswirkungen auf jede einzelne WHERE-Bedingung, jeden JOIN, jede Aggregatfunktion und jeden Vergleich in einer SQL-Abfrage. Wer NULL wie einen normalen Wert behandelt, schreibt Bugs, die oft erst in Produktion sichtbar werden — und dann in Form von leeren Ergebnismengen, falschen Zählungen oder stillen Datenverlusten.
Das Problem ist, dass NULL in SQL eine sogenannte Drei-wertige Logik einführt: TRUE, FALSE und UNKNOWN. Jeder Vergleich mit NULL ergibt UNKNOWN — nicht TRUE und nicht FALSE. Eine WHERE-Bedingung gibt nur dann Zeilen zurück, wenn sie TRUE ergibt. UNKNOWN reicht nicht.
Denkfehler 1: NULL = NULL ergibt TRUE
Der grundlegendste NULL SQL-Denkfehler: NULL = NULL ergibt nicht TRUE, sondern UNKNOWN. NULL ist nicht gleich NULL. Deshalb findet WHERE spalte = NULL niemals Zeilen — auch nicht wenn die Spalte tatsächlich NULL enthält.
-- FALSCH: Findet keine Zeilen, auch wenn discount_percent NULL ist
SELECT * FROM sales_order WHERE discount_percent = NULL;
-- RICHTIG: IS NULL und IS NOT NULL verwenden
SELECT * FROM sales_order WHERE discount_percent IS NULL;
SELECT * FROM sales_order WHERE discount_percent IS NOT NULL;
-- Nachweis: NULL = NULL ergibt UNKNOWN
SELECT NULL = NULL; -- Ergebnis: NULL (UNKNOWN)
SELECT NULL IS NULL; -- Ergebnis: 1 (TRUE)
SELECT NULL IS NOT NULL; -- Ergebnis: 0 (FALSE)
SELECT NULL <=> NULL; -- Ergebnis: 1 (NULL-sicherer Vergleich in MySQL)
-- Praxisfall: Produkte ohne Sonderpreis finden
SELECT entity_id, sku, special_price
FROM catalog_product_entity_decimal
WHERE attribute_id = 78
AND special_price IS NULL; -- Korrekt
-- NICHT SO: Findet nichts!
SELECT entity_id, sku, special_price
FROM catalog_product_entity_decimal
WHERE attribute_id = 78
AND special_price = NULL; -- Findet immer 0 Zeilen
Denkfehler 2: NOT IN mit NULL in Subquery ist ein stiller Datenverlust
Dies ist der gefährlichste NULL SQL-Fallstrick. Wenn eine Subquery in einem NOT IN-Ausdruck auch nur einen einzigen NULL-Wert zurückgibt, ist das Gesamtergebnis immer eine leere Menge — ohne Fehlermeldung, ohne Warnung. Das passiert, weil NOT IN intern alle Werte mit != Wert vergleicht. Da x != NULL UNKNOWN ergibt, kann kein einziger Wert als "nicht enthalten" bestätigt werden.
-- Setup zum Nachvollziehen
CREATE TABLE orders (id INT, customer_id INT);
INSERT INTO orders VALUES (1, 10), (2, 20), (3, NULL);
CREATE TABLE vip_customers (customer_id INT);
INSERT INTO vip_customers VALUES (10), (30);
-- TÖDLICHER FALLSTRICK: NULL in Subquery macht NOT IN immer leer!
SELECT id FROM orders
WHERE customer_id NOT IN (SELECT customer_id FROM vip_customers);
-- Ergebnis: LEER! Wegen customer_id = NULL in orders
-- Warum? Intern wird ausgewertet:
-- customer_id NOT IN (10, 30)
-- = customer_id != 10 AND customer_id != 30
-- Für customer_id = NULL:
-- NULL != 10 --> UNKNOWN
-- NULL != 30 --> UNKNOWN
-- UNKNOWN AND UNKNOWN --> UNKNOWN --> Zeile wird nicht zurückgegeben
-- RICHTIG 1: WHERE NOT EXISTS (empfohlen, NULL-sicher)
SELECT o.id FROM orders o
WHERE NOT EXISTS (
SELECT 1 FROM vip_customers v
WHERE v.customer_id = o.customer_id
);
-- RICHTIG 2: LEFT JOIN / IS NULL Pattern
SELECT o.id
FROM orders o
LEFT JOIN vip_customers v ON o.customer_id = v.customer_id
WHERE v.customer_id IS NULL;
-- RICHTIG 3: NULL explizit ausschließen
SELECT id FROM orders
WHERE customer_id IS NOT NULL
AND customer_id NOT IN (SELECT customer_id FROM vip_customers);
Denkfehler 3: COUNT(*) und COUNT(Spalte) machen dasselbe
Ein weit verbreiteter NULL SQL-Irrtum: COUNT(*) zählt alle Zeilen, unabhängig vom Inhalt. COUNT(spalte) hingegen ignoriert NULL-Werte in dieser Spalte. Das führt zu unterschiedlichen Ergebnissen, die ohne NULL-Kenntnisse schwer zu erklären sind.
-- Tabelle mit NULLs in einer Spalte
CREATE TABLE survey (
id INT,
rating INT -- kann NULL sein (Frage übersprungen)
);
INSERT INTO survey VALUES (1, 5), (2, NULL), (3, 3), (4, NULL), (5, 4);
-- COUNT(*): Zählt alle 5 Zeilen
SELECT COUNT(*) FROM survey; -- Ergebnis: 5
-- COUNT(rating): Ignoriert NULL, zählt nur 3
SELECT COUNT(rating) FROM survey; -- Ergebnis: 3
-- COUNT(DISTINCT rating): Unique Nicht-NULL-Werte
SELECT COUNT(DISTINCT rating) FROM survey; -- Ergebnis: 3 (3, 4, 5)
-- Praxisfall Magento: Bestellungen vs. Bestellungen mit Kommentar
SELECT
COUNT(*) AS total_orders,
COUNT(customer_note) AS orders_with_note,
COUNT(*) - COUNT(customer_note) AS orders_without_note
FROM sales_order
WHERE created_at >= CURRENT_DATE - INTERVAL 30 DAY;
Denkfehler 4: NULL in Aggregatfunktionen wird mitgezählt
Alle Aggregatfunktionen außer COUNT(*) ignorieren NULL-Werte: SUM, AVG, MIN, MAX. Das kann zu irreführenden Ergebnissen führen, besonders bei AVG: Der Durchschnitt wird nur über die Nicht-NULL-Werte berechnet, was einen höheren Wert ergibt als wenn NULLs als 0 gezählt würden.
-- Tabelle: Produktbewertungen (nicht alle Kunden bewerten)
CREATE TABLE reviews (product_id INT, score TINYINT);
INSERT INTO reviews VALUES (1, 5), (1, 4), (1, NULL), (1, NULL), (1, 3);
-- AVG ignoriert NULL: Berechnet (5+4+3)/3 = 4.0, nicht (5+4+0+0+3)/5 = 2.4
SELECT AVG(score) FROM reviews WHERE product_id = 1; -- 4.0
-- SUM ignoriert NULL
SELECT SUM(score) FROM reviews WHERE product_id = 1; -- 12 (nicht 12+0+0)
-- AVG mit NULLs als 0 (wenn das die richtige Interpretation ist):
SELECT AVG(COALESCE(score, 0)) FROM reviews WHERE product_id = 1; -- 2.4
-- Praxisfall: Magento Produktbewertungen
-- review_detail.title kann NULL sein
SELECT
r.entity_pk_value AS product_id,
COUNT(*) AS total_reviews,
COUNT(rd.title) AS reviews_with_title,
ROUND(AVG(rv.value), 1) AS avg_rating
FROM review r
JOIN review_detail rd ON rd.review_id = r.review_id
JOIN rating_option_vote rv ON rv.review_id = r.review_id
GROUP BY r.entity_pk_value
ORDER BY total_reviews DESC
LIMIT 10;
Denkfehler 5: COALESCE, IFNULL und NVL sind dasselbe
In MySQL gibt es COALESCE und IFNULL für den Umgang mit NULL SQL. Beide ersetzen NULL durch einen Ersatzwert, haben aber einen wichtigen Unterschied: IFNULL nimmt genau zwei Argumente, COALESCE nimmt beliebig viele und gibt den ersten Nicht-NULL-Wert zurück. NVL ist eine Oracle-Funktion und existiert in MySQL nicht.
-- IFNULL: zwei Argumente
SELECT IFNULL(discount_amount, 0) FROM sales_order; -- NULL -> 0
-- COALESCE: beliebig viele Argumente, erster Nicht-NULL-Wert
SELECT COALESCE(special_price, group_price, price) AS effective_price
FROM catalog_product_entity_decimal;
-- Gibt special_price wenn nicht NULL, sonst group_price, sonst price
-- NULLIF: Gibt NULL zurück wenn beide Argumente gleich sind
-- Nützlich um Division-durch-Null zu verhindern
SELECT total_revenue / NULLIF(order_count, 0) AS avg_order_value
FROM sales_summary;
-- Wenn order_count = 0: NULLIF gibt NULL zurück, Division ergibt NULL (kein Fehler)
-- Magento: Preis-Fallback-Logik
SELECT
e.entity_id,
e.sku,
COALESCE(
MIN(CASE WHEN a.attribute_id = 78 THEN a.value END), -- special_price
MIN(CASE WHEN a.attribute_id = 64 THEN a.value END) -- price
) AS effective_price
FROM catalog_product_entity e
JOIN catalog_product_entity_decimal a ON a.entity_id = e.entity_id
WHERE a.store_id = 0
GROUP BY e.entity_id, e.sku
LIMIT 20;
Denkfehler 6: NULL in ORDER BY verhält sich vorhersehbar
In MySQL werden NULL-Werte bei ORDER BY ASC zuerst sortiert (kleiner als alle anderen Werte) und bei ORDER BY DESC zuletzt. Das entspricht dem SQL-Standard-Verhalten. MySQL 8 unterstützt NULLS FIRST und NULLS LAST Klauseln für explizite Kontrolle.
-- Standard MySQL: NULL kommt bei ASC zuerst, bei DESC zuletzt
SELECT id, created_at FROM sales_order ORDER BY created_at ASC;
-- NULLs erscheinen am Anfang
SELECT id, created_at FROM sales_order ORDER BY created_at DESC;
-- NULLs erscheinen am Ende
-- MySQL 8: Explizite NULLS FIRST / NULLS LAST Kontrolle
SELECT id, discount_amount
FROM sales_order
ORDER BY discount_amount DESC NULLS LAST;
-- NULLs am Ende, auch bei DESC
-- Workaround für ältere MySQL-Versionen (NULLS LAST bei ASC):
SELECT id, discount_amount
FROM sales_order
ORDER BY
discount_amount IS NULL ASC, -- NULLs zuletzt (0 für nicht-NULL, 1 für NULL)
discount_amount ASC;
-- Praxisfall: Magento-Produkte mit leerem Beschreibungsfeld zuletzt
SELECT entity_id, sku
FROM catalog_product_entity
ORDER BY
(short_description IS NULL) ASC,
short_description ASC;
Denkfehler 7: WHERE flag = 0 findet alle "nicht gesetzten" Zeilen
Ein klassischer NULL SQL-Bug in Magento und anderen Systemen: Eine WHERE-Bedingung wie WHERE is_active = 0 findet nur Zeilen, wo is_active explizit 0 ist — nicht Zeilen, wo is_active NULL ist. NULL ist weder TRUE noch FALSE.
-- Tabelle mit gemischten Werten
CREATE TABLE products (id INT, is_active TINYINT);
INSERT INTO products VALUES (1, 1), (2, 0), (3, NULL), (4, NULL);
-- FALSCH: Findet nur id=2, NICHT id=3 und id=4!
SELECT * FROM products WHERE is_active = 0; -- Ergebnis: nur id=2
-- FALSCH: Findet nur id=1
SELECT * FROM products WHERE is_active != 0; -- Ergebnis: nur id=1
-- RICHTIG: Alle inaktiven (0 oder NULL)
SELECT * FROM products
WHERE is_active = 0 OR is_active IS NULL; -- id=2, id=3, id=4
-- Oder mit COALESCE:
SELECT * FROM products
WHERE COALESCE(is_active, 0) = 0; -- id=2, id=3, id=4
-- Praxisfall Magento: Alle nicht-sichtbaren Produkte (visibility = 1 oder NULL)
SELECT entity_id, sku
FROM catalog_product_entity e
WHERE NOT EXISTS (
SELECT 1 FROM catalog_product_entity_int a
WHERE a.entity_id = e.entity_id
AND a.attribute_id = (SELECT attribute_id FROM eav_attribute WHERE attribute_code = 'visibility')
AND a.value IN (2, 3, 4) -- catalog, search, both
);
Denkfehler 8: CONCAT mit NULL ergibt leeren String
In MySQL gibt die Standard-Konkatenation mit CONCAT NULL zurück, sobald ein Argument NULL ist — nicht einen leeren String. Das führt zu "unsichtbaren" Fehlern, wenn man zusammengesetzte Strings aus Spalten baut, die NULL enthalten können.
-- FALSCH: Ergibt NULL wenn middle_name NULL ist!
SELECT CONCAT(first_name, ' ', middle_name, ' ', last_name) AS full_name
FROM customer_entity;
-- Für customer ohne middle_name: NULL, nicht 'Max Mustermann'
-- RICHTIG 1: CONCAT_WS (Separator-Version ignoriert NULL automatisch)
SELECT CONCAT_WS(' ', first_name, middle_name, last_name) AS full_name
FROM customer_entity;
-- CONCAT_WS ignoriert NULL-Argumente, fügt Separator nur zwischen Nicht-NULL ein
-- RICHTIG 2: COALESCE für einzelne Felder
SELECT CONCAT(first_name, ' ', COALESCE(middle_name, ''), ' ', last_name) AS full_name
FROM customer_entity;
-- Praxisfall Magento: Vollständige Adresse zusammensetzen
SELECT
a.entity_id,
CONCAT_WS(' ',
a.firstname,
a.middlename,
a.lastname
) AS full_name,
CONCAT_WS(', ',
a.street,
a.city,
CONCAT_WS(' ', a.postcode, a.region),
a.country_id
) AS full_address
FROM customer_address_entity a
LIMIT 10;
Denkfehler 9: UNIQUE Constraint lässt nur einen NULL-Wert zu
SQL-Standard und MySQL erlauben mehrere NULL-Werte in einer UNIQUE-Spalte. NULL ist nicht gleich NULL (Denkfehler 1), also betrachtet die UNIQUE-Constraint zwei NULLs nicht als Duplikat. Das ist überraschend, aber korrekt — und kann beim Einfügen von Daten zu unerwartetem Verhalten führen.
-- UNIQUE-Spalte mit NULL: mehrere NULLs sind erlaubt
CREATE TABLE subscriptions (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE,
confirmed_at DATETIME -- Kein UNIQUE, aber als Beispiel
);
-- Mehrere NULLs einfügen: Kein Constraint-Fehler!
INSERT INTO subscriptions (email) VALUES (NULL); -- OK
INSERT INTO subscriptions (email) VALUES (NULL); -- Auch OK!
INSERT INTO subscriptions (email) VALUES (NULL); -- Auch OK!
-- Prüfen
SELECT * FROM subscriptions; -- Zeigt 3 Zeilen mit email = NULL
-- Eindeutige emails sind normal beschränkt:
INSERT INTO subscriptions (email) VALUES ('test@example.com'); -- OK
INSERT INTO subscriptions (email) VALUES ('test@example.com'); -- FEHLER: Duplicate entry
-- Praxisfall Magento: customer_entity.email ist UNIQUE
-- Aber: Gibt es Kunden ohne Email?
SELECT entity_id FROM customer_entity WHERE email IS NULL;
-- UNIQUE Constraint in MySQL: prüfen
SHOW INDEX FROM customer_entity WHERE Non_unique = 0;
Denkfehler 10: WHERE NOT (Bedingung) erfasst alle anderen Fälle
Das ist der subtilste NULL SQL-Denkfehler: WHERE NOT (condition) erfasst nicht die Fälle, bei denen die Bedingung UNKNOWN ist — also Fälle mit NULL-Beteiligung. Das ist eine direkte Konsequenz der Drei-wertigen Logik: NOT TRUE = FALSE, NOT FALSE = TRUE, aber NOT UNKNOWN = UNKNOWN. Und UNKNOWN wird von WHERE nicht zurückgegeben.
-- Drei-wertige Logik in MySQL
SELECT
NOT TRUE AS not_true, -- 0 (FALSE)
NOT FALSE AS not_false, -- 1 (TRUE)
NOT NULL AS not_null, -- NULL (UNKNOWN)
NOT UNKNOWN AS not_unknown; -- NULL (UNKNOWN)
-- Konsequenz: Zwei Queries decken nicht alle Zeilen ab!
-- Query A: aktive Produkte
SELECT id FROM products WHERE is_featured = 1;
-- Query B: nicht-aktive Produkte
SELECT id FROM products WHERE NOT (is_featured = 1);
-- = WHERE is_featured != 1
-- Findet NICHT die Zeilen wo is_featured IS NULL!
-- Vollständige Abdeckung aller drei Fälle:
SELECT id, is_featured,
CASE
WHEN is_featured = 1 THEN 'featured'
WHEN is_featured = 0 THEN 'not_featured'
WHEN is_featured IS NULL THEN 'unknown'
END AS status
FROM products;
-- Praxisfall: Magento Bestellungen nach Zahlungsstatus
SELECT
COUNT(*) FILTER (WHERE payment_captured = 1) AS captured,
COUNT(*) FILTER (WHERE NOT (payment_captured = 1) AND payment_captured IS NOT NULL) AS not_captured,
COUNT(*) FILTER (WHERE payment_captured IS NULL) AS status_unknown
FROM sales_order;
-- (MySQL: FILTER ist nicht verfügbar, CASE WHEN verwenden)
SELECT
SUM(CASE WHEN payment_captured = 1 THEN 1 ELSE 0 END) AS captured,
SUM(CASE WHEN payment_captured = 0 THEN 1 ELSE 0 END) AS not_captured,
SUM(CASE WHEN payment_captured IS NULL THEN 1 ELSE 0 END) AS status_unknown
FROM sales_order;
Mironsoft
NULL SQL-Bugs in Ihrem Shop finden und beheben
NULL-bedingte Bugs sind in Produktionsdatenbanken häufig und schwer zu finden. Wir analysieren Ihre kritischen SQL-Abfragen, Reports und Magento-Queries auf NULL-Fallstricke.
Query Review
NULL-Fallstricke, NOT IN mit Subquery und drei-wertige Logik in Ihren Queries prüfen
Report-Analyse
COUNT- und AVG-Fehler durch NULL in Reporting-Abfragen identifizieren
Magento-Diagnose
EAV-NULL-Probleme, fehlende Attributwerte und stille Dateninkonsistenzen aufdecken
Zusammenfassung
NULL SQL ist keine Randnotiz — es ist einer der häufigsten Quellen stiller Bugs in Produktionsdatenbanken. Die wichtigsten Regeln: Immer IS NULL statt = NULL verwenden. Bei NOT IN mit Subqueries immer prüfen, ob die Subquery NULLs zurückgeben kann — wenn ja, auf NOT EXISTS oder LEFT JOIN umsteigen. COUNT(*) und COUNT(spalte) sind nie identisch wenn NULLs in der Spalte vorkommen können. Und: WHERE NOT (bedingung) ist keine vollständige Negation wenn NULLs beteiligt sind.
NULL SQL — Die 10 Denkfehler auf einen Blick
Vergleich
NULL = NULL ergibt UNKNOWN, nicht TRUE. Immer IS NULL / IS NOT NULL verwenden.
NOT IN Falle
NOT IN mit NULL in Subquery gibt immer leere Menge. NOT EXISTS oder LEFT JOIN verwenden.
COUNT
COUNT(*) zählt alle Zeilen, COUNT(spalte) ignoriert NULL. Unterschied kennen und bewusst wählen.
Drei-wertige Logik
TRUE / FALSE / UNKNOWN. WHERE gibt nur TRUE zurück. NOT UNKNOWN bleibt UNKNOWN.