sql >> Databáze >  >> RDS >> Database

Složitosti NULL – 2. část

Tento článek je druhým v řadě o složitosti NULL. Minulý měsíc jsem představil NULL jako značku SQL pro jakýkoli druh chybějící hodnoty. Vysvětlil jsem, že SQL vám neposkytuje možnost rozlišovat mezi chybějícím a použitelným (hodnoty A) a chybějící a nepoužitelné (I-hodnoty) markery. Také jsem vysvětlil, jak porovnávání zahrnující hodnoty NULL fungují s konstantami, proměnnými, parametry a sloupci. Tento měsíc pokračuji v diskusi tím, že se zaměřím na nekonzistence léčby NULL v různých prvcích T-SQL.

Pokračuji v používání ukázkové databáze TSQLV5 jako minulý měsíc v některých mých příkladech. Skript, který vytváří a naplňuje tuto databázi, najdete zde a její ER diagram zde.

Nekonzistence zpracování NULL

Jak jste již zjistili, léčba NULL není triviální. Některé zmatky a složitost souvisí se skutečností, že zacházení s hodnotami NULL může být nekonzistentní mezi různými prvky T-SQL pro podobné operace. V nadcházejících částech popisuji zpracování NULL v lineárních versus agregovaných výpočtech, klauzule ON/WHERE/HAVING, omezení CHECK versus možnost CHECK, prvky IF/WHILE/CASE, příkaz MERGE, odlišnost a seskupování, stejně jako řazení a jedinečnost.

Lineární versus agregované výpočty

T-SQL, a totéž platí pro standardní SQL, používá odlišnou logiku zpracování NULL při použití skutečné agregační funkce, jako je SUM, MIN a MAX, napříč řádky a při použití stejného výpočtu jako lineární přes sloupce. K demonstraci tohoto rozdílu použiji dvě ukázkové tabulky nazvané #T1 a #T2, které vytvoříte a naplníte spuštěním následujícího kódu:

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

Tabulka #T1 má tři sloupce nazvané col1, col2 a col3. Aktuálně má jeden řádek s hodnotami sloupců 10, 5 a NULL:

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

Tabulka #T2 má jeden sloupec s názvem col1. Aktuálně má tři řádky s hodnotami 10, 5 a NULL v col1:

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

Při použití toho, co je v konečném důsledku agregovaný výpočet, jako je sčítání jako lineární přes sloupce, přítomnost jakéhokoli vstupu NULL vede k výsledku NULL. Toto chování demonstruje následující dotaz:

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

Tento dotaz generuje následující výstup:

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

Naopak skutečné agregační funkce, které jsou aplikovány napříč řádky, jsou navrženy tak, aby ignorovaly vstupy NULL. Následující dotaz demonstruje toto chování pomocí funkce SUM:

SELECT SUM(col1) AS total
FROM #T2;

Tento dotaz generuje následující výstup:

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

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

Všimněte si varování nařízeného standardem SQL indikující přítomnost vstupů NULL, které byly ignorovány. Taková varování můžete potlačit vypnutím možnosti relace ANSI_WARNINGS.

Podobně, když je aplikována na vstupní výraz, funkce COUNT počítá počet řádků se vstupními hodnotami, které nejsou NULL (na rozdíl od funkce COUNT(*), která jednoduše počítá počet řádků). Například nahrazení SUM(sloupec1) za COUNT(sloupec1) ve výše uvedeném dotazu vrátí počet 2.

Je zajímavé, že pokud použijete agregaci COUNT na sloupec, který je definován jako nepovolující hodnoty NULL, optimalizátor převede výraz COUNT() na COUNT(*). To umožňuje použití libovolného indexu pro účely počítání na rozdíl od požadavku na použití indexu, který obsahuje příslušný sloupec. To je další důvod nad rámec zajištění konzistence a integrity vašich dat, který by vás měl povzbudit k prosazování omezení, jako je NOT NULL a další. Taková omezení umožňují optimalizátoru větší flexibilitu při zvažování optimálnějších alternativ a vyhýbají se zbytečné práci.

Na základě této logiky funkce AVG vydělí součet hodnot, které nejsou NULL, počtem hodnot, které nejsou NULL. Jako příklad zvažte následující dotaz:

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

Zde je součet hodnot col1 15 bez NULL vydělen počtem hodnot bez NULL 2. Vynásobíte sloupec1 číselným literálem 1.0, abyste vynutili implicitní převod celočíselných vstupních hodnot na číselné, abyste získali číselné dělení a nikoli celé číslo. divize. Tento dotaz generuje následující výstup:

avgall
---------
7.500000

Podobně agregáty MIN a MAX ignorují vstupy NULL. Zvažte následující dotaz:

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

Tento dotaz generuje následující výstup:

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

Pokus o použití lineárních výpočtů, ale emulace sémantiky agregačních funkcí (ignorování hodnot NULL) není hezký. Emulace SUM, COUNT a AVG není příliš složitá, ale vyžaduje, abyste každý vstup zkontrolovali na NULL, například takto:

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;

Tento dotaz generuje následující výstup:

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

Pokus o použití minima nebo maxima jako lineárního výpočtu na více než dva vstupní sloupce je docela ošemetný ještě předtím, než přidáte logiku pro ignorování hodnot NULL, protože zahrnuje vnoření více výrazů CASE buď přímo, nebo nepřímo (při opětovném použití aliasů sloupců). Zde je například dotaz počítající maximum mezi sloupci 1, 2 a 3 v #T1, bez části, která ignoruje hodnoty 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);

Tento dotaz generuje následující výstup:

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

Pokud prozkoumáte plán dotazů, najdete následující rozšířený výraz počítající konečný výsledek:

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

A to je, když se jedná pouze o tři sloupce. Představte si, že se jedná o tucet sloupců!

Nyní k tomu přidejte logiku pro ignorování hodnot 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);

Tento dotaz generuje následující výstup:

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

Oracle má dvojici funkcí nazvaných GREATEST a LEAST, které aplikují minimální a maximální výpočty jako lineární na vstupní hodnoty. Tyto funkce vracejí hodnotu NULL při zadání libovolného vstupu NULL jako většina lineárních výpočtů. Byla zde otevřená položka zpětné vazby požadující získání podobných funkcí v T-SQL, ale tento požadavek nebyl přenesen do jejich poslední změny webu pro zpětnou vazbu. Pokud Microsoft přidá takové funkce do T-SQL, bylo by skvělé mít možnost ovládat, zda ignorovat hodnoty NULL nebo ne.

Mezitím existuje mnohem elegantnější technika ve srovnání s výše uvedenými, která počítá jakýkoli druh agregace jako lineární napříč sloupci pomocí skutečné sémantiky agregační funkce ignorující hodnoty NULL. Používáte kombinaci operátoru CROSS APPLY a odvozeného tabulkového dotazu proti konstruktoru hodnoty tabulky, který otáčí sloupce na řádky a aplikuje agregaci jako skutečnou agregační funkci. Zde je příklad demonstrující výpočty MIN a MAX, ale tuto techniku ​​můžete použít s jakoukoli agregační funkcí, kterou chcete:

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

Tento dotaz generuje následující výstup:

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

Co když chcete opak? Co když potřebujete spočítat agregaci napříč řádky, ale pokud existuje nějaký vstup NULL, vytvoříte hodnotu NULL? Předpokládejme například, že potřebujete sečíst všechny hodnoty col1 z #T1, ale vrátit NULL, pokud je některý ze vstupů NULL. Toho lze dosáhnout pomocí následující techniky:

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

Agregát MIN použijete na výraz CASE, který vrací nuly pro vstupy NULL a jedničky pro vstupy bez NULL. Pokud je tam nějaký vstup NULL, výsledek funkce MIN je 0, jinak je 1. Potom pomocí funkce NULLIF převedete výsledek 0 na NULL. Výsledek funkce NULLIF pak vynásobíte původním součtem. Pokud existuje nějaký vstup NULL, vynásobíte původní součet hodnotou NULL a získáte NULL. Pokud není zadán žádný vstup NULL, vynásobíte výsledek původního součtu 1, čímž získáte původní součet.

Zpět k lineárním výpočtům poskytujícím NULL pro jakýkoli vstup NULL platí stejná logika pro zřetězení řetězců pomocí operátoru +, jak ukazuje následující dotaz:

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

Tento dotaz generuje následující výstup:

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

Chcete zřetězit části umístění zaměstnanců do jednoho řetězce pomocí čárky jako oddělovače. Ale chcete ignorovat NULL vstupy. Místo toho, když je některý ze vstupů NULL, dostanete jako výsledek NULL. Někteří vypnou možnost relace CONCAT_NULL_YIELDS_NULL, což způsobí, že se vstup NULL pro účely zřetězení převede na prázdný řetězec, ale tato možnost se nedoporučuje, protože uplatňuje nestandardní chování. Navíc vám zůstane několik po sobě jdoucích oddělovačů, pokud existují vstupy NULL, což obvykle není žádoucí chování. Další možností je explicitně nahradit NULL vstupy prázdným řetězcem pomocí funkcí ISNULL nebo COALESCE, ale to obvykle vede ke zdlouhavému podrobnému kódu. Mnohem elegantnější možností je použití funkce CONCAT_WS, která byla představena v SQL Server 2017. Tato funkce zřetězí vstupy, ignoruje hodnoty NULL, pomocí oddělovače poskytnutého jako první vstup. Zde je dotaz na řešení pomocí této funkce:

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

Tento dotaz generuje následující výstup:

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

ZAPNUTO/KDE/MÁM

Když používáte klauzule dotazu WHERE, HAVING a ON pro účely filtrování/párování, je důležité si uvědomit, že používají trojhodnotovou predikátovou logiku. Když máte zapojenou logiku se třemi hodnotami, chcete přesně identifikovat, jak klauzule zpracovává případy TRUE, FALSE a NEZNÁMÉ. Tyto tři klauzule jsou navrženy tak, aby akceptovaly PRAVDIVÉ případy a odmítly NEPRAVDA a NEZNÁMÉ případy.

K demonstraci tohoto chování použiji tabulku nazvanou Kontakty, kterou vytvoříte a naplníte spuštěním následujícího kódu:.

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

Všimněte si, že kontakty 1 a 2 mají platné hodinové sazby a kontakt 3 nikoli, takže jeho hodinová sazba je nastavena na NULL. Zvažte následující dotaz, který hledá kontakty s kladnou hodinovou sazbou:

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

Tento predikát se vyhodnotí jako TRUE pro kontakty 1 a 2 a jako UNKNOWN pro kontakt 3, takže výstup obsahuje pouze kontakty 1 a 2:

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

Myšlenka je taková, že když jste si jisti, že predikát je pravdivý, chcete řádek vrátit, jinak ho chcete zahodit. To se může zpočátku zdát triviální, dokud si neuvědomíte, že některé jazykové prvky, které také používají predikáty, fungují jinak.

Omezení CHECK versus možnost CHECK

Omezení CHECK je nástroj, který používáte k vynucení integrity v tabulce na základě predikátu. Predikát je vyhodnocen, když se pokusíte vložit nebo aktualizovat řádky v tabulce. Na rozdíl od filtrování dotazů a odpovídajících klauzulí, které přijímají PRAVDIVÉ případy a odmítají NEPRAVDA a NEZNÁMÉ případy, je omezení CHECK navrženo tak, aby akceptovalo PRAVDIVÉ a NEZNÁMÉ případy a odmítlo NEPRAVDA. Myšlenka je taková, že když jste si jisti, že predikát je nepravdivý, chcete pokus o změnu odmítnout, jinak to chcete povolit.

Pokud prozkoumáte definici naší tabulky Kontakty, všimnete si, že má následující omezení CHECK, které odmítá kontakty s nekladnými hodinovými sazbami:

CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)

Všimněte si, že omezení používá stejný predikát jako ten, který jste použili v předchozím filtru dotazu.

Zkuste přidat kontakt s kladnou hodinovou sazbou:

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

Tento pokus byl úspěšný.

Zkuste přidat kontakt s hodinovou sazbou NULL:

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

Tento pokus je také úspěšný, protože omezení CHECK je navrženo tak, aby akceptovalo PRAVDIVÉ a NEZNÁMÉ případy. To je případ, kdy jsou filtr dotazů a omezení CHECK navrženy tak, aby fungovaly odlišně.

Zkuste přidat kontakt s nekladnou hodinovou sazbou:

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

Tento pokus se nezdaří s následující chybou:

Zpráva 547, úroveň 16, stav 0, řádek 454
Příkaz INSERT byl v konfliktu s omezením CHECK "CHK_Contacts_hourlyrate". Ke konfliktu došlo v databázi „TSQLV5“, tabulce „dbo.Contacts“, sloupci „hodinová sazba“.

T-SQL také umožňuje vynutit integritu modifikací prostřednictvím pohledů pomocí možnosti CHECK. Někteří si myslí, že tato možnost slouží k podobnému účelu jako omezení KONTROLA, pokud použijete úpravu prostřednictvím pohledu. Zvažte například následující pohled, který používá filtr založený na predikátové hodinové sazbě> 0,00 a je definován pomocí možnosti CHECK:

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

Jak se ukázalo, na rozdíl od omezení CHECK je možnost view CHECK navržena tak, aby akceptovala PRAVDIVÉ případy a odmítla FALSE i NEZNÁMÉ případy. Je tedy ve skutečnosti navržen tak, aby se choval více jako filtr dotazů, který se běžně chová, také za účelem vynucení integrity.

Zkuste do zobrazení vložit řádek s kladnou hodinovou sazbou:

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

Tento pokus byl úspěšný.

Zkuste do zobrazení vložit řádek s hodinovou sazbou NULL:

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

Tento pokus se nezdaří s následující chybou:

Zpráva 550, Úroveň 16, Stav 1, Řádek 473
Pokus o vložení nebo aktualizaci selhal, protože cílové zobrazení buď uvádí WITH CHECK OPTION, nebo zahrnuje pohled, který uvádí WITH CHECK OPTION a jeden nebo více řádků vyplývajících z operace neproběhlo splňují podmínku CHECK OPTION.

Myšlenka je taková, že jakmile do pohledu přidáte možnost CHECK, chcete povolit pouze úpravy, jejichž výsledkem jsou řádky, které pohled vrátí. To je trochu jiné než uvažování s omezením CHECK – odmítněte změny, u kterých jste si jisti, že predikát je nepravdivý. To může být trochu matoucí. Pokud chcete, aby zobrazení umožňovalo úpravy, které nastavují hodinovou sazbu na NULL, potřebujete filtr dotazů, aby je také povolil přidáním NEBO hodinová sazba JE NULL. Jen si musíte uvědomit, že omezení CHECK a možnost CHECK jsou navrženy tak, aby fungovaly odlišně s ohledem na případ UNKNOWN. První to přijímá, zatímco druhý to odmítá.

Dotaz na tabulku Kontakty po všech výše uvedených změnách:

SELECT id, name, hourlyrate
FROM dbo.Contacts;

V tomto okamžiku byste měli získat následující výstup:

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

Prvky jazyka IF, WHILE a CASE pracují s predikáty.

Příkaz IF je navržen takto:

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

Je intuitivní očekávat, že po klauzuli IF bude blok TRUE a po klauzuli ELSE blok FALSE, ale musíte si uvědomit, že klauzule ELSE se ve skutečnosti aktivuje, když je predikát FALSE nebo UNKNOWN. Teoreticky by tříhodnotový logický jazyk mohl mít příkaz IF s oddělením tří případů. Něco jako toto:

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 dokonce povolit kombinace logických výsledků, takže pokud byste chtěli spojit FALSE a NEZNÁMÝ do jedné sekce, mohli byste použít něco takového:

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

Mezitím můžete takové konstrukce emulovat vnořením příkazů IF-ELSE a explicitním hledáním NULL v operandech pomocí operátoru IS NULL.

Příkaz WHILE má pouze blok TRUE. Je navržen následovně:

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

Příkaz nebo blok BEGIN-END tvořící tělo smyčky se aktivuje, když je predikát TURE. Jakmile je predikát FALSE nebo UNKNOWN, řízení přejde na příkaz následující po cyklu WHILE.

Na rozdíl od IF a WHILE, což jsou příkazy provádějící kód, CASE je výraz vracející hodnotu. Syntaxe vyhledávaného Výraz CASE je následující:

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

Výraz CASE je navržen tak, aby vrátil výraz za klauzulí THEN, který odpovídá prvnímu predikátu WHEN, který se vyhodnotí jako TRUE. Pokud existuje klauzule ELSE, je aktivována, pokud žádný predikát WHEN není PRAVDA (všechny jsou FALSE nebo UNKNOWN). Bez explicitní klauzule ELSE se použije implicitní ELSE NULL. Pokud chcete případ NEZNÁMÝ zpracovat samostatně, můžete explicitně vyhledat hodnoty NULL v operandech predikátu pomocí operátoru IS NULL.

Jednoduché Výraz CASE používá implicitní porovnání založené na rovnosti mezi zdrojovým výrazem a porovnávanými výrazy:

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

Jednoduchý výraz CASE je navržen podobně jako hledaný výraz CASE, pokud jde o zpracování tříhodnotové logiky, ale protože porovnávání používá implicitní srovnání založené na rovnosti, nemůžete případ UNKNOWN zpracovat samostatně. Pokus o použití NULL v jednom z porovnávaných výrazů v klauzulích WHEN nemá smysl, protože porovnání nebude mít za následek hodnotu TRUE, i když je zdrojový výraz NULL. Zvažte následující příklad:

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

To se implicitně převede na následující:

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

V důsledku toho je výsledek:

Vstup není NULL

Chcete-li zjistit vstup NULL, musíte použít syntaxi hledaného výrazu CASE a operátor IS NULL, například:

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

Tentokrát je výsledek:

Vstup je NULL

SLOUČIT

Příkaz MERGE se používá ke sloučení dat ze zdroje do cíle. Predikát sloučení se používá k identifikaci následujících případů a použití akce proti cíli:

  • Zdrojový řádek se shoduje s cílovým řádkem (aktivuje se, když je nalezena shoda pro zdrojový řádek, kde je predikát sloučení TRUE):použijte UPDATE nebo DELETE proti cíli
  • Zdrojovému řádku neodpovídá cílový řádek (aktivuje se, když nejsou nalezeny žádné shody pro zdrojový řádek, kde je predikát sloučení PRAVDA, spíše pro všechny je predikát FALSE nebo UNKNOWN):použijte INSERT proti cíli
  • Cílovému řádku neodpovídá zdrojový řádek (aktivuje se, když nejsou nalezeny žádné shody pro cílový řádek, kde je predikát sloučení PRAVDA, spíše pro všechny je predikát FALSE nebo UNKNOWN):použijte UPDATE nebo DELETE proti cíli

Všechny tři scénáře oddělují TRUE do jedné skupiny a FALSE nebo UNKNOWN do druhé. Nezískáte samostatné sekce pro zpracování PRAVDIVÝCH, NEPRAVDIVÝCH a NEZNÁMÝCH případů.

Abych to demonstroval, použiji tabulku s názvem T3, kterou vytvoříte a naplníte spuštěním následujícího kódu:

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

Zvažte následující příkaz 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;

Zdrojový řádek, kde sloupec1 je 1, odpovídá cílovému řádku, kde sloupec1 je 1 (predikát je TRUE), a proto je sloupec2 cílového řádku nastaven na 100.

Zdrojový řádek, kde col1 je 3, neodpovídá žádnému cílovému řádku (pro všechny je predikát FALSE nebo UNKNOWN), a proto je do T3 vložen nový řádek s 3 jako hodnotou col1 a 300 jako hodnotou col2.

Cílovým řádkům, kde sloupec1 je 2 a kde sloupec1 je NULL, neodpovídá žádný zdrojový řádek (pro všechny řádky je predikát FALSE nebo UNKNOWN), a proto je v obou případech sloupec2 v cílových řádcích nastaven na -1.

Dotaz na T3 vrátí následující výstup po provedení výše uvedeného příkazu MERGE:

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

Udržujte stůl T3 kolem; použije se později.

Odlišnost a seskupení

Na rozdíl od porovnávání, která se provádějí pomocí operátorů rovnosti a nerovnosti, srovnání prováděná pro účely odlišnosti a seskupování seskupují hodnoty NULL dohromady. Jedna hodnota NULL se považuje za odlišnou od jiné hodnoty NULL, ale hodnota NULL se považuje za odlišnou od hodnoty jiné než NULL. V důsledku toho použití klauzule DISTINCT odstraní duplicitní výskyty hodnot NULL. To ukazuje následující dotaz:

SELECT DISTINCT country, region FROM HR.Employees;

Tento dotaz generuje následující výstup:

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

Země USA a region NULL mají více zaměstnanců a po odstranění duplikátů výsledek ukazuje pouze jeden výskyt kombinace.

Stejně jako odlišnost, seskupování také seskupuje hodnoty NULL dohromady, jak ukazuje následující dotaz:

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

Tento dotaz generuje následující výstup:

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

Opět byli všichni čtyři zaměstnanci se zemí UK a regionem NULL seskupeni.

Objednávání

Řazení považuje více hodnot NULL za stejnou hodnotu řazení. Standard SQL ponechává na implementaci, aby si vybrala, zda se mají hodnoty NULL seřadit jako první nebo jako poslední ve srovnání s hodnotami bez NULL. Společnost Microsoft se rozhodla považovat hodnoty NULL za mající nižší hodnoty řazení ve srovnání s hodnotami bez NULL na serveru SQL Server, takže při použití směru vzestupného pořadí T-SQL nejprve objednává hodnoty NULL. To ukazuje následující dotaz:

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

Tento dotaz generuje následující výstup:

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

Příští měsíc k tomuto tématu přidám více, probereme standardní prvky, které vám dávají kontrolu nad chováním při řazení NULL, a zástupná řešení pro tyto prvky v T-SQL.

Jedinečnost

Při vynucení jedinečnosti u sloupce s možnou hodnotou NULL pomocí omezení UNIQUE nebo jedinečného indexu zachází T-SQL s hodnotami NULL stejně jako s hodnotami bez hodnoty NULL. Odmítá duplicitní hodnoty NULL, jako by jedna hodnota NULL nebyla jedinečná od jiné hodnoty NULL.

Připomeňme, že naše tabulka T3 má UNIKÁTNÍ omezení definované na col1. Zde je jeho definice:

CONSTRAINT UNQ_T3 UNIQUE(col1)

Chcete-li zobrazit jeho aktuální obsah, zadejte dotaz T3:

SELECT * FROM dbo.T3;

Pokud jste spustili všechny úpravy proti T3 z předchozích příkladů v tomto článku, měli byste získat následující výstup:

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

Pokuste se přidat druhý řádek s NULL v col1:

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

Zobrazí se následující chyba:

Zpráva 2627, úroveň 14, stav 1, řádek 558
Porušení omezení UNIQUE KEY 'UNQ_T3'. Nelze vložit duplicitní klíč do objektu 'dbo.T3'. Hodnota duplicitního klíče je ().

Toto chování je ve skutečnosti nestandardní. Příští měsíc popíšu standardní specifikaci a způsob, jak ji emulovat v T-SQL.

Závěr

V této druhé části série o složitosti NULL jsem se zaměřil na nekonzistence zpracování NULL mezi různými prvky T-SQL. Zabýval jsem se lineárními versus agregovanými výpočty, filtrováním a párovacími klauzulemi, omezením CHECK versus možností CHECK, prvky IF, WHILE a CASE, příkazem MERGE, odlišností a seskupováním, řazením a jedinečností. Nekonzistence, kterými jsem se zabýval, dále zdůrazňují, jak důležité je správně porozumět zacházení s hodnotami NULL na platformě, kterou používáte, abyste se ujistili, že píšete správný a robustní kód. Příští měsíc budu v sérii pokračovat tím, že pokryjem standardní možnosti zpracování NULL SQL, které nejsou v T-SQL dostupné, a poskytnu zástupná řešení, která jsou v T-SQL podporována.


  1. Jak funguje funkce Strftime() v SQLite

  2. Jak importovat databázi PostgreSQL pomocí phpPgAdmin

  3. Jak exportovat pole obrázku do souboru?

  4. Neaktivní relace v Oracle od JDBC