sql >> Database >  >> RDS >> Database

NULL Complexiteiten – Deel 2

Dit artikel is het tweede in een serie over NULL-complexiteit. Vorige maand introduceerde ik de NULL als SQL-markering voor elke vorm van ontbrekende waarde. Ik heb uitgelegd dat SQL je niet de mogelijkheid biedt om onderscheid te maken tussen ontbrekende en van toepassing zijnde (A-waarden) en ontbrekend en niet van toepassing (I-waarden) markeringen. Ik heb ook uitgelegd hoe vergelijkingen met NULL's werken met constanten, variabelen, parameters en kolommen. Deze maand zet ik de discussie voort door in te gaan op inconsistenties in de NULL-behandeling in verschillende T-SQL-elementen.

Ik zal de voorbeelddatabase TSQLV5 zoals vorige maand in sommige van mijn voorbeelden blijven gebruiken. U vindt het script dat deze database maakt en vult hier, en het ER-diagram hier.

NULL-behandelingsinconsistenties

Zoals je al hebt begrepen, is een NULL-behandeling niet triviaal. Een deel van de verwarring en complexiteit heeft te maken met het feit dat de behandeling van NULL's inconsistent kan zijn tussen verschillende elementen van T-SQL voor vergelijkbare bewerkingen. In de komende paragrafen beschrijf ik NULL-afhandeling in lineaire versus geaggregeerde berekeningen, ON/WHERE/HAVING-clausules, CHECK-beperking versus CHECK-optie, IF/WHILE/CASE-elementen, de MERGE-instructie, onderscheidbaarheid en groepering, evenals ordening en uniciteit.

Lineaire versus geaggregeerde berekeningen

T-SQL, en hetzelfde geldt voor standaard SQL, gebruikt andere NULL-verwerkingslogica bij het toepassen van een werkelijke aggregatiefunctie zoals SUM, MIN en MAX op rijen versus bij het toepassen van dezelfde berekening als een lineaire op kolommen. Om dit verschil te demonstreren, gebruik ik twee voorbeeldtabellen genaamd #T1 en #T2 die u maakt en vult door de volgende code uit te voeren:

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

De tabel #T1 heeft drie kolommen genaamd col1, col2 en col3. Het heeft momenteel één rij met respectievelijk de kolomwaarden 10, 5 en NULL:

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

De tabel #T2 heeft één kolom genaamd col1. Het heeft momenteel drie rijen met de waarden 10, 5 en NULL in col1:

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

Bij het toepassen van wat uiteindelijk een geaggregeerde berekening is, zoals optellen als een lineaire over kolommen, levert de aanwezigheid van een NULL-invoer een NULL-resultaat op. De volgende query demonstreert dit gedrag:

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

Deze query genereert de volgende uitvoer:

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

Daarentegen zijn feitelijke aggregatiefuncties, die over rijen worden toegepast, ontworpen om NULL-invoer te negeren. De volgende query demonstreert dit gedrag met behulp van de SUM-functie:

SELECT SUM(col1) AS total
FROM #T2;

Deze query genereert de volgende uitvoer:

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

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

Let op de waarschuwing opgelegd door de SQL-standaard die de aanwezigheid aangeeft van NULL-invoer die werd genegeerd. U kunt dergelijke waarschuwingen onderdrukken door de sessie-optie ANSI_WARNINGS uit te schakelen.

Evenzo, wanneer toegepast op een invoeruitdrukking, telt de functie AANTAL het aantal rijen met niet-NULL-invoerwaarden (in tegenstelling tot AANTAL(*) dat eenvoudig het aantal rijen telt). Als u bijvoorbeeld SUM(col1) vervangt door COUNT(col1) in de bovenstaande query, wordt de telling van 2 geretourneerd.

Vreemd genoeg, als u een COUNT-aggregaat toepast op een kolom die is gedefinieerd als het niet toestaan ​​van NULL's, converteert de optimizer de expressie COUNT() naar COUNT(*). Dit maakt het gebruik van elke index mogelijk om te tellen, in plaats van het gebruik van een index die de betreffende kolom bevat. Dat is nog een reden, naast het waarborgen van de consistentie en integriteit van uw gegevens, die u zou moeten aanmoedigen om beperkingen zoals NOT NULL en andere af te dwingen. Dergelijke beperkingen geven de optimizer meer flexibiliteit bij het overwegen van meer optimale alternatieven en het vermijden van onnodig werk.

Op basis van deze logica deelt de AVG-functie de som van niet-NULL-waarden door het aantal niet-NULL-waarden. Beschouw de volgende vraag als voorbeeld:

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

Hier wordt de som van de niet-NULL col1-waarden 15 gedeeld door het aantal niet-NULL-waarden 2. U vermenigvuldigt col1 met de numerieke letterlijke 1.0 om impliciete conversie van de gehele invoerwaarden naar numerieke waarden te forceren om een ​​numerieke deling te krijgen en niet een geheel getal divisie. Deze query genereert de volgende uitvoer:

avgall
---------
7.500000

Evenzo negeren de MIN- en MAX-aggregaten NULL-invoer. Overweeg de volgende vraag:

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

Deze query genereert de volgende uitvoer:

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

Pogingen om lineaire berekeningen toe te passen, maar de semantiek van geaggregeerde functies na te bootsen (negeer NULL's) is niet mooi. Het emuleren van SUM, COUNT en AVG is niet al te ingewikkeld, maar het vereist wel dat je elke invoer op NULL's controleert, zoals:

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;

Deze query genereert de volgende uitvoer:

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

Pogingen om een ​​minimum of maximum als lineaire berekening toe te passen op meer dan twee invoerkolommen is behoorlijk lastig, zelfs voordat u de logica toevoegt om NULL's te negeren, aangezien het gaat om het direct of indirect nesten van meerdere CASE-expressies (wanneer u kolomaliassen opnieuw gebruikt). Hier is bijvoorbeeld een query die het maximum berekent tussen col1, col2 en col3 in #T1, zonder het deel dat NULL's negeert:

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

Deze query genereert de volgende uitvoer:

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

Als je het zoekplan bekijkt, zul je de volgende uitgebreide expressie vinden die het uiteindelijke resultaat berekent:

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

En dat is wanneer er maar drie kolommen bij betrokken zijn. Stel je voor dat er een dozijn kolommen bij betrokken zijn!

Voeg hier nu de logica aan toe om NULL's te negeren:

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

Deze query genereert de volgende uitvoer:

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

Oracle heeft een paar functies, GREATEST en LEAST genaamd, die respectievelijk minimum- en maximumberekeningen als lineaire op de invoerwaarden toepassen. Deze functies retourneren een NULL, gegeven elke NULL-invoer, zoals de meeste lineaire berekeningen doen. Er was een open feedback-item waarin werd gevraagd om vergelijkbare functies in T-SQL te krijgen, maar dit verzoek is niet overgenomen in hun laatste wijziging van de feedbacksite. Als Microsoft dergelijke functies aan T-SQL toevoegt, zou het geweldig zijn om een ​​optie te hebben die bepaalt of NULL's worden genegeerd of niet.

Ondertussen is er een veel elegantere techniek in vergelijking met de bovengenoemde technieken die elk soort aggregaat als een lineaire over kolommen berekent met behulp van werkelijke aggregatiefunctiesemantiek waarbij NULL's worden genegeerd. U gebruikt een combinatie van de CROSS APPLY-operator en een afgeleide tabelquery tegen een tabelwaardeconstructor die kolommen naar rijen roteert en de aggregatie toepast als een daadwerkelijke aggregatiefunctie. Hier is een voorbeeld dat de MIN- en MAX-berekeningen demonstreert, maar u kunt deze techniek gebruiken met elke aggregatiefunctie die u leuk vindt:

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

Deze query genereert de volgende uitvoer:

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

Wat als je het tegenovergestelde wilt? Wat als u een aggregaat over rijen moet berekenen, maar een NULL moet produceren als er een NULL-invoer is? Stel bijvoorbeeld dat u alle col1-waarden van #T1 moet optellen, maar NULL moet retourneren als een van de invoer NULL is. Dit kan worden bereikt met de volgende techniek:

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

U past een MIN-aggregatie toe op een CASE-expressie die nullen retourneert voor NULL-invoer en enen voor niet-NULL-invoer. Als er een NULL-invoer is, is het resultaat van de MIN-functie 0, anders is het 1. Met behulp van de NULLIF-functie converteert u een 0-resultaat naar een NULL. Vervolgens vermenigvuldig je het resultaat van de NULLIF-functie met de oorspronkelijke som. Als er een NULL-invoer is, vermenigvuldigt u de oorspronkelijke som met een NULL, wat een NULL oplevert. Als er geen NULL-invoer is, vermenigvuldigt u het resultaat van de oorspronkelijke som met 1, wat de oorspronkelijke som oplevert.

Terug naar lineaire berekeningen die een NULL opleveren voor elke NULL-invoer, dezelfde logica is van toepassing op stringconcatenatie met behulp van de + operator, zoals de volgende query laat zien:

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

Deze query genereert de volgende uitvoer:

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

U wilt de locatiedelen van werknemers samenvoegen tot één tekenreeks, met een komma als scheidingsteken. Maar u wilt NULL-invoer negeren. In plaats daarvan, wanneer een van de invoer een NULL is, krijgt u een NULL als resultaat. Sommige schakelen de CONCAT_NULL_YIELDS_NULL-sessieoptie uit, waardoor een NULL-invoer wordt geconverteerd naar een lege tekenreeks voor aaneenschakelingsdoeleinden, maar deze optie wordt niet aanbevolen omdat het niet-standaard gedrag toepast. Bovendien houdt u meerdere opeenvolgende scheidingstekens over wanneer er NULL-invoer is, wat meestal niet het gewenste gedrag is. Een andere optie is om NULL-invoer expliciet te vervangen door een lege tekenreeks met behulp van de ISNULL- of COALESCE-functies, maar dit resulteert meestal in lange uitgebreide code. Een veel elegantere optie is om de CONCAT_WS-functie te gebruiken, die werd geïntroduceerd in SQL Server 2017. Deze functie voegt de invoer samen, negeert NULL's en gebruikt het scheidingsteken als eerste invoer. Hier is de oplossingsquery die deze functie gebruikt:

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

Deze query genereert de volgende uitvoer:

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

OP/WAAR/HEEFT

Wanneer u de WHERE-, HAVING- en ON-queryclausules gebruikt voor filter-/overeenkomstdoeleinden, is het belangrijk om te onthouden dat ze predikatenlogica met drie waarden gebruiken. Als je logica met drie waarden hebt, wil je nauwkeurig identificeren hoe de clausule TRUE, FALSE en UNKNOWN-gevallen afhandelt. Deze drie clausules zijn bedoeld om TRUE-gevallen te accepteren en FALSE- en UNKNOWN-gevallen te verwerpen.

Om dit gedrag te demonstreren, gebruik ik een tabel met de naam Contacten die u maakt en invult door de volgende code uit te voeren:.

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

Merk op dat contact 1 en 2 toepasselijke uurtarieven hebben en contact 3 niet, dus het uurtarief is ingesteld op NULL. Overweeg de volgende vraag op zoek naar contacten met een positief uurtarief:

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

Dit predikaat evalueert TRUE voor contacten 1 en 2, en ONBEKEND voor contact 3, vandaar dat de output alleen contacten 1 en 2 bevat:

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

De gedachte hier is dat wanneer u zeker weet dat het predikaat waar is, u de rij wilt retourneren, anders wilt u deze weggooien. Dit lijkt in eerste instantie misschien triviaal, totdat je je realiseert dat sommige taalelementen die ook predikaten gebruiken anders werken.

CHECK beperking versus CHECK optie

Een CHECK-beperking is een hulpmiddel dat u gebruikt om integriteit in een tabel af te dwingen op basis van een predikaat. Het predikaat wordt geëvalueerd wanneer u probeert rijen in de tabel in te voegen of bij te werken. In tegenstelling tot clausules voor het filteren en matchen van zoekopdrachten die TRUE-cases accepteren en FALSE- en UNKNOWN-cases afwijzen, is een CHECK-beperking ontworpen om TRUE en UNKNOWN-cases te accepteren en FALSE-cases af te wijzen. De gedachte hier is dat wanneer u zeker weet dat het predikaat onwaar is, u de wijzigingspoging wilt afwijzen, anders wilt u deze toestaan.

Als je de definitie van onze tabel Contacten bekijkt, zul je merken dat deze de volgende CHECK-beperking heeft, waarbij contacten met niet-positieve uurtarieven worden afgewezen:

CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)

Merk op dat de beperking hetzelfde predikaat gebruikt als het predikaat dat u in het vorige queryfilter hebt gebruikt.

Probeer een contact met een positief uurtarief toe te voegen:

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

Deze poging slaagt.

Probeer een contact met een NULL-uurtarief toe te voegen:

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

Deze poging slaagt ook, aangezien een CHECK-beperking is ontworpen om TRUE en UNKNOWN-gevallen te accepteren. Dat is het geval wanneer een queryfilter en een CHECK-beperking zijn ontworpen om anders te werken.

Probeer een contact met een niet-positief uurtarief toe te voegen:

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

Deze poging mislukt met de volgende fout:

Msg 547, Level 16, State 0, Line 454
De INSERT-instructie was in strijd met de CHECK-beperking "CHK_Contacts_hourlyrate". Het conflict deed zich voor in database "TSQLV5", tabel "dbo.Contacts", kolom 'uurtarief'.

Met T-SQL kunt u ook de integriteit van wijzigingen afdwingen via weergaven met behulp van een CHECK-optie. Sommigen beschouwen deze optie als een soortgelijk doel als een CHECK-beperking, zolang u de wijziging toepast via de weergave. Beschouw bijvoorbeeld de volgende weergave, die een filter gebruikt op basis van het predikaat uurtarief> 0,00 en is gedefinieerd met de optie CONTROLEREN:

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

Het blijkt dat, in tegenstelling tot een CHECK-beperking, de optie CHECK is ontworpen om TRUE-cases te accepteren en zowel FALSE- als UNKNOWN-cases te verwerpen. Het is dus eigenlijk ontworpen om zich meer te gedragen zoals het queryfilter dat normaal ook doet, ook met het oog op het afdwingen van integriteit.

Probeer een rij met een positief uurtarief in te voegen via de weergave:

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

Deze poging slaagt.

Probeer een rij met een NULL-uurtarief in te voegen via de weergave:

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

Deze poging mislukt met de volgende fout:

Msg 550, Level 16, State 1, Line 473
De poging tot invoegen of bijwerken is mislukt omdat de doelweergave ofwel WITH CHECK OPTION specificeert ofwel een view omvat die WITH CHECK OPTION specificeert en een of meer rijen die het resultaat zijn van de bewerking niet kwalificeren onder de beperking CHECK OPTION.

De gedachte hier is dat zodra u de optie CONTROLEREN aan de weergave hebt toegevoegd, u alleen wijzigingen wilt toestaan ​​die resulteren in rijen die door de weergave worden geretourneerd. Dat is een beetje anders dan denken met een CHECK-beperking:verwerp wijzigingen waarvan u zeker weet dat het predikaat onwaar is. Dit kan een beetje verwarrend zijn. Als u wilt dat de weergave wijzigingen toestaat die het uurtarief op NULL zetten, hebt u het queryfilter nodig om deze ook toe te staan ​​door OR uurtarief IS NULL toe te voegen. U hoeft zich alleen maar te realiseren dat een CHECK-beperking en een CHECK-optie zijn ontworpen om anders te werken met betrekking tot het ONBEKENDE geval. De eerste accepteert het, terwijl de laatste het afwijst.

Vraag de tabel Contacten na alle bovenstaande wijzigingen:

SELECT id, name, hourlyrate
FROM dbo.Contacts;

U zou op dit punt de volgende uitvoer moeten krijgen:

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

De taalelementen IF, WHILE en CASE werken met predikaten.

Het IF-statement is als volgt opgebouwd:

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

Het is intuïtief om een ​​TRUE-blok te verwachten na de IF-clausule en een FALSE-blok na de ELSE-clausule, maar u moet zich realiseren dat de ELSE-clausule daadwerkelijk wordt geactiveerd wanneer het predikaat FALSE of UNKNOWN is. Theoretisch zou een logische taal met drie waarden een IF-statement kunnen hebben met een scheiding van de drie gevallen. Zoiets als dit:

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>

En laat zelfs combinaties van logische uitkomsten toe, zodat als je FALSE en UNKNOWN in één sectie wilt combineren, je zoiets als dit zou kunnen gebruiken:

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

Ondertussen kunt u dergelijke constructies emuleren door IF-ELSE-instructies te nesten en expliciet te zoeken naar NULL's in de operanden met de IS NULL-operator.

Het WHILE-statement heeft alleen een TRUE-blok. Het is als volgt ontworpen:

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

Het statement of BEGIN-END-blok dat de body van de lus vormt, wordt geactiveerd terwijl het predikaat TURE is. Zodra het predikaat FALSE of UNKNOWN is, gaat de controle naar het statement dat volgt op de WHILE-lus.

In tegenstelling tot IF en WHILE, die instructies zijn die code uitvoeren, is CASE een expressie die een waarde retourneert. De syntaxis van een gezocht CASE-expressie is als volgt:

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

Een CASE-expressie is ontworpen om de expressie te retourneren die volgt op de THEN-component die overeenkomt met het eerste WHEN-predikaat dat resulteert in TRUE. Als er een ELSE-clausule is, wordt deze geactiveerd als er geen WHEN-predikaat WAAR is (allemaal FALSE of UNKNOWN). Zonder een expliciete ELSE-clausule wordt een impliciete ELSE NULL gebruikt. Als u een ONBEKEND geval apart wilt behandelen, kunt u expliciet zoeken naar NULL's in de operanden van het predikaat met behulp van de IS NULL-operator.

Een eenvoudige CASE-expressie gebruikt impliciete op gelijkheid gebaseerde vergelijkingen tussen de bronexpressie en de vergeleken expressies:

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
zijn

De eenvoudige CASE-expressie is vergelijkbaar met de gezochte CASE-expressie wat betreft de verwerking van de driewaardige logica, maar aangezien de vergelijkingen een impliciete op gelijkheid gebaseerde vergelijking gebruiken, kunt u het UNKNOWN-geval niet afzonderlijk behandelen. Een poging om een ​​NULL te gebruiken in een van de vergeleken expressies in de WHEN-clausules is zinloos omdat de vergelijking niet zal resulteren in TRUE, zelfs niet als de bronexpressie NULL is. Beschouw het volgende voorbeeld:

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

Dit wordt impliciet geconverteerd naar het volgende:

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

Het resultaat is dus:

Invoer is niet NULL

Om een ​​NULL-invoer te detecteren, moet u de gezochte CASE-expressiesyntaxis en de IS NULL-operator gebruiken, zoals:

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

Dit keer is de uitkomst:

Invoer is NULL

SEMMEN

De MERGE-instructie wordt gebruikt om gegevens van een bron samen te voegen tot een doel. U gebruikt een samenvoegpredikaat om de volgende gevallen te identificeren en een actie tegen het doel toe te passen:

  • Een bronrij komt overeen met een doelrij (geactiveerd wanneer een overeenkomst wordt gevonden voor de bronrij waar het samenvoegpredicaat TRUE is):pas UPDATE of DELETE toe op doel
  • Een bronrij komt niet overeen met een doelrij (geactiveerd wanneer er geen overeenkomsten zijn gevonden voor de bronrij waar het samenvoegpredicaat TRUE is, in plaats van voor alle predikaten FALSE of UNKNOWN):pas een INSERT toe tegen doel
  • Een doelrij komt niet overeen met een bronrij (geactiveerd wanneer er geen overeenkomsten zijn gevonden voor de doelrij waar het samenvoegpredicaat TRUE is, in plaats van voor alle predikaten FALSE of UNKNOWN):pas UPDATE of DELETE toe op doel
  • li>

Alle drie de scenario's scheiden WAAR voor de ene groep en ONJUIST of ONBEKEND voor de andere. U krijgt geen aparte secties voor het afhandelen van TRUE, het afhandelen van FALSE en het afhandelen van ONBEKENDE zaken.

Om dit te demonstreren, gebruik ik een tabel met de naam T3 die u maakt en vult door de volgende code uit te voeren:

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

Overweeg de volgende MERGE-instructie:

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;

De bronrij waar col1 1 is, komt overeen met de doelrij waar col1 1 is (predikaat is TRUE) en daarom is de col2 van de doelrij ingesteld op 100.

De bronrij waar col1 3 is, komt niet overeen met een doelrij (allemaal is het predikaat FALSE of UNKNOWN) en daarom wordt een nieuwe rij ingevoegd in T3 met 3 als de col1-waarde en 300 als de col2-waarde.

De doelrijen waar col1 2 is en waar col1 NULL is, komen niet overeen met een bronrij (voor alle rijen is het predikaat FALSE of UNKNOWN) en daarom is col2 in de doelrijen in beide gevallen ingesteld op -1.

De query tegen T3 retourneert de volgende uitvoer na het uitvoeren van de bovenstaande MERGE-instructie:

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

Houd tafel T3 in de buurt; het wordt later gebruikt.

Onderscheid en groepering

In tegenstelling tot vergelijkingen die worden gedaan met behulp van operatoren voor gelijkheid en ongelijkheid, groeperen vergelijkingen die worden gedaan voor onderscheidings- en groeperingsdoeleinden NULL's samen. Een NULL wordt beschouwd als niet verschillend van een andere NULL, maar een NULL wordt beschouwd als verschillend van een niet-NULL-waarde. Als u een DISTINCT-clausule toepast, worden dus dubbele exemplaren van NULL's verwijderd. De volgende zoekopdracht toont dit aan:

SELECT DISTINCT country, region FROM HR.Employees;

Deze query genereert de volgende uitvoer:

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

Er zijn meerdere werknemers met het land USA en de regio NULL, en na het verwijderen van duplicaten toont het resultaat slechts één exemplaar van de combinatie.

Net als onderscheid, groepeert groeperen ook NULL's, zoals de volgende query laat zien:

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

Deze query genereert de volgende uitvoer:

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

Nogmaals, alle vier de werknemers met het land UK en regio NULL werden gegroepeerd.

Bestellen

Bestellen behandelt meerdere NULL's als dezelfde bestelwaarde. De SQL-standaard laat het aan de implementatie over om te kiezen of NULL's als eerste of als laatste worden besteld in vergelijking met niet-NULL-waarden. Microsoft heeft ervoor gekozen om NULL's te beschouwen als lagere bestelwaarden in vergelijking met niet-NULL's in SQL Server, dus bij gebruik van oplopende volgorde, bestelt T-SQL eerst NULL's. De volgende zoekopdracht toont dit aan:

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

Deze query genereert de volgende uitvoer:

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

Volgende maand zal ik meer over dit onderwerp toevoegen, waarbij ik standaardelementen bespreek die je controle geven over het NULL-bestelgedrag en de tijdelijke oplossingen voor die elementen in T-SQL.

Uniek

Bij het afdwingen van uniciteit op een NULL-kolom met behulp van een UNIQUE-beperking of een unieke index, behandelt T-SQL NULL's net als niet-NULL-waarden. Het verwerpt dubbele NULL's alsof een NULL niet uniek is van een andere NULL.

Bedenk dat onze tabel T3 een UNIEKE beperking heeft die is gedefinieerd op col1. Hier is de definitie:

CONSTRAINT UNQ_T3 UNIQUE(col1)

Vraag T3 om de huidige inhoud te zien:

SELECT * FROM dbo.T3;

Als u alle wijzigingen tegen T3 uit de eerdere voorbeelden in dit artikel hebt uitgevoerd, zou u de volgende uitvoer moeten krijgen:

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

Poging om een ​​tweede rij toe te voegen met een NULL in col1:

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

U krijgt de volgende foutmelding:

Msg 2627, Level 14, State 1, Line 558
Overtreding van UNIQUE KEY-beperking 'UNQ_T3'. Kan geen dubbele sleutel invoegen in object 'dbo.T3'. De dubbele sleutelwaarde is ().

Dit gedrag is eigenlijk niet standaard. Volgende maand beschrijf ik de standaardspecificatie en hoe deze te emuleren in T-SQL.

Conclusie

In dit tweede deel van de serie over NULL-complexiteiten heb ik me gericht op inconsistenties in de NULL-behandeling tussen verschillende T-SQL-elementen. Ik behandelde lineaire versus geaggregeerde berekeningen, filter- en matching-clausules, de CHECK-beperking versus de CHECK-optie, IF-, WHILE- en CASE-elementen, de MERGE-instructie, onderscheidbaarheid en groepering, ordening en uniciteit. De inconsistenties die ik heb behandeld, benadrukken verder hoe belangrijk het is om de behandeling van NULL's in het platform dat u gebruikt correct te begrijpen, om ervoor te zorgen dat u correcte en robuuste code schrijft. Volgende maand zal ik de serie voortzetten door de SQL-standaard NULL-behandelingsopties te behandelen die niet beschikbaar zijn in T-SQL, en tijdelijke oplossingen te bieden die worden ondersteund in T-SQL.


  1. SQLite JSON_TREE()

  2. Hoe IPv6 van binair te converteren voor opslag in MySQL

  3. Hoe u de ANSI_NULLS-instelling van uw sessie in SQL Server kunt controleren?

  4. Hoe Asin() werkt in PostgreSQL