Database
 sql >> Datenbank >  >> RDS >> Database

NULL-Komplexitäten – Teil 2

Dieser Artikel ist der zweite in einer Reihe über NULL-Komplexitäten. Letzten Monat habe ich NULL als SQL-Marker für jede Art von fehlendem Wert eingeführt. Ich habe erklärt, dass SQL Ihnen nicht die Möglichkeit bietet, zwischen fehlend und zutreffend zu unterscheiden (A-Werte) und fehlend und nicht anwendbar (I-Werte) Marker. Ich habe auch erklärt, wie Vergleiche mit NULL-Werten mit Konstanten, Variablen, Parametern und Spalten funktionieren. Diesen Monat setze ich die Diskussion fort, indem ich Inkonsistenzen in der NULL-Behandlung in verschiedenen T-SQL-Elementen behandle.

Ich werde in einigen meiner Beispiele weiterhin die Beispieldatenbank TSQLV5 wie im letzten Monat verwenden. Das Skript, das diese Datenbank erstellt und füllt, finden Sie hier und ihr ER-Diagramm hier.

Inkonsistenzen bei NULL-Behandlung

Wie Sie bereits festgestellt haben, ist die NULL-Behandlung nicht trivial. Ein Teil der Verwirrung und Komplexität hat mit der Tatsache zu tun, dass die Behandlung von NULL-Werten zwischen verschiedenen Elementen von T-SQL für ähnliche Operationen inkonsistent sein kann. In den folgenden Abschnitten beschreibe ich die NULL-Behandlung in linearen versus aggregierten Berechnungen, ON/WHERE/HAVING-Klauseln, CHECK-Einschränkung versus CHECK-Option, IF/WHILE/CASE-Elemente, die MERGE-Anweisung, Unterscheidbarkeit und Gruppierung sowie Ordnung und Eindeutigkeit /P>

Lineare versus aggregierte Berechnungen

T-SQL, und dasselbe gilt für Standard-SQL, verwendet eine andere NULL-Handhabungslogik, wenn eine tatsächliche Aggregatfunktion wie SUM, MIN und MAX auf Zeilen angewendet wird, als wenn dieselbe Berechnung als lineare auf Spalten angewendet wird. Um diesen Unterschied zu demonstrieren, verwende ich zwei Beispieltabellen namens #T1 und #T2, die Sie erstellen und füllen, indem Sie den folgenden Code ausführen:

DROP TABLE IF EXISTS #T1, #T2;
 
SELECT * INTO #T1 FROM ( VALUES(10, 5, NULL) ) AS D(col1, col2, col3);
 
SELECT * INTO #T2 FROM ( VALUES(10),(5),(NULL) ) AS D(col1);

Die Tabelle #T1 hat drei Spalten namens col1, col2 und col3. Es hat derzeit eine Zeile mit den Spaltenwerten 10, 5 bzw. NULL:

SELECT * FROM #T1;
col1        col2        col3
----------- ----------- -----------
10          5           NULL

Die Tabelle #T2 hat eine Spalte namens col1. Es hat derzeit drei Zeilen mit den Werten 10, 5 und NULL in col1:

SELECT * FROM #T2;
col1
-----------
10
5
NULL

Bei der Anwendung einer letztendlich aggregierten Berechnung, z. B. einer linearen Addition über Spalten hinweg, führt das Vorhandensein einer NULL-Eingabe zu einem NULL-Ergebnis. Die folgende Abfrage demonstriert dieses Verhalten:

SELECT col1 + col2 + col3 AS total
FROM #T1;

Diese Abfrage generiert die folgende Ausgabe:

total
-----------
NULL

Umgekehrt sind tatsächliche Aggregatfunktionen, die auf Zeilen angewendet werden, so konzipiert, dass sie NULL-Eingaben ignorieren. Die folgende Abfrage demonstriert dieses Verhalten mit der SUM-Funktion:

SELECT SUM(col1) AS total
FROM #T2;

Diese Abfrage generiert die folgende Ausgabe:

total
-----------
15

Warning: Null value is eliminated by an aggregate or other SET operation.

Beachten Sie die vom SQL-Standard vorgeschriebene Warnung, die auf das Vorhandensein von NULL-Eingaben hinweist, die ignoriert wurden. Sie können solche Warnungen unterdrücken, indem Sie die Sitzungsoption ANSI_WARNINGS deaktivieren.

In ähnlicher Weise zählt die COUNT-Funktion, wenn sie auf einen Eingabeausdruck angewendet wird, die Anzahl der Zeilen mit Nicht-NULL-Eingabewerten (im Gegensatz zu COUNT(*), das einfach die Anzahl der Zeilen zählt). Wenn Sie in der obigen Abfrage beispielsweise SUMME(Spalte1) durch ZÄHLUNG(Spalte1) ersetzen, wird die Anzahl 2 zurückgegeben.

Merkwürdigerweise konvertiert der Optimierer den Ausdruck COUNT() in COUNT(*), wenn Sie ein COUNT-Aggregat auf eine Spalte anwenden, die so definiert ist, dass sie keine NULL-Werte zulässt. Dies ermöglicht die Verwendung eines beliebigen Indexes zum Zweck des Zählens, anstatt die Verwendung eines Indexes zu erfordern, der die fragliche Spalte enthält. Das ist ein weiterer Grund neben der Sicherstellung der Konsistenz und Integrität Ihrer Daten, der Sie ermutigen sollte, Einschränkungen wie NOT NULL und andere durchzusetzen. Solche Einschränkungen geben dem Optimierer mehr Flexibilität bei der Berücksichtigung optimalerer Alternativen und vermeiden unnötige Arbeit.

Basierend auf dieser Logik dividiert die AVG-Funktion die Summe der Nicht-NULL-Werte durch die Anzahl der Nicht-NULL-Werte. Betrachten Sie die folgende Abfrage als Beispiel:

SELECT AVG(1.0 * col1) AS avgall
FROM #T2;

Hier wird die Summe der Nicht-NULL-Werte von col1 15 durch die Anzahl der Nicht-NULL-Werte 2 geteilt. Sie multiplizieren col1 mit dem numerischen Literal 1,0, um eine implizite Konvertierung der ganzzahligen Eingabewerte in numerische zu erzwingen, um eine numerische Division und keine ganze Zahl zu erhalten Aufteilung. Diese Abfrage generiert die folgende Ausgabe:

avgall
---------
7.500000

Ebenso ignorieren die Aggregate MIN und MAX NULL-Eingaben. Betrachten Sie die folgende Abfrage:

SELECT MIN(col1) AS mincol1, MAX(col1) AS maxcol1
FROM #T2;

Diese Abfrage generiert die folgende Ausgabe:

mincol1     maxcol1
----------- -----------
5           10

Der Versuch, lineare Berechnungen anzuwenden, aber die Semantik von Aggregatfunktionen zu emulieren (NULL-Werte zu ignorieren), ist nicht schön. Das Emulieren von SUM, COUNT und AVG ist nicht allzu komplex, aber es erfordert, dass Sie jede Eingabe auf NULLen überprüfen, etwa so:

SELECT col1, col2, col3,
  CASE
    WHEN COALESCE(col1, col2, col3) IS NULL THEN NULL
    ELSE COALESCE(col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0)
  END AS sumall,
  CASE WHEN col1 IS NOT NULL THEN 1 ELSE 0 END
    + CASE WHEN col2 IS NOT NULL THEN 1 ELSE 0 END
    + CASE WHEN col3 IS NOT NULL THEN 1 ELSE 0 END AS cntall,
  CASE
    WHEN COALESCE(col1, col2, col3) IS NULL THEN NULL
    ELSE 1.0 * (COALESCE(col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0))
           / (CASE WHEN col1 IS NOT NULL THEN 1 ELSE 0 END
                + CASE WHEN col2 IS NOT NULL THEN 1 ELSE 0 END
                + CASE WHEN col3 IS NOT NULL THEN 1 ELSE 0 END)
  END AS avgall
FROM #T1;

Diese Abfrage generiert die folgende Ausgabe:

col1        col2        col3        sumall      cntall      avgall
----------- ----------- ----------- ----------- ----------- ---------------
10          5           NULL        15          2           7.500000000000

Der Versuch, ein Minimum oder Maximum als lineare Berechnung auf mehr als zwei Eingabespalten anzuwenden, ist ziemlich schwierig, selbst bevor Sie die Logik zum Ignorieren von NULLen hinzufügen, da es das direkte oder indirekte Verschachteln mehrerer CASE-Ausdrücke beinhaltet (wenn Sie Spaltenaliase wiederverwenden). Hier ist zum Beispiel eine Abfrage, die das Maximum aus col1, col2 und col3 in #T1 berechnet, ohne den Teil, der NULLen ignoriert:

SELECT col1, col2, col3, 
  CASE WHEN col1 IS NULL OR col2 IS NULL OR col3 IS NULL THEN NULL ELSE max2 END AS maxall
FROM #T1
  CROSS APPLY (VALUES(CASE WHEN col1 >= col2 THEN col1 ELSE col2 END)) AS A1(max1)
  CROSS APPLY (VALUES(CASE WHEN max1 >= col3 THEN max1 ELSE col3 END)) AS A2(max2);

Diese Abfrage generiert die folgende Ausgabe:

col1        col2        col3        maxall
----------- ----------- ----------- -----------
10          5           NULL        NULL

Wenn Sie den Abfrageplan untersuchen, finden Sie den folgenden erweiterten Ausdruck, der das Endergebnis berechnet:

[Expr1005] = Scalar Operator(CASE WHEN CASE WHEN [#T1].[col1] IS NOT NULL THEN [#T1].[col1] ELSE 
  CASE WHEN [#T1].[col2] IS NOT NULL THEN [#T1].[col2] 
    ELSE [#T1].[col3] END END IS NULL THEN NULL ELSE 
  CASE WHEN CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1].[col1] 
    ELSE [#T1].[col2] END>=[#T1].[col3] THEN 
  CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1].[col1] 
    ELSE [#T1].[col2] END ELSE [#T1].[col3] END END)

Und das ist, wenn es nur drei Spalten gibt. Stellen Sie sich vor, Sie hätten ein Dutzend Spalten involviert!

Fügen Sie nun die Logik zum Ignorieren von NULLen hinzu:

SELECT col1, col2, col3, max2 AS maxall
FROM #T1
  CROSS APPLY (VALUES(CASE WHEN col1 >= col2 OR col2 IS NULL THEN col1 ELSE col2 END)) AS A1(max1)
  CROSS APPLY (VALUES(CASE WHEN max1 >= col3 OR col3 IS NULL THEN max1 ELSE col3 END)) AS A2(max2);

Diese Abfrage generiert die folgende Ausgabe:

col1        col2        col3        maxall
----------- ----------- ----------- -----------
10          5           NULL        10

Oracle verfügt über zwei Funktionen mit den Namen GREATEST und LEAST, die Minimal- bzw. Maximalberechnungen als lineare Berechnungen auf die Eingabewerte anwenden. Diese Funktionen geben bei jeder NULL-Eingabe einen NULL-Wert zurück, wie dies bei den meisten linearen Berechnungen der Fall ist. Es gab ein offenes Feedback-Element mit der Bitte, ähnliche Funktionen in T-SQL zu erhalten, aber diese Anfrage wurde nicht in die letzte Änderung der Feedback-Site übernommen. Wenn Microsoft solche Funktionen zu T-SQL hinzufügt, wäre es großartig, eine Option zu haben, die steuert, ob NULL-Werte ignoriert werden oder nicht.

In der Zwischenzeit gibt es im Vergleich zu den oben genannten eine viel elegantere Technik, die jede Art von Aggregat als lineares spaltenübergreifend berechnet, wobei die Semantik der tatsächlichen Aggregatfunktion verwendet wird, wobei NULL-Werte ignoriert werden. Sie verwenden eine Kombination aus dem CROSS APPLY-Operator und einer abgeleiteten Tabellenabfrage für einen Tabellenwertkonstruktor, der Spalten in Zeilen rotiert und das Aggregat als tatsächliche Aggregatfunktion anwendet. Hier ist ein Beispiel, das die MIN- und MAX-Berechnungen demonstriert, aber Sie können diese Technik mit jeder beliebigen Aggregatfunktion verwenden:

SELECT col1, col2, col3, maxall, minall
FROM #T1 CROSS APPLY
  (SELECT MAX(mycol), MIN(mycol)
   FROM (VALUES(col1),(col2),(col3)) AS D1(mycol)) AS D2(maxall, minall);

Diese Abfrage generiert die folgende Ausgabe:

col1        col2        col3        maxall      minall
----------- ----------- ----------- ----------- -----------
10          5           NULL        10          5

Was, wenn Sie das Gegenteil wollen? Was ist, wenn Sie ein Aggregat über Zeilen hinweg berechnen müssen, aber NULL erzeugen, wenn es eine NULL-Eingabe gibt? Angenommen, Sie müssen alle col1-Werte von #T1 summieren, aber NULL zurückgeben, wenn eine der Eingaben NULL ist. Dies kann mit der folgenden Technik erreicht werden:

SELECT SUM(col1) * NULLIF(MIN(CASE WHEN col1 IS NULL THEN 0 ELSE 1 END), 0) AS sumall
FROM #T2;

Sie wenden ein MIN-Aggregat auf einen CASE-Ausdruck an, der Nullen für NULL-Eingaben und Einsen für Nicht-NULL-Eingaben zurückgibt. Wenn es eine NULL-Eingabe gibt, ist das Ergebnis der MIN-Funktion 0, andernfalls ist es 1. Dann wandeln Sie mit der NULLIF-Funktion ein 0-Ergebnis in eine NULL um. Anschließend multiplizieren Sie das Ergebnis der NULLIF-Funktion mit der ursprünglichen Summe. Wenn es eine NULL-Eingabe gibt, multiplizieren Sie die ursprüngliche Summe mit einer NULL, was eine NULL ergibt. Wenn keine NULL-Eingabe vorhanden ist, multiplizieren Sie das Ergebnis der ursprünglichen Summe mit 1, was die ursprüngliche Summe ergibt.

Zurück zu linearen Berechnungen, die NULL für jede NULL-Eingabe ergeben, gilt die gleiche Logik für die Zeichenfolgenverkettung mit dem +-Operator, wie die folgende Abfrage zeigt:

USE TSQLV5;
 
SELECT empid, country, region, city,
  country + N',' + region + N',' + city AS emplocation
FROM HR.Employees;

Diese Abfrage generiert die folgende Ausgabe:

empid       country         region          city            emplocation
----------- --------------- --------------- --------------- ----------------
1           USA             WA              Seattle         USA,WA,Seattle
2           USA             WA              Tacoma          USA,WA,Tacoma
3           USA             WA              Kirkland        USA,WA,Kirkland
4           USA             WA              Redmond         USA,WA,Redmond
5           UK              NULL            London          NULL
6           UK              NULL            London          NULL
7           UK              NULL            London          NULL
8           USA             WA              Seattle         USA,WA,Seattle
9           UK              NULL            London          NULL

Sie möchten die Standortteile von Mitarbeitern zu einer Zeichenfolge verketten und dabei ein Komma als Trennzeichen verwenden. Aber Sie wollen NULL-Eingaben ignorieren. Wenn eine der Eingaben NULL ist, erhalten Sie stattdessen NULL als Ergebnis. Einige deaktivieren die Sitzungsoption CONCAT_NULL_YIELDS_NULL, die bewirkt, dass eine NULL-Eingabe zu Verkettungszwecken in eine leere Zeichenfolge konvertiert wird, aber diese Option wird nicht empfohlen, da sie ein nicht standardmäßiges Verhalten anwendet. Darüber hinaus bleiben bei NULL-Eingaben mehrere aufeinanderfolgende Trennzeichen übrig, was normalerweise nicht das gewünschte Verhalten ist. Eine andere Möglichkeit besteht darin, NULL-Eingaben mithilfe der Funktionen ISNULL oder COALESCE explizit durch eine leere Zeichenfolge zu ersetzen, aber dies führt normalerweise zu langem, ausführlichem Code. Eine viel elegantere Möglichkeit ist die Verwendung der Funktion CONCAT_WS, die in SQL Server 2017 eingeführt wurde. Diese Funktion verkettet die Eingaben, ignoriert NULL-Werte und verwendet das als erste Eingabe bereitgestellte Trennzeichen. Hier ist die Lösungsabfrage mit dieser Funktion:

SELECT empid, country, region, city,
  CONCAT_WS(N',', country, region, city) AS emplocation
FROM HR.Employees;

Diese Abfrage generiert die folgende Ausgabe:

empid       country         region          city            emplocation
----------- --------------- --------------- --------------- ----------------
1           USA             WA              Seattle         USA,WA,Seattle
2           USA             WA              Tacoma          USA,WA,Tacoma
3           USA             WA              Kirkland        USA,WA,Kirkland
4           USA             WA              Redmond         USA,WA,Redmond
5           UK              NULL            London          UK,London
6           UK              NULL            London          UK,London
7           UK              NULL            London          UK,London
8           USA             WA              Seattle         USA,WA,Seattle
9           UK              NULL            London          UK,London

AUF/WO/HABEN

Wenn Sie die WHERE-, HAVING- und ON-Abfrageklauseln zum Filtern/Abgleichen verwenden, ist es wichtig, daran zu denken, dass sie eine dreiwertige Prädikatenlogik verwenden. Wenn es um dreiwertige Logik geht, möchten Sie genau identifizieren, wie die Klausel TRUE-, FALSE- und UNKNOWN-Fälle behandelt. Diese drei Klauseln sind so konzipiert, dass sie TRUE-Fälle akzeptieren und FALSE- und UNBEKANNTE Fälle zurückweisen.

Um dieses Verhalten zu demonstrieren, verwende ich eine Tabelle namens Kontakte, die Sie erstellen und füllen, indem Sie den folgenden Code ausführen:.

DROP TABLE IF EXISTS dbo.Contacts;
GO
 
CREATE TABLE dbo.Contacts
(
  id INT NOT NULL 
    CONSTRAINT PK_Contacts PRIMARY KEY,
  name VARCHAR(10) NOT NULL,
  hourlyrate NUMERIC(12, 2) NULL
    CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)
);
 
INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES
  (1, 'A', 100.00),(2, 'B', 200.00),(3, 'C', NULL);

Beachten Sie, dass die Kontakte 1 und 2 anwendbare Stundensätze haben und Kontakt 3 nicht, daher wird sein Stundensatz auf NULL gesetzt. Betrachten Sie die folgende Abfrage, die nach Kontakten mit einem positiven Stundensatz sucht:

SELECT id, name, hourlyrate
FROM dbo.Contacts
WHERE hourlyrate > 0.00;

Dieses Prädikat ergibt TRUE für die Kontakte 1 und 2 und UNKNOWN für Kontakt 3, daher enthält die Ausgabe nur die Kontakte 1 und 2:

id          name       hourlyrate
----------- ---------- -----------
1           A          100.00
2           B          200.00

Der Gedanke hier ist, dass Sie die Zeile zurückgeben möchten, wenn Sie sicher sind, dass das Prädikat wahr ist, andernfalls möchten Sie sie verwerfen. Dies mag zunächst trivial erscheinen, bis Sie feststellen, dass einige Sprachelemente, die auch Prädikate verwenden, anders funktionieren.

CHECK-Einschränkung versus CHECK-Option

Eine CHECK-Einschränkung ist ein Tool, das Sie verwenden, um die Integrität in einer Tabelle basierend auf einem Prädikat zu erzwingen. Das Prädikat wird ausgewertet, wenn Sie versuchen, Zeilen in die Tabelle einzufügen oder zu aktualisieren. Im Gegensatz zu Klauseln zum Filtern und Abgleichen von Abfragen, die TRUE-Fälle akzeptieren und FALSE- und UNKNOWN-Fälle zurückweisen, ist eine CHECK-Einschränkung so konzipiert, dass sie TRUE- und UNKNOWN-Fälle akzeptiert und FALSE-Fälle zurückweist. Der Gedanke hier ist, dass Sie, wenn Sie sicher sind, dass das Prädikat falsch ist, die versuchte Änderung ablehnen möchten, andernfalls möchten Sie sie zulassen.

Wenn Sie sich die Definition unserer Kontakttabelle ansehen, werden Sie feststellen, dass sie die folgende CHECK-Einschränkung hat, die Kontakte mit nicht positiven Stundensätzen zurückweist:

CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)

Beachten Sie, dass die Einschränkung dasselbe Prädikat verwendet wie das, das Sie im vorherigen Abfragefilter verwendet haben.

Versuchen Sie, einen Kontakt mit einem positiven Stundensatz hinzuzufügen:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (4, 'D', 150.00);

Dieser Versuch ist erfolgreich.

Versuchen Sie, einen Kontakt mit einem Stundensatz von NULL hinzuzufügen:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (5, 'E', NULL);

Dieser Versuch ist ebenfalls erfolgreich, da eine CHECK-Einschränkung darauf ausgelegt ist, TRUE- und UNKNOWN-Fälle zu akzeptieren. Das ist der Fall, wenn ein Abfragefilter und eine CHECK-Einschränkung so konzipiert sind, dass sie unterschiedlich funktionieren.

Versuchen Sie, einen Kontakt mit einem nicht positiven Stundensatz hinzuzufügen:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (6, 'F', -100.00);

Dieser Versuch schlägt mit folgendem Fehler fehl:

Nachricht 547, Ebene 16, Status 0, Zeile 454
Die INSERT-Anweisung stand in Konflikt mit der CHECK-Einschränkung „CHK_Contacts_hourlyrate“. Der Konflikt ist in Datenbank „TSQLV5“, Tabelle „dbo.Contacts“, Spalte „hourlyrate“ aufgetreten.

T-SQL ermöglicht es Ihnen auch, die Integrität von Änderungen durch Ansichten mit einer CHECK-Option zu erzwingen. Einige meinen, dass diese Option einen ähnlichen Zweck erfüllt wie eine CHECK-Einschränkung, solange Sie die Änderung über die Ansicht anwenden. Betrachten Sie beispielsweise die folgende Ansicht, die einen Filter basierend auf dem Prädikat Stundensatz> 0,00 verwendet und mit der Option CHECK definiert ist:

CREATE OR ALTER VIEW dbo.MyContacts
AS
SELECT id, name, hourlyrate
FROM dbo.Contacts
WHERE hourlyrate > 0.00
WITH CHECK OPTION;

Wie sich herausstellt, ist die Ansichtsoption CHECK im Gegensatz zu einer CHECK-Einschränkung so konzipiert, dass sie TRUE-Fälle akzeptiert und sowohl FALSE- als auch UNKNOWN-Fälle zurückweist. Es ist also eigentlich so konzipiert, dass es sich eher wie der Abfragefilter verhält, auch um die Integrität zu erzwingen.

Versuchen Sie, eine Zeile mit einem positiven Stundensatz über die Ansicht einzufügen:

INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (7, 'G', 300.00);

Dieser Versuch ist erfolgreich.

Versuchen Sie, eine Zeile mit einem NULL-Stundensatz in die Ansicht einzufügen:

INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (8, 'H', NULL);

Dieser Versuch schlägt mit folgendem Fehler fehl:

Msg 550, Level 16, State 1, Line 473
Die versuchte Einfügung oder Aktualisierung ist fehlgeschlagen, weil die Zielansicht entweder WITH CHECK OPTION angibt oder eine Ansicht umfasst, die WITH CHECK OPTION angibt, und eine oder mehrere Zeilen, die aus der Operation resultieren, nicht sich unter der Einschränkung CHECK OPTION qualifizieren.

Der Gedanke dabei ist, dass Sie nach dem Hinzufügen der CHECK-Option zur Ansicht nur Änderungen zulassen möchten, die zu Zeilen führen, die von der Ansicht zurückgegeben werden. Das ist etwas anders als das Denken mit einer CHECK-Einschränkung – lehnen Sie Änderungen ab, bei denen Sie sicher sind, dass das Prädikat falsch ist. Dies kann etwas verwirrend sein. Wenn Sie möchten, dass die Ansicht Änderungen zulässt, die den Stundensatz auf NULL setzen, muss der Abfragefilter diese ebenfalls zulassen, indem Sie OR hourlyrate IS NULL hinzufügen. Sie müssen sich nur darüber im Klaren sein, dass eine CHECK-Einschränkung und eine CHECK-Option in Bezug auf den UNKNOWN-Fall anders funktionieren. Ersteres akzeptiert es, während letzteres es ablehnt.

Fragen Sie die Kontakttabelle nach allen oben genannten Änderungen ab:

SELECT id, name, hourlyrate
FROM dbo.Contacts;

Sie sollten an dieser Stelle die folgende Ausgabe erhalten:

id          name       hourlyrate
----------- ---------- -----------
1           A          100.00
2           B          200.00
3           C          NULL
4           D          150.00
5           E          NULL
7           G          300.00

IF/WHILE/CASE

Die Sprachelemente IF, WHILE und CASE arbeiten mit Prädikaten.

Die IF-Anweisung ist wie folgt aufgebaut:

IF <predicate>
  <statement or BEGIN-END block when TRUE>
ELSE
  <statement or BEGIN-END block when FALSE or UNKNOWN>

Es ist intuitiv, einen TRUE-Block nach der IF-Klausel und einen FALSE-Block nach der ELSE-Klausel zu erwarten, aber Sie müssen sich darüber im Klaren sein, dass die ELSE-Klausel tatsächlich aktiviert wird, wenn das Prädikat FALSE oder UNKNOWN ist. Theoretisch hätte eine dreiwertige Logiksprache eine IF-Anweisung mit einer Trennung der drei Fälle haben können. Etwa so:

IF <predicate>
  WHEN TRUE
    <statement or BEGIN-END block when TRUE>
  WHEN FALSE
    <statement or BEGIN-END block when FALSE>
  WHEN UNKNOWN
    <statement or BEGIN-END block when UNKNOWN>

Und erlauben Sie sogar Kombinationen von logischen Ergebnissen, sodass Sie, wenn Sie FALSE und UNKNOWN in einem Abschnitt kombinieren möchten, so etwas verwenden könnten:

IF <predicate>
  WHEN TRUE
    <statement or BEGIN-END block when TRUE>
  WHEN FALSE OR UNKNOWN
    <statement or BEGIN-END block when FALSE OR UNKNOWN>

In der Zwischenzeit können Sie solche Konstrukte emulieren, indem Sie IF-ELSE-Anweisungen verschachteln und mit dem Operator IS NULL explizit nach NULL-Werten in den Operanden suchen.

Die WHILE-Anweisung hat nur einen TRUE-Block. Es ist wie folgt aufgebaut:

WHILE <predicate>
  <statement or BEGIN-END block when TRUE>

Die Anweisung oder der BEGIN-END-Block, der den Rumpf der Schleife bildet, wird aktiviert, während das Prädikat TURE ist. Sobald das Prädikat FALSE oder UNKNOWN ist, geht die Kontrolle auf die Anweisung nach der WHILE-Schleife über.

Im Gegensatz zu IF und WHILE, bei denen es sich um Anweisungen handelt, die Code ausführen, ist CASE ein Ausdruck, der einen Wert zurückgibt. Die Syntax einer Suche Der CASE-Ausdruck lautet wie folgt:

CASE
  WHEN <predicate 1> THEN <expression 1 when TRUE>
  WHEN <predicate 2> THEN <expression 2 when TRUE >
  ...
  WHEN <predicate n> THEN <expression n when TRUE >
  ELSE <else expression when all are FALSE or UNKNOWN>
END

Ein CASE-Ausdruck soll den Ausdruck zurückgeben, der auf die THEN-Klausel folgt, die dem ersten WHEN-Prädikat entspricht, das als TRUE ausgewertet wird. Wenn es eine ELSE-Klausel gibt, wird sie aktiviert, wenn kein WHEN-Prädikat TRUE ist (alle sind FALSE oder UNKNOWN). Wenn keine explizite ELSE-Klausel vorhanden ist, wird ein implizites ELSE NULL verwendet. Wenn Sie einen UNBEKANNTEN Fall separat behandeln möchten, können Sie mit dem IS NULL-Operator explizit nach NULL-Werten in den Operanden des Prädikats suchen.

Eine einfache Der CASE-Ausdruck verwendet implizite, auf Gleichheit basierende Vergleiche zwischen dem Quellausdruck und den verglichenen Ausdrücken:

CASE <source expression>
  WHEN <comp expression 1> THEN <result expression 1 when TRUE>
  WHEN <comp expression 2> THEN <result expression 2 when TRUE >
  ...
  WHEN <comp expression n> THEN <result expression n when TRUE >
  ELSE <else result expression when all are FALSE or UNKNOWN>
END

Der einfache CASE-Ausdruck ist in Bezug auf die Behandlung der dreiwertigen Logik ähnlich wie der gesuchte CASE-Ausdruck aufgebaut, da die Vergleiche jedoch einen impliziten gleichheitsbasierten Vergleich verwenden, können Sie den UNKNOWN-Fall nicht separat behandeln. Ein Versuch, NULL in einem der verglichenen Ausdrücke in den WHEN-Klauseln zu verwenden, ist bedeutungslos, da der Vergleich nicht TRUE ergibt, selbst wenn der Quellausdruck NULL ist. Betrachten Sie das folgende Beispiel:

DECLARE @input AS INT = NULL;
 
SELECT CASE @input WHEN NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Dies wird implizit in Folgendes umgewandelt:

DECLARE @input AS INT = NULL;
 
SELECT CASE WHEN @input = NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Folglich ist das Ergebnis:

Eingabe ist nicht NULL

Um eine NULL-Eingabe zu erkennen, müssen Sie die gesuchte CASE-Ausdruckssyntax und den IS NULL-Operator wie folgt verwenden:

DECLARE @input AS INT = NULL;
 
SELECT CASE WHEN @input IS NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Diesmal ist das Ergebnis:

Eingabe ist NULL

VEREINIGUNG

Die MERGE-Anweisung wird verwendet, um Daten von einer Quelle mit einem Ziel zusammenzuführen. Sie verwenden ein Zusammenführungsprädikat, um die folgenden Fälle zu identifizieren und eine Aktion auf das Ziel anzuwenden:

  • Eine Quellzeile wird mit einer Zielzeile abgeglichen (wird aktiviert, wenn eine Übereinstimmung für die Quellzeile gefunden wird, bei der das Zusammenführungsprädikat WAHR ist):Wende UPDATE oder DELETE auf das Ziel an
  • Eine Quellzeile wird nicht mit einer Zielzeile abgeglichen (wird aktiviert, wenn keine Übereinstimmungen für die Quellzeile gefunden werden, bei der das Zusammenführungsprädikat TRUE ist, sondern für alle Prädikate FALSE oder UNKNOWN):Wenden Sie ein INSERT auf das Ziel an
  • Eine Zielzeile wird nicht mit einer Quellzeile abgeglichen (wird aktiviert, wenn keine Übereinstimmungen für die Zielzeile gefunden werden, bei der das Zusammenführungsprädikat TRUE ist, sondern für alle Prädikate FALSE oder UNKNOWN):wenden Sie UPDATE oder DELETE auf das Ziel an

Alle drei Szenarien trennen WAHR für eine Gruppe und FALSCH oder UNBEKANNT für eine andere. Sie erhalten keine separaten Abschnitte für die Behandlung von TRUE-, FALSE- und UNKNOWN-Fällen.

Um dies zu demonstrieren, verwende ich eine Tabelle namens T3, die Sie erstellen und füllen, indem Sie den folgenden Code ausführen:

DROP TABLE IF EXISTS dbo.T3;
GO
 
CREATE TABLE dbo.T3(col1 INT NULL, col2 INT NULL, CONSTRAINT UNQ_T3 UNIQUE(col1));
 
INSERT INTO dbo.T3(col1) VALUES(1),(2),(NULL);

Betrachten Sie die folgende MERGE-Anweisung:

MERGE INTO dbo.T3 AS TGT
USING (VALUES(1, 100), (3, 300)) AS SRC(col1, col2)
  ON SRC.col1 = TGT.col1
WHEN MATCHED THEN UPDATE
  SET TGT.col2 = SRC.col2
WHEN NOT MATCHED THEN INSERT(col1, col2) VALUES(SRC.col1, SRC.col2)
WHEN NOT MATCHED BY SOURCE THEN UPDATE
  SET col2 = -1;
 
SELECT col1, col2 FROM dbo.T3;

Die Quellzeile, in der col1 1 ist, wird mit der Zielzeile abgeglichen, in der col1 1 ist (Prädikat ist TRUE), und daher wird col2 der Zielzeile auf 100 gesetzt.

Die Quellzeile, in der col1 3 ist, wird von keiner Zielzeile abgeglichen (für alle Prädikate ist FALSE oder UNKNOWN) und daher wird eine neue Zeile in T3 mit 3 als Wert für col1 und 300 als Wert für col2 eingefügt.

Die Zielzeilen, in denen col1 2 und col1 NULL ist, werden von keiner Quellzeile abgeglichen (für alle Zeilen ist das Prädikat FALSE oder UNKNOWN) und daher wird in beiden Fällen col2 in den Zielzeilen auf -1 gesetzt.

Die Abfrage von T3 gibt die folgende Ausgabe zurück, nachdem die obige MERGE-Anweisung ausgeführt wurde:

col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Halten Sie den Tisch T3 herum; es wird später verwendet.

Unterscheidbarkeit und Gruppierung

Im Gegensatz zu Vergleichen, die mit Gleichheits- und Ungleichheitsoperatoren durchgeführt werden, gruppieren Vergleiche, die für Unterscheidbarkeits- und Gruppierungszwecke durchgeführt werden, NULL-Werte zusammen. Eine NULL wird als von einer anderen NULL nicht verschieden betrachtet, aber eine NULL wird als von einem Nicht-NULL-Wert verschieden betrachtet. Folglich entfernt das Anwenden einer DISTINCT-Klausel doppelte Vorkommen von NULL-Werten. Die folgende Abfrage demonstriert dies:

SELECT DISTINCT country, region FROM HR.Employees;

Diese Abfrage generiert die folgende Ausgabe:

country         region
--------------- ---------------
UK              NULL
USA             WA

Es gibt mehrere Mitarbeiter mit dem Land USA und der Region NULL, und nach dem Entfernen von Duplikaten zeigt das Ergebnis nur ein Vorkommen der Kombination.

Wie die Unterscheidbarkeit gruppiert auch die Gruppierung NULL-Werte zusammen, wie die folgende Abfrage zeigt:

SELECT country, region, COUNT(*) AS numemps
FROM HR.Employees
GROUP BY country, region;

Diese Abfrage generiert die folgende Ausgabe:

country         region          numemps
--------------- --------------- -----------
UK              NULL            4
USA             WA              5

Auch hier wurden alle vier Mitarbeiter mit dem Land UK und der Region NULL zusammengefasst.

Bestellung

Beim Sortieren werden mehrere NULLen so behandelt, als hätten sie denselben Sortierwert. Der SQL-Standard überlässt es der Implementierung, zu entscheiden, ob NULL-Werte im Vergleich zu Nicht-NULL-Werten an erster oder letzter Stelle angeordnet werden sollen. Microsoft hat sich dafür entschieden, NULL-Werte im Vergleich zu Nicht-NULL-Werten in SQL Server mit niedrigeren Sortierwerten zu betrachten, sodass T-SQL bei Verwendung der aufsteigenden Reihenfolge NULL-Werte zuerst anordnet. Die folgende Abfrage demonstriert dies:

SELECT id, name, hourlyrate
FROM dbo.Contacts
ORDER BY hourlyrate;

Diese Abfrage generiert die folgende Ausgabe:

id          name       hourlyrate
----------- ---------- -----------
3           C          NULL
5           E          NULL
1           A          100.00
4           D          150.00
2           B          200.00
7           G          300.00

Nächsten Monat werde ich mehr zu diesem Thema hinzufügen und Standardelemente diskutieren, die Ihnen die Kontrolle über das NULL-Ordnungsverhalten und die Problemumgehungen für diese Elemente in T-SQL geben.

Einzigartigkeit

Beim Erzwingen der Eindeutigkeit für eine NULL-fähige Spalte mit einer UNIQUE-Einschränkung oder einem eindeutigen Index behandelt T-SQL NULL-Werte genauso wie Nicht-NULL-Werte. Es lehnt doppelte NULLen ab, als ob eine NULL nicht eindeutig von einer anderen NULL wäre.

Erinnern Sie sich daran, dass unsere Tabelle T3 eine UNIQUE-Einschränkung hat, die auf col1 definiert ist. Hier ist die Definition:

CONSTRAINT UNQ_T3 UNIQUE(col1)

Fragen Sie T3 ab, um seinen aktuellen Inhalt anzuzeigen:

SELECT * FROM dbo.T3;

Wenn Sie alle Modifikationen aus den früheren Beispielen in diesem Artikel gegen T3 ausgeführt haben, sollten Sie die folgende Ausgabe erhalten:

col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Versuchen Sie, eine zweite Zeile mit NULL in Spalte1 hinzuzufügen:

INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);

Sie erhalten die folgende Fehlermeldung:

Nachricht 2627, Ebene 14, Status 1, Zeile 558
Verletzung der UNIQUE KEY-Einschränkung 'UNQ_T3'. Doppelter Schlüssel kann nicht in Objekt „dbo.T3“ eingefügt werden. Der doppelte Schlüsselwert ist ().

Dieses Verhalten ist tatsächlich nicht standardmäßig. Nächsten Monat beschreibe ich die Standardspezifikation und wie man sie in T-SQL emuliert.

Schlussfolgerung

In diesem zweiten Teil der Serie über NULL-Komplexitäten habe ich mich auf Inkonsistenzen bei der NULL-Behandlung zwischen verschiedenen T-SQL-Elementen konzentriert. Ich behandelte lineare versus aggregierte Berechnungen, Filter- und Matching-Klauseln, die CHECK-Einschränkung versus die CHECK-Option, IF-, WHILE- und CASE-Elemente, die MERGE-Anweisung, Unterscheidbarkeit und Gruppierung, Ordnung und Eindeutigkeit. Die Ungereimtheiten, die ich behandelt habe, betonen weiter, wie wichtig es ist, die Behandlung von NULL-Werten auf der von Ihnen verwendeten Plattform richtig zu verstehen, um sicherzustellen, dass Sie korrekten und robusten Code schreiben. Nächsten Monat werde ich die Reihe fortsetzen, indem ich die NULL-Behandlungsoptionen des SQL-Standards abdecke, die in T-SQL nicht verfügbar sind, und Problemumgehungen biete, die in T-SQL unterstützt werden.