sql >> Databasteknik >  >> RDS >> Database

NULL Complexities – Del 2

Den här artikeln är den andra i en serie om NULL-komplexitet. Förra månaden introducerade jag NULL som SQL-markör för alla typer av saknat värde. Jag förklarade att SQL inte ger dig möjligheten att skilja mellan saknade och tillämpliga (A-värden) och saknas och är otillämpliga (I-värden) markörer. Jag förklarade också hur jämförelser som involverar NULL fungerar med konstanter, variabler, parametrar och kolumner. Den här månaden fortsätter jag diskussionen genom att ta upp NULL-behandlingsinkonsekvenser i olika T-SQL-element.

Jag kommer att fortsätta använda exempeldatabasen TSQLV5 som förra månaden i några av mina exempel. Du kan hitta skriptet som skapar och fyller denna databas här, och dess ER-diagram här.

NOLL behandlingsinkonsekvenser

Som du redan har förstått är NULL-behandling inte trivial. En del av förvirringen och komplexiteten har att göra med det faktum att behandlingen av NULL kan vara inkonsekvent mellan olika delar av T-SQL för liknande operationer. I de kommande avsnitten beskriver jag NULL-hantering i linjära kontra aggregerade beräkningar, ON/WHERE/HAVING-satser, CHECK-begränsning kontra CHECK-alternativet, IF/WHILE/CASE-element, MERGE-satsen, distinkthet och gruppering, såväl som ordning och unikhet.

Linjära kontra aggregerade beräkningar

T-SQL, och detsamma gäller för standard SQL, använder olika NULL-hanteringslogik när man tillämpar en faktisk aggregatfunktion som SUM, MIN och MAX över rader jämfört med när man tillämpar samma beräkning som en linjär över kolumner. För att visa denna skillnad använder jag två exempeltabeller som heter #T1 och #T2 som du skapar och fyller i genom att köra följande 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);

Tabellen #T1 har tre kolumner som kallas kol1, kol2 och kol3. Den har för närvarande en rad med kolumnvärdena 10, 5 respektive NULL:

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

Tabellen #T2 har en kolumn som heter kol1. Den har för närvarande tre rader med värdena 10, 5 och NULL i kol1:

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

När man tillämpar vad som i slutändan är en aggregerad beräkning som addition som en linjär över kolumner, ger närvaron av någon NULL-ingång ett NULL-resultat. Följande fråga visar detta beteende:

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

Den här frågan genererar följande utdata:

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

Omvänt är faktiska aggregatfunktioner, som tillämpas över rader, utformade för att ignorera NULL-indata. Följande fråga visar detta beteende med funktionen SUMMA:

SELECT SUM(col1) AS total
FROM #T2;

Den här frågan genererar följande utdata:

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

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

Lägg märke till varningen från SQL-standarden som indikerar närvaron av NULL-indata som ignorerades. Du kan undertrycka sådana varningar genom att stänga av sessionsalternativet ANSI_WARNINGS.

På liknande sätt, när den tillämpas på ett inmatningsuttryck, räknar COUNT-funktionen antalet rader med icke-NULL-indatavärden (i motsats till COUNT(*) som helt enkelt räknar antalet rader). Om du till exempel ersätter SUM(col1) med COUNT(col1) i ovanstående fråga returneras antalet 2.

Märkligt nog, om du tillämpar ett COUNT-aggregat på en kolumn som är definierad som att den inte tillåter NULL, konverterar optimeraren uttrycket COUNT() till COUNT(*). Detta möjliggör användning av vilket index som helst i syfte att räkna i motsats till att kräva användning av ett index som innehåller kolumnen i fråga. Det är ytterligare ett skäl utöver att säkerställa konsistensen och integriteten hos dina data som borde uppmuntra dig att upprätthålla begränsningar som NOT NULL och andra. Sådana begränsningar ger optimeraren mer flexibilitet när det gäller att överväga mer optimala alternativ och undvika onödigt arbete.

Baserat på denna logik dividerar AVG-funktionen summan av icke-NULL-värden med antalet icke-NULL-värden. Betrakta följande fråga som ett exempel:

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

Här divideras summan av icke-NULL col1-värdena 15 med antalet icke-NULL-värden 2. Du multiplicerar col1 med den numeriska literalen 1,0 för att tvinga fram implicit konvertering av heltalsinmatningsvärdena till numeriska ettor för att få numerisk division och inte heltal division. Den här frågan genererar följande utdata:

avgall
---------
7.500000

På liknande sätt ignorerar MIN- och MAX-aggregaten NULL-ingångar. Tänk på följande fråga:

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

Den här frågan genererar följande utdata:

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

Att försöka tillämpa linjära beräkningar men emulera aggregerad funktionssemantik (ignorera NULL) är inte snyggt. Att emulera SUM, COUNT och AVG är inte alltför komplicerat, men det kräver att du kontrollerar varje indata för NULL, som så:

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;

Den här frågan genererar följande utdata:

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

Att försöka tillämpa ett minimum eller maximum som en linjär beräkning på mer än två ingångskolumner är ganska knepigt även innan du lägger till logiken för att ignorera NULLs eftersom det involverar kapsling av flera CASE-uttryck antingen direkt eller indirekt (när du återanvänder kolumnalias). Till exempel, här är en fråga som beräknar maximum bland col1, col2 och col3 i #T1, utan delen som ignorerar 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);

Den här frågan genererar följande utdata:

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

Om du granskar frågeplanen hittar du följande utökade uttryck som beräknar slutresultatet:

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

Och det är då det bara är tre kolumner inblandade. Föreställ dig att ha ett dussin kolumner inblandade!

Lägg nu till logiken för att ignorera 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);

Den här frågan genererar följande utdata:

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

Oracle har ett par funktioner som kallas GREATEST och MINST som tillämpar minimi- respektive maximumberäkningar som linjära på ingångsvärdena. Dessa funktioner returnerar en NULL givet någon NULL-ingång som de flesta linjära beräkningar gör. Det fanns ett öppet återkopplingsobjekt som bad om att få liknande funktioner i T-SQL, men denna begäran överfördes inte i deras senaste återkopplingswebbplatsändring. Om Microsoft lägger till sådana funktioner i T-SQL skulle det vara bra att ha ett alternativ som styr om man vill ignorera NULL eller inte.

Under tiden finns det en mycket mer elegant teknik jämfört med de ovan nämnda som beräknar vilken typ av aggregat som helst som ett linjärt över kolumner med hjälp av faktisk aggregatfunktionssemantik som ignorerar NULL. Du använder en kombination av operatorn CROSS APPLY och en härledd tabellfråga mot en tabellvärdekonstruktor som roterar kolumner till rader och tillämpar aggregatet som en faktisk aggregatfunktion. Här är ett exempel som visar MIN- och MAX-beräkningarna, men du kan använda den här tekniken med vilken aggregerad funktion du vill:

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

Den här frågan genererar följande utdata:

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

Tänk om du vill ha motsatsen? Vad händer om du behöver beräkna ett aggregat över rader, men producera en NULL om det finns någon NULL-ingång? Anta till exempel att du behöver summera alla col1-värden från #T1, men returnera NULL om någon av ingångarna är NULL. Detta kan uppnås med följande teknik:

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

Du tillämpar ett MIN-aggregat på ett CASE-uttryck som returnerar nollor för NULL-indata och ettor för icke-NULL-indata. Om det finns någon NULL-ingång är resultatet av MIN-funktionen 0, annars är det 1. Med hjälp av NULLIF-funktionen konverterar du sedan ett 0-resultat till ett NULL. Du multiplicerar sedan resultatet av NULLIF-funktionen med den ursprungliga summan. Om det finns någon NULL-ingång multiplicerar du den ursprungliga summan med en NULL, vilket ger en NULL. Om det inte finns någon NULL-inmatning multiplicerar du resultatet av den ursprungliga summan med 1, vilket ger den ursprungliga summan.

Tillbaka till linjära beräkningar som ger en NULL för alla NULL-indata, samma logik gäller för strängsammansättning med operatorn +, som följande fråga visar:

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

Den här frågan genererar följande utdata:

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

Du vill sammanfoga positionsdelarna för anställda i en sträng med ett kommatecken som avgränsare. Men du vill ignorera NULL-ingångar. Istället, när någon av ingångarna är en NULL, får du en NULL som resultat. Vissa stänger av sessionsalternativet CONCAT_NULL_YIELDS_NULL, vilket gör att en NULL-ingång konverteras till en tom sträng för sammanlänkningsändamål, men det här alternativet rekommenderas inte eftersom det tillämpar icke-standardiserat beteende. Dessutom kommer du att sitta kvar med flera på varandra följande separatorer när det finns NULL-ingångar, vilket vanligtvis inte är det önskade beteendet. Ett annat alternativ är att explicit ersätta NULL-inmatningar med en tom sträng med funktionerna ISNULL eller COALESCE, men detta resulterar vanligtvis i en lång, utförlig kod. Ett mycket mer elegant alternativ är att använda CONCAT_WS-funktionen, som introducerades i SQL Server 2017. Denna funktion sammanfogar ingångarna, ignorerar NULL, med hjälp av separatorn som tillhandahålls som den första ingången. Här är lösningsfrågan med den här funktionen:

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

Den här frågan genererar följande utdata:

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

PÅ/VAR/HA

När du använder frågesatserna WHERE, HAVING och ON för filtrerings-/matchningsändamål är det viktigt att komma ihåg att de använder trevärdespredikatlogik. När du har logik med tre värden inblandad, vill du noggrant identifiera hur satsen hanterar SANN, FALSK och OKÄNDA fall. Dessa tre klausuler är utformade för att acceptera TRUE fall och förkasta FALSE och OKÄNDA fall.

För att demonstrera detta beteende använder jag en tabell som heter Kontakter som du skapar och fyller i genom att köra följande 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);

Observera att kontakt 1 och 2 har tillämpliga timpriser, och kontakt 3 har inte, så dess timpris är satt till NULL. Överväg följande fråga för att leta efter kontakter med ett positivt timpris:

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

Detta predikat utvärderas till TRUE för kontakterna 1 och 2 och till OKÄNDA för kontakt 3, därför innehåller utgången endast kontakterna 1 och 2:

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

Tanken här är att när du är säker på att predikatet är sant, vill du returnera raden, annars vill du kassera den. Detta kan tyckas trivialt till en början, tills du inser att vissa språkelement som också använder predikat fungerar annorlunda.

Kontrollera begränsning kontra alternativet KONTROLL

En CHECK-begränsning är ett verktyg som du använder för att upprätthålla integritet i en tabell baserat på ett predikat. Predikatet utvärderas när du försöker infoga eller uppdatera rader i tabellen. Till skillnad från frågefiltrering och matchningssatser som accepterar TRUE-fall och avvisar FALSE och OKÄNDA fall, är en CHECK-begränsning utformad för att acceptera TRUE och OKÄNDA fall och förkasta FALSE-fall. Tanken här är att när du är säker på att predikatet är falskt, vill du avvisa försöket att ändra, annars vill du tillåta det.

Om du undersöker definitionen av vår kontakttabell kommer du att märka att den har följande CHECK-begränsning, vilket avvisar kontakter med icke-positiva timpriser:

CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)

Observera att begränsningen använder samma predikat som det du använde i föregående frågefilter.

Försök att lägga till en kontakt med ett positivt timpris:

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

Detta försök lyckas.

Försök att lägga till en kontakt med NULL timpris:

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

Detta försök lyckas också, eftersom en CHECK-begränsning är utformad för att acceptera SANTA och OKÄNDA fall. Det är fallet när ett frågefilter och en CHECK-begränsning är utformade för att fungera annorlunda.

Försök att lägga till en kontakt med ett icke-positivt timpris:

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

Detta försök misslyckas med följande fel:

Msg 547, Level 16, State 0, Line 454
INSERT-satsen stod i konflikt med CHECK-begränsningen "CHK_Contacts_hourlyrate". Konflikten inträffade i databasen "TSQLV5", tabellen "dbo.Contacts", kolumnen "timpris".

T-SQL låter dig också upprätthålla modifieringars integritet genom vyer med hjälp av ett CHECK-alternativ. Vissa tycker att det här alternativet tjänar ett liknande syfte som en CHECK-begränsning så länge du tillämpar ändringen genom vyn. Tänk till exempel på följande vy, som använder ett filter baserat på predikatet timpris> 0,00 och definieras med alternativet CHECK:

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

Som det visar sig, till skillnad från en CHECK-begränsning, är alternativet view CHECK utformat för att acceptera TRUE fall och avvisa både FALSE och OKÄNDA fall. Så det är faktiskt utformat för att bete sig mer som frågefiltret normalt gör också i syfte att upprätthålla integritet.

Testa att infoga en rad med ett positivt timpris genom vyn:

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

Detta försök lyckas.

Försök att infoga en rad med ett NULL timpris genom vyn:

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

Detta försök misslyckas med följande fel:

Msg 550, Level 16, State 1, Line 473
Försöket att infoga eller uppdatera misslyckades eftersom målvyn antingen anger WITH CHECK OPTION eller sträcker sig över en vy som specificerar WITH CHECK OPTION och en eller flera rader som härrörde från operationen inte kvalificera sig under begränsningen KONTROLLOPTION.

Tanken här är att när du väl lägger till alternativet CHECK i vyn, vill du bara tillåta ändringar som resulterar i rader som returneras av vyn. Det är lite annorlunda än tänkandet med en CHECK-begränsning – avvisa ändringar för vilka du är säker på att predikatet är falskt. Detta kan vara lite förvirrande. Om du vill att vyn ska tillåta ändringar som ställer in timpriset till NULL, behöver du frågefiltret för att även tillåta dessa genom att lägga till ELLER timpris ÄR NULL. Du behöver bara inse att en CHECK-begränsning och ett CHECK-alternativ är utformade för att fungera annorlunda med avseende på det OKÄNDA fallet. Den förra accepterar den medan den senare förkastar den.

Fråga i tabellen Kontakter efter alla ovanstående ändringar:

SELECT id, name, hourlyrate
FROM dbo.Contacts;

Du bör få följande utdata vid denna tidpunkt:

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

OM/MED/FALL

Språkelementen IF, WHILE och CASE fungerar med predikat.

IF-satsen är utformad enligt följande:

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

Det är intuitivt att förvänta sig att ha ett TRUE-block efter IF-satsen och ett FALSE-block efter ELSE-satsen, men du måste inse att ELSE-satsen faktiskt aktiveras när predikatet är FALSE eller OKÄNT. Teoretiskt sett kunde ett trevärdigt logiskt språk ha haft ett IF-uttalande med en separation av de tre fallen. Något så här:

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>

Och tillåt till och med kombinationer av logiska utfall så att om du vill kombinera FALSK och OKÄNT till ett avsnitt, kan du använda något sånt här:

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

Under tiden kan du emulera sådana konstruktioner genom att kapsla IF-ELSE-satser och uttryckligen leta efter NULLs i operanderna med IS NULL-operatorn.

WHILE-satsen har bara ett TRUE-block. Den är utformad enligt följande:

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

Satsen eller BEGIN-END-blocket som bildar slingans kropp aktiveras medan predikatet är TURE. Så snart predikatet är FALSE eller OKÄNT, övergår kontrollen till satsen efter WHILE-slingan.

Till skillnad från IF och WHILE, som är programsatser som exekverar kod, är CASE ett uttryck som returnerar ett värde. Syntaxen för en sökt CASE-uttryck är som följer:

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

Ett CASE-uttryck är utformat för att returnera uttrycket efter THEN-satsen som motsvarar det första WHEN-predikatet som utvärderas till TRUE. Om det finns en ELSE-sats, aktiveras den om inget WHEN-predikat är SANT (alla är FALSE eller OKÄNDA). Om en explicit ELSE-sats saknas, används en implicit ELSE NULL. Om du vill hantera ett OKÄNT fall separat, kan du uttryckligen leta efter NULL i predikatets operander med hjälp av IS NULL-operatorn.

En enkel CASE-uttryck använder implicita likhetsbaserade jämförelser mellan källuttrycket och de jämförda uttrycken:

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

Det enkla CASE-uttrycket är utformat liknande det sökta CASE-uttrycket när det gäller hanteringen av logiken med tre värden, men eftersom jämförelserna använder en implicit likhetsbaserad jämförelse kan du inte hantera det OKÄNDA fallet separat. Ett försök att använda en NULL i ett av de jämförda uttrycken i WHEN-satserna är meningslöst eftersom jämförelsen inte kommer att resultera i TRUE även när källuttrycket är NULL. Tänk på följande exempel:

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

Detta konverteras implicit till följande:

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

Följaktligen är resultatet:

Ingången är inte NULL

För att upptäcka en NULL-inmatning måste du använda den sökta CASE-uttryckssyntaxen och IS NULL-operatorn, så här:

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

Denna gång är resultatet:

Ingången är NULL

SAMMANSLUT

MERGE-satsen används för att slå samman data från en källa till ett mål. Du använder ett sammanslagningspredikat för att identifiera följande fall och tillämpa en åtgärd mot målet:

  • En källrad matchas av en målrad (aktiveras när en matchning hittas för källraden där sammanslagningspredikatet är TRUE):tillämpa UPPDATERA eller DELETE mot målet
  • En källrad matchas inte av en målrad (aktiverad när inga matchningar hittas för källraden där sammanslagningspredikatet är SANT, snarare för allt predikatet är FALSK eller OKÄNT):applicera en INSERT mot målet
  • >
  • En målrad matchas inte av en källrad (aktiverad när inga matchningar hittas för målraden där sammanslagningspredikatet är SANT, snarare för allt predikatet är FALSK eller OKÄNT):tillämpa UPPDATERA eller DELETE mot målet

Alla tre scenarierna skiljer TRUE till en grupp och FALSE eller OKÄNT till en annan. Du får inte separata avsnitt för hantering av SANT, hantering av FALSK och hantering av OKÄNDA ärenden.

För att demonstrera detta använder jag en tabell som heter T3 som du skapar och fyller i genom att köra följande 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);

Tänk på följande MERGE-sats:

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;

Källraden där kol1 är 1 matchas av målraden där kol1 är 1 (predikatet är TRUE) och därför är målradens kol2 satt till 100.

Källraden där col1 är 3 matchas inte av någon målrad (för alla är predikatet FALSK eller OKÄNT) och därför infogas en ny rad i T3 med 3 som col1-värde och 300 som col2-värde.

Målraderna där col1 är 2 och där col1 är NULL matchas inte av någon källrad (för alla rader är predikatet FALSK eller OKÄNT) och därför är i båda fallen col2 i målraderna satt till -1.

Frågan mot T3 returnerar följande utdata efter exekvering av ovanstående MERGE-sats:

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

Håll bord T3 runt; den används senare.

Distinkt och gruppering

Till skillnad från jämförelser som görs med operatorer för jämlikhet och ojämlikhet, grupperar jämförelser gjorda för distinktions- och grupperingsändamål NULLs tillsammans. En NULL anses inte vara skild från en annan NULL, men en NULL anses vara skild från ett icke-NULL-värde. Följaktligen tar tillämpning av en DISTINCT-sats bort dubbla förekomster av NULL. Följande fråga visar detta:

SELECT DISTINCT country, region FROM HR.Employees;

Den här frågan genererar följande utdata:

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

Det finns flera anställda med landet USA och regionen NULL, och efter borttagning av dubbletter visar resultatet endast en förekomst av kombinationen.

Liksom distinkt, grupperar gruppering också NULLs tillsammans, vilket följande fråga visar:

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

Den här frågan genererar följande utdata:

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

Återigen, alla fyra anställda med landet Storbritannien och regionen NULL grupperades tillsammans.

Beställer

Beställning behandlar flera NULLs som att ha samma beställningsvärde. SQL-standarden överlåter till implementeringen att välja om NULL ska beställas först eller sist jämfört med icke-NULL-värden. Microsoft valde att betrakta NULLs som att ha lägre ordningsvärden jämfört med icke-NULLs i SQL Server, så när man använder stigande ordningsriktning beställer T-SQL NULL först. Följande fråga visar detta:

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

Den här frågan genererar följande utdata:

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ästa månad kommer jag att lägga till mer om det här ämnet och diskutera standardelement som ger dig kontroll över NULL-orderbeteende och lösningarna för dessa element i T-SQL.

Unikhet

När man upprätthåller unikhet på en NULL-bar kolumn med antingen en UNIQUE begränsning eller ett unikt index, behandlar T-SQL NULLs precis som icke-NULL-värden. Den avvisar dubbletter av NULL som om en NULL inte är unik från en annan NULL.

Kom ihåg att vår tabell T3 har en UNIK begränsning definierad på kol1. Här är dess definition:

CONSTRAINT UNQ_T3 UNIQUE(col1)

Fråga T3 för att se dess nuvarande innehåll:

SELECT * FROM dbo.T3;

Om du körde alla ändringar mot T3 från de tidigare exemplen i den här artikeln, bör du få följande utdata:

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

Försök att lägga till en andra rad med en NULL i kol1:

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

Du får följande felmeddelande:

Msg 2627, Level 14, State 1, Line 558
Brott mot UNIQUE KEY-begränsningen 'UNQ_T3'. Kan inte infoga dubblettnyckel i objektet 'dbo.T3'. Dubblettnyckelvärdet är ().

Detta beteende är faktiskt icke-standardiserat. Nästa månad kommer jag att beskriva standardspecifikationen och hur man emulerar den i T-SQL.

Slutsats

I den här andra delen av serien om NULL-komplexitet fokuserade jag på NULL-behandlingsinkonsekvenser mellan olika T-SQL-element. Jag täckte linjära kontra aggregerade beräkningar, filtrerings- och matchningssatser, CHECK-begränsningen kontra CHECK-alternativet, IF, WHILE och CASE-element, MERGE-satsen, distinkthet och gruppering, ordning och unikhet. Inkonsekvenserna jag täckte understryker ytterligare hur viktigt det är att korrekt förstå behandlingen av NULLs i plattformen du använder, för att se till att du skriver korrekt och robust kod. Nästa månad kommer jag att fortsätta serien genom att täcka SQL-standarden NULL-behandlingsalternativ som inte är tillgängliga i T-SQL, och tillhandahålla lösningar som stöds i T-SQL.


  1. Postgis installation:typ geometri existerar inte

  2. Hur går man med på samma bord, två gånger, i mysql?

  3. Konvertera från asynkron till synkron replikering i PostgreSQL

  4. Hur man hittar skillnaden mellan två datum i MySQL