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(
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 454De 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 473De 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> ENDzijn
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 NULLOm 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 NULLSEMMEN
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 558Overtreding 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.