Zu jeder Art von Datenverarbeitung und Analyse gehört auch eine Daten-Vorverarbeitung. Das NULL-Handling ist dabei ein essenzieller Teil. NULL-Werte treten dann auf, wenn Daten schlicht fehlen, z.B. wenn jemand das optionale Feld Telefonnummer oder Geschlecht nicht angeben möchte. Das bedeutet, es sind durchaus nicht immer Fehler, wenn NULL-Werte vorkommen. Wie und ob wir NULLs behandeln und bereinigen müssen, ist häufig vom Use-Case und der Art der Daten abhängig. Voraussetzung dafür ist jedoch zu verstehen, wie Snowflake NULL-Werte interpretiert. Snowflake hat ein eigenes NULL-Verständnis und eigene Funktionen, um mit NULLs umzugehen.
Hier sind 3 Lektionen aus der Praxis und 3 Honigtöpfe, in die man bei Unachtsamkeit leicht hinein stapfen kann.
Lektion 1
Bei Datenbanken denken wir meist an Tabellen mit Spalten und Zeilen, ein bisschen wie bei Excel. Dies entspricht häufig aber nur der logischen Sicht. Snowflake ordnet Zeilen einer Partition zu, aber intern speichert es die Daten nicht Zeile für Zeile, sondern verwaltet jede Spalte separat. Snowflake ist columnar.
Für jede Tabelle und zu jeder Mikropartition werden Metadaten gespeichert, die es Snowflake ermöglichen Daten zum einen besser zu komprimieren und zum anderen Abfragen zu optimieren. Query-Performance durch Prunen: Snowflake analysiert vor der Ausführung einer Query, welche Partitionen für das Ergebnis gar nicht erst angeschaut werden müssen.
Zu diesen Statistiken in den Metadaten gehören auch row-counts. Das heisst, wenn ein select count(*) abgefragt wird, dann wird nicht die gesamte Tabelle gescannt, wie in vielen anderen Datenbanken, sondern das Ergebnis ist sofort verfügbar, ohne dass irgend eine Berechnung erfolgen muss. Es ist dafür kein Warehouse von Nöten. Es ist ein Metadata-based Result. Nice Feature.
Nehmen wir an myTable1 hat vier Spalten. In col2 und col3 gibt es NULL-Werte, z.B wie folgt:
myTable1 | |||
COL1 | COL2 | COL3 | COL4 |
1 | col2 | 3 | t1 col4 1 |
2 | col2 | null | t1 col4 2 |
3 | null | 2 | t1 col4 3 |
Dann werden folgende Abfragen u.U. überraschen.
(1) select count(*) from myTable1;
>> 3
(2) select count(col3) from myTable1;
>> 2
(3) select count(t.*) from myTable1 as t;
>> 1
Die Query (3) gibt die Anzahl aller Zeilen zurück, die gar keine NULLs enthalten. Dieser Count muss berechnet werden, ist also nicht zu verwechseln mit count(*). Die Abfrage (3) kann allerdings gut verwendet werden um festzustellen, ob überhaupt NULLs in den Daten vorhanden sind.
(2) ist auch ein Metadata-based Result. Das bedeutet, es werden zu jeder Spalte Metadaten gespeichert. Hier zählen hingegen bei Snowflake die NULL-Werte nicht mit.
Lektion 2
Snowflake verfügt, wie jede andere Datenbank auch, über Vergleichs-Operatoren, mit denen zwei Werte miteinander verglichen werden können:
- Gleichheit: =, != oder <>
- Rang: <, >
- Kombiniert: <=, >=
Wann immer einer dieser Operatoren auf NULL trifft, gibt er NULL zurück, und nicht TRUE oder FALSE. Snowflake verarbeitet hier also nicht binär, sondern in {TRUE, FALSE, NULL}.
Diese Lektion klingt simpel und nachvollziehbar, hat aber Konsequenzen über die man sich nicht gleich im Klaren ist. Im Folgenden werde ich in meinen Honigtöpfen darauf noch näher eingehen. Zum Einstimmen bereits einige Beispiele:
Select 1 = 1; --> TRUE
Select 1 = 0; --> FALSE
Select 0 < 1; --> TRUE
Select '' = NULL; --> NULL
Select 1 > NULL; --> NULL
Select NULL = NULL --> NULL
Insbesondere wird NULL = NULL nicht von allen Datenbanken so gehandhabt.
Zudem gibt es noch einen weiteren Operator der explizit auf NULL testet: is.
Select NULL is NULL --> TRUE
Select '' is not NULL --> TRUE
Select NULL is not NULL --> FALSE
Lektion 3
Wenn man weiß, wie es geht, ist vieles einfach. Diese Lektion gehört dazu. Snowflake verfügt über eine Snowflake-spezifische NULL-safe-Funktion: EQUAL_NULL(). Sie verbindet die beiden Operatoren = und is miteinander. NULL wird hier wie etwas Bekanntes, wie ein Wert wie alle anderen, behandelt. EQUAL_NULL() ist also binär und kann beides, auf NULL und auf Gleichheit testen:
Select equal_null(1,1); --> TRUE
Select equal_null(NULL, NULL) --> TRUE
Select equal_null(1, NULL) --> FALSE
Soweit so gut. Kommen wir zu unseren Honigtöpfen.
Honigtopf 1
Dieser Honigtopf zielt auf die Lektion 1. Je nachdem wie die count()-Funktion verwendet wird, erhalten wir unterschiedliche Ergebnisse für gewisse Statistiken. Statistiken wie der Median, Mittelwert oder die Standardabweichung beziehen die Anzahl der Zeilen mit ein. Standardmäßig werden bei Snowflake hier nur Nicht-NULL-Werte betrachtet. Wenn man hingegen die reine Definition eines Mittelwertes selber umgesetzt hat (2), dann kann es zu Irritationen kommen.
(1) Select avg(col3) from myTable1;
>> 2.5
(2) Select sum(col3)/count(*) from myTable1;
>> 1.666667
(3) Select sum(col3)/count(t.*) from myTable1 as t;
>> 5
(4) Select col2, count(col2) as "#" from myTable1 group by col2;
>> COL2 #
col2 2
null 0
Die Query (2) behandelt NULL als wäre es 0, was viele Datenbanken so handhaben. Snowflake dagegen sieht NULL erstmal nicht als Wert an. NULL zählt nicht mit. Man kann nun selbst entscheiden, was der wahrhaftigere Mittelwert in seinem Fall ist bzw. als was man NULLs bei Numerischen Datentypen ansehen möchte.
Ein wirklicher Honigtopf kann hier das Gleichsetzen von count(*) und count(alias.*) oder count(column) sein. Die Query (3) hat für count(t.*) genau eine Zeile in der es gar keine NULLs gibt. Daher kommt hier einfach die Summe zurück. Die Abfrage (4) gibt für die Anzahl NULLs 0 als Ergebnis zurück, weil NULLs wie gesagt nicht zählen.
Honigtopf 2
Manchmal möchte man in einer Bedingung gerade sagen, dass etwas nicht einem Wert oder einer Menge entspricht. In SQL schreibt man entweder einfach ein NOT vor eine Bedingung oder gebraucht den alltäglichen Ungleich-Operator !=. Dies verleitet allerdings etwas dazu, in Schwarz-Weiß zu denken. Des Weiteren können Bedingungen verschachtelt und mittels AND oder OR miteinander verknüpft werden. Dabei können ohnehin bereits verwirrende Konstrukte entstehen. Dies trifft erst Recht zu an den Stellen, wo Snowflake sich nicht binär verhält, NULL eine eigene Kategorie darstellt und eine dritte Ergebnis-Menge bildet.
Zum Verdeutlichen eine zweite Tabelle: myTable2
myTable2 | |||
COL1 | COL2 | COL3 | COL4 |
1 | col2 | 3 | t2 col4 1 |
2 | col2 | null | t2 col4 2 |
3 | null | null | t2 col4 3 |
(1) Select t1.col3 as t1, t2.col3 as t2 from myTable1 as t1, myTable2 as t2
where t1.col3 = t2.col3;
>> T1 T2
3 3
(2) Select t1.col3 as t1, t2.col3 as t2 from myTable1 as t1, myTable2 as t2
where NOT t1.col3 = t2.col3;
>> T1 T2
2 3
(3) Select t1.col3 as t1, t2.col3 as t2 from myTable1 as t1, myTable2 as t2
where (t1.col3 = t2.col3) is NULL
>> T1 T2
null 3
null null
null null
3 null
3 null
2 null
2 null
(4) Select t1.col3 as t1, t2.col3 as t2, t1=t2 from myTable1 as t1, myTable2 as t2 where (t1.col3 = t2.col3) is NOT NULL;
>> T1 T2 T1=T2
3 3 TRUE
2 3 FALSE
Honigtopf 3
Ich bin über einen Community-Beitrag gestolpert, bei dem ich mit der „Selected as Best“-Antwort nicht übereinstimme: „merge-statement-join-expression-contains-null-value“ [Link].
Zum einem stimmt darin die Annahme „NULL = NULL always return false“ nicht. Zum anderen ist es eben nicht die einzige Option, NULLs mit Hilfe von nvl oder coalesce mit Default-Werten zu ersetzen, damit eine Bedingung zutrifft. Das kann u.U. sogar riskant sein. Wenn NULL zum Beispiel mit 0 ersetzt wird, was mache ich dann, wenn es tatsächlich einen Wert 0 gibt?
Das Beispiel aus dem Beitrag eignet sich aber gut. Es geht dabei um ein Merge Statement das Duplikate produziert, weil es Inserts statt Updates macht. Unsere Tabellen myTable1 und myTable2 sind bereits an das Beispiel angelehnt. Unter dem Link sind auch create table statements zu finden, um unser Beispiel einfach nachzuspielen.
Merge
Merge statements sind wie folgt aufgebaut:
MERGE INTO myTable1 t1
USING myTable2 t2
on t1.col1 = t2.col1 // Join-Bedingung
and t1.col2 = t2.col2
and t1.col3 = t2.col3
WHEN MATCHED
THEN UPDATE
SET t1.col4 = t2.col4
WHEN NOT MATCHED
THEN INSERT
VALUES (t2.col1, t2.col2, t2.col3, t2.col4);
Der Knackpunkt ist vor allem die Join-Bedingung. Wenn die Bedingung zutrifft, dann UPDATE, wenn nicht dann INSERT. Hier müssten wir bereits hellhörig werden. Das klingt nach Schwarz-Weiß-Denken. Sobald eine Spalte einen NULL-Wert beinhaltet, wir mit = vergleichen und mit AND verknüpfen, bekommen wir NULL für die gesamte Join-Bedingung. NOT MATCHED. Folglich wird ein INSERT ausgeführt. Das ist der Grund, warum Duplikate statt Updates produziert werden:
mergeResult mit Duplikaten | |||
COL1 | COL2 | COL3 | COL4 |
1 | col2 | 3 | t2 col4 1 |
2 | col2 | null | t2 col4 2 |
2 | col2 | null | t1 col4 2 |
3 | null | null | t2 col4 3 |
3 | null | 2 | t1 col4 3 |
Joins
Bei Joins gibt es noch eine Kurzschreibweise für Fälle in deinen t1 und t2 die gleichen Spaltennamen und Definitionen haben: t1 join t2 using(<column_list>). Hier ist allerdings nicht gleich ersichtliche welche Vergleichs-Operatoren Snowflake dabei intern anwendet. Die Dokumentation sagt es uns:
Wenn hier eine Liste von Spalten übergeben wird, dann werden diese mit AND verknüpft und wir können einen ähnlichen Fall wie im obigen Merge-Statement haben: NOT MATCHED, weil eine Bedingung NULL ergab. In dem Fall können uns Daten verloren gehen.
Wo NULL-Werte in Bedingungen vorkommen können bedeutet das, dass wir bei Joins, und insbesondere bei Merge Statements, bei der eine binäre Bedingung gefordert ist, die binäre EQUAL_NULL()-Funktion verwenden sollten.
MERGE INTO myTable1 t1
USING myTable2 t2
ON equal_null(t1.col1, t2.col1)
AND equal_null(t1.col2, t2.col2)
AND equal_null(t1.col3, t2.col3)
WHEN MATCHED
THEN UPDATE
SET t1.col4 = t2.col4
WHEN NOT MATCHED
THEN INSERT
VALUES (t2.col1, t2.col2, t2.col3, t2.col4);
mergeResult mit equal_null() | |||
COL1 | COL2 | COL3 | COL4 |
1 | col2 | 3 | t2 col4 1 |
2 | col2 | null | t2 col4 2 |
3 | null | null | t2 col4 3 |
3 | null | 2 | t1 col4 3 |
Fazit
Zu jeder Datenverarbeitung gehört eine Daten-Vorverarbeitung. Die Analyse, ob und an welchen Stellen NULL-Werte existieren und wie ich damit umgehen möchte, ist davon ein Teil. Snowflake tut gewisse Dinge anders. So hat es ein eigenes NULL-Verständnis und auch eigene NULL-Handling-Funktionen. Manchmal ist zudem nicht gleich ersichtlich, was Snowflake intern bereits für eine Logik anwendet. In den meisten Fällen allerdings schreibt der Entwickler oder Analyst die Abfragen. Diesen sollte daher folgendes bewusst sein. Bei Snowflake
- zählen NULLs nicht mit
- sind Vergleiche gegen NULL immer NULL
- gibt es eine EQUAL_NULL() Funktion, die aus 3 wieder 2 macht
Erfahren Sie hier mehr über Lösungen im Bereich Snowflake oder besuchen Sie eines unserer kostenlosen Webinare.