Database
 sql >> Baza danych >  >> RDS >> Database

NULL złożoności – część 2

Ten artykuł jest drugim z serii o złożoności NULL. W zeszłym miesiącu wprowadziłem NULL jako znacznik SQL dla każdego rodzaju brakującej wartości. Wyjaśniłem, że SQL nie umożliwia rozróżnienia między brakującym a odpowiednim (wartości A) oraz brakujące i nieaktualne (wartości I). Wyjaśniłem również, jak porównania z wartościami NULL działają ze stałymi, zmiennymi, parametrami i kolumnami. W tym miesiącu kontynuuję dyskusję, omawiając niespójności traktowania NULL w różnych elementach T-SQL.

Będę nadal używał przykładowej bazy danych TSQLV5, tak jak w zeszłym miesiącu w niektórych moich przykładach. Skrypt, który tworzy i wypełnia tę bazę danych, oraz jego diagram ER można znaleźć tutaj.

NULL niespójności w leczeniu

Jak już się przekonałeś, leczenie NULL nie jest trywialne. Część zamieszania i złożoności ma związek z faktem, że traktowanie wartości NULL może być niespójne między różnymi elementami T-SQL dla podobnych operacji. W kolejnych podrozdziałach opiszę obsługę NULL w obliczeniach liniowych versus agregujących, klauzule ON/WHERE/HAVING, opcję CHECK ograniczenie versus CHECK, elementy IF/WHILE/CASE, instrukcję MERGE, odrębność i grupowanie, a także kolejność i niepowtarzalność.

Obliczenia liniowe i zagregowane

T-SQL, podobnie jak w przypadku standardowego SQL, wykorzystuje inną logikę obsługi wartości NULL podczas stosowania rzeczywistej funkcji agregującej, takiej jak SUM, MIN i MAX w wierszach, w przeciwieństwie do stosowania tego samego obliczenia jako liniowego w odniesieniu do kolumn. Aby zademonstrować tę różnicę, użyję dwóch przykładowych tabel o nazwach #T1 i #T2, które tworzysz i wypełniasz, uruchamiając następujący kod:

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);

Tabela #T1 ma trzy kolumny o nazwach col1, col2 i col3. Obecnie ma jeden wiersz z wartościami kolumn odpowiednio 10, 5 i NULL:

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

Tabela #T2 ma jedną kolumnę o nazwie col1. Obecnie ma trzy wiersze z wartościami 10, 5 i NULL w kol1:

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

Podczas stosowania tego, co jest ostatecznie obliczeniem agregacyjnym, takim jak dodawanie jako liniowe w kolumnach, obecność dowolnego wejścia NULL daje wynik NULL. Poniższe zapytanie demonstruje to zachowanie:

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

To zapytanie generuje następujące dane wyjściowe:

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

I odwrotnie, rzeczywiste funkcje agregujące, które są stosowane w wierszach, są zaprojektowane tak, aby ignorować dane wejściowe o wartości NULL. Poniższe zapytanie demonstruje to zachowanie przy użyciu funkcji SUMA:

SELECT SUM(col1) AS total
FROM #T2;

To zapytanie generuje następujące dane wyjściowe:

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

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

Zwróć uwagę na ostrzeżenie wymagane przez standard SQL, wskazujące na obecność zignorowanych danych wejściowych o wartości NULL. Możesz wyłączyć takie ostrzeżenia, wyłączając opcję sesji ANSI_WARNINGS.

Podobnie po zastosowaniu do wyrażenia wejściowego funkcja ILE.LICZB zlicza liczbę wierszy z wartościami wejściowymi innymi niż NULL (w przeciwieństwie do funkcji ILE.LICZB (*), która po prostu zlicza liczbę wierszy). Na przykład zamiana SUMA(kol1) na COUNT(kol1) w powyższym zapytaniu zwraca liczbę 2.

Co ciekawe, jeśli zastosujesz agregację COUNT do kolumny, która jest zdefiniowana jako nie zezwalająca na wartości NULL, optymalizator konwertuje wyrażenie COUNT() na COUNT(*). Umożliwia to użycie dowolnego indeksu w celu zliczania, w przeciwieństwie do wymagania użycia indeksu zawierającego daną kolumnę. To kolejny powód poza zapewnieniem spójności i integralności danych, który powinien zachęcić Cię do egzekwowania ograniczeń, takich jak NOT NULL i inne. Takie ograniczenia pozwalają optymalizatorowi na większą elastyczność w rozważaniu bardziej optymalnych alternatyw i unikanie niepotrzebnej pracy.

W oparciu o tę logikę funkcja AVG dzieli sumę wartości różnych od NULL przez liczbę wartości innych niż NULL. Rozważ następujące zapytanie jako przykład:

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

Tutaj suma wartości col1 innych niż NULL 15 jest dzielona przez liczbę wartości innych niż NULL 2. Mnożysz col1 przez literał numeryczny 1.0, aby wymusić niejawną konwersję liczb całkowitych na wartości liczbowe, aby uzyskać dzielenie liczbowe, a nie całkowite dział. To zapytanie generuje następujące dane wyjściowe:

avgall
---------
7.500000

Podobnie agregaty MIN i MAX ignorują dane wejściowe NULL. Rozważ następujące zapytanie:

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

To zapytanie generuje następujące dane wyjściowe:

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

Próba zastosowania obliczeń liniowych, ale emulacja semantyki funkcji agregujących (ignorowanie wartości NULL) nie jest ładna. Emulacja SUM, COUNT i AVG nie jest zbyt skomplikowana, ale wymaga sprawdzenia każdego wejścia pod kątem wartości NULL, na przykład:

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;

To zapytanie generuje następujące dane wyjściowe:

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

Próba zastosowania minimum lub maksimum jako obliczenia liniowego do więcej niż dwóch kolumn wejściowych jest dość trudna, nawet przed dodaniem logiki ignorowania wartości NULL, ponieważ wiąże się to z zagnieżdżaniem wielu wyrażeń CASE bezpośrednio lub pośrednio (przy ponownym użyciu aliasów kolumn). Na przykład, oto zapytanie obliczające maksimum między col1, col2 i col3 w #T1, bez części ignorującej wartości NULL:

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);

To zapytanie generuje następujące dane wyjściowe:

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

Jeśli przeanalizujesz plan zapytania, znajdziesz następujące rozszerzone wyrażenie obliczające wynik końcowy:

[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)

I wtedy w grę wchodzą tylko trzy kolumny. Wyobraź sobie, że zaangażowanych jest tuzin kolumn!

Teraz dodaj do tego logikę ignorowania wartości NULL:

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);

To zapytanie generuje następujące dane wyjściowe:

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

Oracle ma parę funkcji nazwanych NAJWIĘKSZĄ i NAJMNIEJSZĄ, które stosują odpowiednio obliczenia minimum i maksimum jako liniowe do wartości wejściowych. Funkcje te zwracają wartość NULL przy dowolnym wejściu NULL, tak jak robi to większość obliczeń liniowych. Pojawił się element otwartej opinii z prośbą o uzyskanie podobnych funkcji w T-SQL, ale to żądanie nie zostało przeniesione w ostatniej zmianie witryny z opiniami. Jeśli Microsoft doda takie funkcje do T-SQL, byłoby wspaniale mieć opcję kontrolującą, czy ignorować NULL, czy nie.

W międzyczasie istnieje znacznie bardziej elegancka technika w porównaniu z wyżej wymienionymi, która oblicza dowolny rodzaj agregacji jako liniową w kolumnach przy użyciu rzeczywistej semantyki funkcji agregacji, ignorując wartości NULL. Używasz kombinacji operatora CROSS APPLY i zapytania tabeli pochodnej względem konstruktora wartości tabeli, który obraca kolumny do wierszy i stosuje agregację jako rzeczywistą funkcję agregującą. Oto przykład demonstrujący obliczenia MIN i MAX, ale możesz użyć tej techniki z dowolną funkcją agregującą, którą lubisz:

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);

To zapytanie generuje następujące dane wyjściowe:

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

A jeśli chcesz czegoś przeciwnego? Co zrobić, jeśli musisz obliczyć agregację w wierszach, ale wygenerujesz NULL, jeśli istnieje jakiekolwiek wejście NULL? Załóżmy na przykład, że musisz zsumować wszystkie wartości col1 z #T1, ale zwrócić NULL, jeśli którekolwiek z danych wejściowych ma wartość NULL. Można to osiągnąć za pomocą następującej techniki:

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

Stosujesz agregację MIN do wyrażenia CASE, które zwraca zera dla danych wejściowych o wartości NULL i jedynek dla danych wejściowych innych niż NULL. Jeśli istnieje jakiekolwiek wejście NULL, wynikiem funkcji MIN jest 0, w przeciwnym razie jest to 1. Następnie za pomocą funkcji NULLIF konwertujesz wynik 0 na NULL. Następnie mnożysz wynik funkcji NULLIF przez pierwotną sumę. Jeśli istnieje jakiekolwiek wejście NULL, mnożysz oryginalną sumę przez NULL, otrzymując NULL. Jeśli nie ma wartości NULL, mnożysz wynik oryginalnej sumy przez 1, otrzymując oryginalną sumę.

Wracając do obliczeń liniowych, które dają NULL dla dowolnego wejścia NULL, ta sama logika dotyczy łączenia ciągów za pomocą operatora +, jak pokazuje poniższe zapytanie:

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

To zapytanie generuje następujące dane wyjściowe:

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

Chcesz połączyć części lokalizacji pracowników w jeden ciąg, używając przecinka jako separatora. Ale chcesz zignorować wejścia NULL. Zamiast tego, gdy którekolwiek z danych wejściowych ma wartość NULL, jako wynik otrzymujesz NULL. Niektóre wyłączają opcję sesji CONCAT_NULL_YIELDS_NULL, która powoduje, że dane wejściowe NULL są konwertowane na pusty ciąg w celu łączenia, ale ta opcja nie jest zalecana, ponieważ stosuje niestandardowe zachowanie. Co więcej, pozostaniesz z wieloma kolejnymi separatorami, gdy pojawią się dane wejściowe NULL, co zwykle nie jest pożądanym zachowaniem. Inną opcją jest jawne zastąpienie danych wejściowych NULL pustym ciągiem za pomocą funkcji ISNULL lub COALESCE, ale zwykle skutkuje to długim, pełnym kodem. Dużo bardziej elegancką opcją jest użycie funkcji CONCAT_WS, która została wprowadzona w SQL Server 2017. Funkcja ta łączy dane wejściowe, ignorując wartości NULL, używając separatora podanego jako pierwsze wejście. Oto zapytanie o rozwiązanie przy użyciu tej funkcji:

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

To zapytanie generuje następujące dane wyjściowe:

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

NA/GDZIE/POSIADAJĄC

Używając klauzul zapytań WHERE, HAVING i ON do celów filtrowania/dopasowywania, należy pamiętać, że używają one trójwartościowej logiki predykatów. Kiedy masz do czynienia z logiką trójwartościową, chcesz dokładnie określić, w jaki sposób klauzula obsługuje przypadki TRUE, FALSE i UNKNOWN. Te trzy klauzule mają na celu akceptowanie przypadków PRAWDZIWYCH i odrzucanie przypadków FAŁSZYWYCH i NIEZNANYCH.

Aby zademonstrować to zachowanie, użyję tabeli o nazwie Kontakty, którą tworzysz i wypełniasz, uruchamiając następujący kod:.

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);

Zwróć uwagę, że kontakty 1 i 2 mają odpowiednie stawki godzinowe, a kontakt 3 nie, więc jego stawka godzinowa jest ustawiona na NULL. Rozważ następujące zapytanie, szukając kontaktów z dodatnią stawką godzinową:

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

Ten predykat przyjmuje wartość TRUE dla styków 1 i 2 oraz UNKNOWN dla styku 3, stąd wyjście zawiera tylko styki 1 i 2:

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

Myślenie tutaj jest takie, że gdy jesteś pewien, że orzeczenie jest prawdziwe, chcesz zwrócić wiersz, w przeciwnym razie chcesz go odrzucić. Na początku może się to wydawać trywialne, dopóki nie zdasz sobie sprawy, że niektóre elementy języka, które również używają predykatów, działają inaczej.

SPRAWDŹ ograniczenie w porównaniu z opcją SPRAWDŹ

Ograniczenie CHECK to narzędzie używane do wymuszania integralności w tabeli na podstawie predykatu. Predykat jest oceniany podczas próby wstawiania lub aktualizowania wierszy w tabeli. W przeciwieństwie do klauzul filtrowania zapytań i dopasowywania, które akceptują przypadki TRUE i odrzucają przypadki FALSE i UNKNOWN, ograniczenie CHECK jest zaprojektowane tak, aby akceptować przypadki TRUE i UNKNOWN oraz odrzucać przypadki FALSE. Myślenie tutaj jest takie, że kiedy jesteś pewien, że orzeczenie jest fałszywe, chcesz odrzucić próbę zmiany, w przeciwnym razie chcesz na to zezwolić.

Jeśli przyjrzysz się definicji naszej tabeli Kontakty, zauważysz, że zawiera ona następujące ograniczenie CHECK, odrzucające kontakty z niedodatnimi stawkami godzinowymi:

CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)

Zwróć uwagę, że ograniczenie używa tego samego predykatu, co w poprzednim filtrze zapytania.

Spróbuj dodać kontakt z dodatnią stawką godzinową:

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

Ta próba się powiodła.

Spróbuj dodać kontakt ze stawką godzinową NULL:

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

Ta próba również się powiedzie, ponieważ ograniczenie CHECK jest zaprojektowane tak, aby akceptować przypadki TRUE i UNKNOWN. Tak jest w przypadku, gdy filtr zapytania i ograniczenie CHECK są zaprojektowane tak, aby działały inaczej.

Spróbuj dodać kontakt z niedodatnią stawką godzinową:

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

Ta próba kończy się niepowodzeniem z następującym błędem:

Msg 547, poziom 16, stan 0, wiersz 454
Instrukcja INSERT była w konflikcie z ograniczeniem CHECK "CHK_Contacts_hourlyrate". Konflikt wystąpił w bazie danych "TSQLV5", tabeli "dbo.Contacts", kolumnie "hourlyrate".

T-SQL pozwala również na wymuszenie integralności modyfikacji poprzez widoki z opcją CHECK. Niektórzy uważają tę opcję za służącą podobnemu celowi do ograniczenia CHECK, o ile zastosujesz modyfikację poprzez widok. Rozważmy na przykład następujący widok, który używa filtra opartego na predykacie stawka godzinowa> 0,00 i jest zdefiniowany za pomocą opcji SPRAWDŹ:

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

Jak się okazuje, w przeciwieństwie do ograniczenia CHECK, opcja widoku CHECK jest zaprojektowana tak, aby akceptować przypadki TRUE i odrzucać zarówno przypadki FALSE, jak i UNKNOWN. Tak więc został zaprojektowany tak, aby zachowywał się bardziej jak filtr zapytań, który zwykle robi, również w celu wymuszenia integralności.

Spróbuj wstawić wiersz z dodatnią stawką godzinową w widoku:

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

Ta próba się powiodła.

Spróbuj wstawić wiersz ze stawką godzinową NULL w widoku:

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

Ta próba kończy się niepowodzeniem z następującym błędem:

Msg 550, Poziom 16, Stan 1, Wiersz 473
Próba wstawienia lub aktualizacji nie powiodła się, ponieważ widok docelowy określa Z OPCJĄ KONTROLI lub obejmuje widok, który określa Z OPCJĄ KONTROLI, a jeden lub więcej wierszy wynikających z operacji nie kwalifikują się w ramach ograniczenia SPRAWDŹ OPCJE.

Chodzi o to, że po dodaniu opcji SPRAWDŹ do widoku, chcesz zezwolić tylko na modyfikacje, których wynikiem będą wiersze zwracane przez widok. To trochę inne niż myślenie z ograniczeniem CHECK — odrzuć zmiany, dla których masz pewność, że orzeczenie jest fałszywe. To może być nieco mylące. Jeśli chcesz, aby widok zezwalał na modyfikacje, które ustawiają stawkę godzinową na NULL, potrzebujesz filtru zapytań, aby również na nie zezwalać, dodając LUB stawka godzinowa IS NULL. Musisz tylko zdać sobie sprawę, że ograniczenie CHECK i opcja CHECK są zaprojektowane tak, aby działały inaczej w odniesieniu do przypadku UNKNOWN. Pierwszy akceptuje to, a drugi odrzuca.

Przeprowadź zapytanie do tabeli Kontakty po wszystkich powyższych zmianach:

SELECT id, name, hourlyrate
FROM dbo.Contacts;

W tym momencie powinieneś otrzymać następujące dane wyjściowe:

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

JEŚLI/POCZĄTKO/SPRAWA

Elementy języka IF, WHILE i CASE działają z predykatami.

Instrukcja IF została zaprojektowana w następujący sposób:

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

Intuicyjnie można oczekiwać bloku TRUE po klauzuli IF i bloku FALSE po klauzuli ELSE, ale musisz zdać sobie sprawę, że klauzula ELSE jest faktycznie aktywowana, gdy predykat jest FALSE lub UNKNOWN. Teoretycznie język logiki o trzech wartościach mógłby mieć stwierdzenie IF z oddzieleniem trzech przypadków. Coś takiego:

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>

A nawet zezwalaj na kombinacje logicznych wyników, tak aby jeśli chcesz połączyć FAŁSZ i NIEZNANE w jedną sekcję, możesz użyć czegoś takiego:

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

W międzyczasie możesz emulować takie konstrukcje, zagnieżdżając instrukcje IF-ELSE i jawnie szukając znaków NULL w operandach za pomocą operatora IS NULL.

Instrukcja WHILE ma tylko blok TRUE. Został zaprojektowany w następujący sposób:

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

Instrukcja lub blok BEGIN-END tworzący ciało pętli jest aktywowany, gdy predykatem jest TURE. Gdy tylko predykat jest FALSE lub UNKNOWN, kontrola przechodzi do instrukcji następującej po pętli WHILE.

W przeciwieństwie do IF i WHILE, które są instrukcjami wykonującymi kod, CASE jest wyrażeniem zwracającym wartość. Składnia wyszukiwanej Wyrażenie CASE wygląda następująco:

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

Wyrażenie CASE ma na celu zwrócenie wyrażenia następującego po klauzuli THEN, która odpowiada pierwszemu predykatowi WHEN, którego wynikiem jest TRUE. Jeśli istnieje klauzula ELSE, jest ona aktywowana, jeśli żaden predykat WHEN nie ma wartości TRUE (wszystkie są FALSE lub UNKNOWN). W przypadku braku jawnej klauzuli ELSE używana jest niejawna klauzula ELSE NULL. Jeśli chcesz oddzielnie obsłużyć sprawę UNKNOWN, możesz jawnie wyszukać wartości NULL w operandach predykatu za pomocą operatora IS NULL.

Prosty Wyrażenie CASE używa niejawnych porównań opartych na równości między wyrażeniem źródłowym a porównywanymi wyrażeniami:

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

Proste wyrażenie CASE zostało zaprojektowane podobnie do przeszukiwanego wyrażenia CASE pod względem obsługi logiki trójwartościowej, ale ponieważ porównania używają niejawnego porównania opartego na równości, nie można osobno obsłużyć przypadku NIEZNANEGO. Próba użycia NULL w jednym z porównywanych wyrażeń w klauzulach WHEN jest bez znaczenia, ponieważ porównanie nie da w wyniku TRUE, nawet jeśli wyrażenie źródłowe ma wartość NULL. Rozważ następujący przykład:

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

Zostanie to domyślnie przekonwertowane na następujące:

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

W rezultacie wynik jest następujący:

Wejście nie ma wartości NULL

Aby wykryć wejście NULL, musisz użyć składni przeszukiwanego wyrażenia CASE i operatora IS NULL, na przykład:

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

Tym razem wynik jest następujący:

Wejście ma wartość NULL

POŁĄCZ

Instrukcja MERGE służy do scalania danych ze źródła do celu. Używasz predykatu scalającego, aby zidentyfikować następujące przypadki i zastosować działanie wobec celu:

  • Wiersz źródłowy jest dopasowywany do wiersza docelowego (aktywowany, gdy zostanie znalezione dopasowanie dla wiersza źródłowego, w którym predykat scalania ma wartość TRUE):zastosuj UPDATE lub DELETE względem celu
  • Wiersz źródłowy nie jest dopasowany do wiersza docelowego (aktywowany, gdy nie znaleziono dopasowań dla wiersza źródłowego, w którym predykat scalający ma wartość TRUE, a dla wszystkich predykatów jest FALSE lub UNKNOWN):zastosuj INSERT względem celu
  • Wiersz docelowy nie jest dopasowany do wiersza źródłowego (aktywowany, gdy nie znaleziono dopasowań dla wiersza docelowego, w którym predykat scalający ma wartość TRUE, a dla wszystkich predykatów jest FALSE lub UNKNOWN):zastosuj UPDATE lub DELETE względem celu

Wszystkie trzy scenariusze oddzielają PRAWDA dla jednej grupy oraz FAŁSZ lub NIEZNANE dla innej. Nie otrzymujesz osobnych sekcji dotyczących obsługi PRAWDA, obsługi FAŁSZ i obsługi NIEZNANYCH spraw.

Aby to zademonstrować, użyję tabeli o nazwie T3, którą tworzysz i wypełniasz, uruchamiając następujący kod:

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);

Rozważ następującą instrukcję MERGE:

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;

Wiersz źródłowy, w którym col1 wynosi 1, jest dopasowywany do wiersza docelowego, w którym col1 wynosi 1 (predykat to PRAWDA), a zatem col2 wiersza docelowego jest ustawiony na 100.

Wiersz źródłowy, w którym col1 wynosi 3, nie jest dopasowany do żadnego wiersza docelowego (dla wszystkich predykatów jest FALSE lub UNKNOWN), a zatem nowy wiersz jest wstawiany do T3 z 3 jako wartością col1 i 300 jako wartością col2.

Wiersze docelowe, w których col1 wynosi 2, a col1 to NULL, nie są dopasowywane do żadnego wiersza źródłowego (dla wszystkich wierszy predykat ma wartość FALSE lub UNKNOWN), a zatem w obu przypadkach col2 w wierszach docelowych jest ustawiony na -1.

Zapytanie przeciwko T3 zwraca następujące dane wyjściowe po wykonaniu powyższej instrukcji MERGE:

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

Trzymaj stół T3 dookoła; jest używany później.

Odróżnianie i grupowanie

W przeciwieństwie do porównań, które są wykonywane przy użyciu operatorów równości i nierówności, porównania wykonane dla celów odrębności i grupowania grupują wartości NULL. Jeden NULL jest uważany za nieróżniący się od innego NULL, ale NULL jest uważany za różny od wartości innej niż NULL. W konsekwencji zastosowanie klauzuli DISTINCT usuwa zduplikowane wystąpienia wartości NULL. Pokazuje to następujące zapytanie:

SELECT DISTINCT country, region FROM HR.Employees;

To zapytanie generuje następujące dane wyjściowe:

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

Istnieje wielu pracowników z krajem USA i regionem NULL, a po usunięciu duplikatów wynik pokazuje tylko jedno wystąpienie kombinacji.

Podobnie jak odrębność, grupowanie również grupuje wartości NULL, jak pokazuje poniższe zapytanie:

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

To zapytanie generuje następujące dane wyjściowe:

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

Ponownie wszyscy czterej pracownicy z kraju Wielka Brytania i regionu NULL zostali zgrupowani razem.

Zamawianie

Porządkowanie traktuje wiele wartości NULL jako mających tę samą wartość porządkowania. Standard SQL pozostawia implementacji wybór, czy uporządkować wartości NULL jako pierwsze, czy ostatnie w porównaniu z wartościami innymi niż NULL. Firma Microsoft zdecydowała się uznać, że wartości NULL mają niższe wartości porządkowania w porównaniu z wartościami innymi niż NULL w SQL Server, więc podczas korzystania z kierunku rosnącego porządku T-SQL najpierw porządkuje wartości NULL. Pokazuje to następujące zapytanie:

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

To zapytanie generuje następujące dane wyjściowe:

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

W przyszłym miesiącu dodam więcej na ten temat, omawiając standardowe elementy, które zapewniają kontrolę nad zachowaniem kolejności NULL i obejść te elementy w T-SQL.

Wyjątkowość

Podczas wymuszania unikalności w kolumnie dopuszczającej wartość NULL przy użyciu ograniczenia UNIQUE lub indeksu unikatowego, T-SQL traktuje wartości NULL tak samo, jak wartości inne niż NULL. Odrzuca zduplikowane wartości NULL, tak jakby jeden NULL nie różnił się od innego NULL.

Przypomnijmy, że nasza tabela T3 ma ograniczenie UNIQUE zdefiniowane na col1. Oto jego definicja:

CONSTRAINT UNQ_T3 UNIQUE(col1)

Zapytaj T3, aby zobaczyć jego aktualną zawartość:

SELECT * FROM dbo.T3;

Jeśli uruchomiłeś wszystkie modyfikacje przeciwko T3 z wcześniejszych przykładów w tym artykule, powinieneś otrzymać następujące dane wyjściowe:

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

Spróbuj dodać drugi wiersz z wartością NULL w kol1:

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

Pojawia się następujący błąd:

Msg 2627, poziom 14, stan 1, wiersz 558
Naruszenie ograniczenia UNIKATOWEGO KLUCZA „UNQ_T3”. Nie można wstawić zduplikowanego klucza w obiekcie „dbo.T3”. Zduplikowana wartość klucza to ().

To zachowanie jest w rzeczywistości niestandardowe. W przyszłym miesiącu opiszę standardową specyfikację i sposób emulowania jej w T-SQL.

Wniosek

W drugiej części serii poświęconej złożonościom NULL skupiłem się na niespójnościach w leczeniu NULL pomiędzy różnymi elementami T-SQL. Omówiłem obliczenia liniowe a agregujące, klauzule filtrujące i dopasowujące, ograniczenie CHECK kontra opcja CHECK, elementy IF, WHILE i CASE, instrukcję MERGE, odrębność i grupowanie, porządkowanie i unikatowość. Niespójności, które omówiłem, dodatkowo podkreślają, jak ważne jest prawidłowe zrozumienie obsługi wartości NULL na platformie, z której korzystasz, aby upewnić się, że piszesz poprawny i solidny kod. W przyszłym miesiącu będę kontynuować serię, omawiając standardowe opcje leczenia SQL NULL, które nie są dostępne w T-SQL, i dostarczając obejścia, które są obsługiwane w T-SQL.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Kto jest aktywnym biegaczem

  2. Uruchomienie bazy danych RAC kończy się niepowodzeniem z błędem ORA-12547

  3. Wprowadzenie do eksploracji danych

  4. Odkrywanie testów jednostkowych Java za pomocą JUnit Test Framework

  5. Tworzenie bardziej zaawansowanego modelu ze statusami użytkownika, wątku i postu