De CASE
expressie is een van mijn favoriete constructies in T-SQL. Het is vrij flexibel en is soms de enige manier om de volgorde te bepalen waarin SQL Server predikaten evalueert.
Het wordt echter vaak verkeerd begrepen.
Wat is de T-SQL CASE-expressie?
In T-SQL, CASE
is een expressie die een of meer mogelijke expressies evalueert en de eerste geschikte expressie retourneert. De term expressie is hier misschien een beetje overbelast, maar in feite is het alles dat kan worden geëvalueerd als een enkele, scalaire waarde, zoals een variabele, een kolom, een letterlijke tekenreeks, of zelfs de uitvoer van een ingebouwde of scalaire functie .
Er zijn twee vormen van CASE in T-SQL:
- Eenvoudige CASE-uitdrukking – wanneer u alleen gelijkheid hoeft te evalueren:
CASE <input> WHEN <eval> THEN <return> … [ELSE <return>] END
- Gezochte CASE-expressie – wanneer u complexere uitdrukkingen moet evalueren, zoals ongelijkheid, LIKE of IS NOT NULL:
CASE WHEN <input_bool> THEN <return> … [ELSE <return>] END
De retourexpressie is altijd een enkele waarde en het uitvoergegevenstype wordt bepaald door de prioriteit van het gegevenstype.
Zoals ik al zei, wordt de uitdrukking CASE vaak verkeerd begrepen; hier zijn enkele voorbeelden:
CASE is een uitdrukking, geen statement
Waarschijnlijk niet belangrijk voor de meeste mensen, en misschien is dit gewoon mijn pedante kant, maar veel mensen noemen het een CASE
verklaring – inclusief Microsoft, wiens documentatie gebruik maakt van statement en expressie af en toe uitwisselbaar. Ik vind dit een beetje vervelend (zoals row/record en kolom/veld ) en hoewel het voornamelijk semantiek is, is er een belangrijk onderscheid tussen een uitdrukking en een instructie:een uitdrukking retourneert een resultaat. Als mensen denken aan CASE
als een statement , leidt dit tot experimenten met het inkorten van code als volgt:
SELECT CASE [status] WHEN 'A' THEN StatusLabel = 'Authorized', LastEvent = AuthorizedTime WHEN 'C' THEN StatusLabel = 'Completed', LastEvent = CompletedTime END FROM dbo.some_table;
Of dit:
SELECT CASE WHEN @foo = 1 THEN (SELECT foo, bar FROM dbo.fizzbuzz) ELSE (SELECT blat, mort FROM dbo.splunge) END;
Dit type control-of-flow-logica is mogelijk met CASE
uitspraken in andere talen (zoals VBScript), maar niet in Transact-SQL's CASE
uitdrukking . Om CASE
te gebruiken binnen dezelfde query-logica zou u een CASE
. moeten gebruiken uitdrukking voor elke uitvoerkolom:
SELECT StatusLabel = CASE [status] WHEN 'A' THEN 'Authorized' WHEN 'C' THEN 'Completed' END, LastEvent = CASE [status] WHEN 'A' THEN AuthorizedTime WHEN 'C' THEN CompletedTime END FROM dbo.some_table;
CASE zal niet altijd kortsluiten
De officiële documentatie suggereerde ooit dat de hele uitdrukking zal kortsluiten, wat betekent dat het de uitdrukking van links naar rechts zal evalueren en stopt met evalueren wanneer er een overeenkomst wordt gevonden:
De CASE-instructie [sic!] evalueert de voorwaarden opeenvolgend en stopt met de eerste voorwaarde waarvan aan de voorwaarde is voldaan.Dit is echter niet altijd waar. En het strekt tot eer, in een recentere versie, probeerde de pagina een scenario uit te leggen waarin dit niet gegarandeerd is. Maar het krijgt maar een deel van het verhaal:
In sommige situaties wordt een uitdrukking geëvalueerd voordat een CASE-instructie [sic!] de resultaten van de uitdrukking als invoer ontvangt. Fouten bij het evalueren van deze uitdrukkingen zijn mogelijk. Geaggregeerde expressies die voorkomen in WHEN-argumenten voor een CASE-instructie [sic!] worden eerst geëvalueerd en vervolgens geleverd aan de CASE-instructie [sic!]. De volgende query produceert bijvoorbeeld een fout door delen door nul bij het produceren van de waarde van het MAX-aggregaat. Dit gebeurt voordat de CASE-expressie wordt geëvalueerd.Het voorbeeld van delen door nul is vrij eenvoudig te reproduceren en ik demonstreerde het in dit antwoord op dba.stackexchange.com:
DECLARE @i INT = 1; SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;
Resultaat:
Msg 8134, Level 16, State 1Delen door nul opgetreden fout.
Er zijn triviale oplossingen (zoals ELSE (SELECT MIN(1/0)) END
), maar dit komt als een echte verrassing voor velen die de bovenstaande zinnen van Books Online niet hebben onthouden. Ik werd voor het eerst op de hoogte gebracht van dit specifieke scenario in een gesprek op een privé-e-maildistributielijst door Itzik Ben-Gan (@ItzikBenGan), die op zijn beurt aanvankelijk werd geïnformeerd door Jaime Lafargue. Ik heb de bug gemeld in Connect #690017:CASE / COALESCE wordt niet altijd in tekstuele volgorde geëvalueerd; het werd snel gesloten als "By Design." Paul White (blog | @SQL_Kiwi) heeft vervolgens Connect #691535 ingediend:Aggregaten volgen de semantiek van CASE niet, en het werd gesloten als "Vast". De oplossing was in dit geval verduidelijking in het Books Online-artikel; namelijk het fragment dat ik hierboven heb gekopieerd.
Dit gedrag kan zich ook in sommige andere, minder voor de hand liggende scenario's voordoen. Connect #780132 :FREETEXT() houdt zich bijvoorbeeld niet aan de volgorde van evaluatie in CASE-statements (er zijn geen aggregaten bij betrokken) laat zien dat, nou ja, CASE
De evaluatievolgorde is ook niet gegarandeerd van links naar rechts bij het gebruik van bepaalde full-text-functies. Over dat item merkte Paul White op dat hij ook iets soortgelijks had waargenomen met behulp van de nieuwe LAG()
functie geïntroduceerd in SQL Server 2012. Ik heb geen repro bij de hand, maar ik geloof hem wel, en ik denk niet dat we alle randgevallen hebben ontdekt waarin dit kan voorkomen.
Dus, als het om aggregaten of niet-native services zoals Full-Text Search gaat, maak dan geen aannames over kortsluiting in een CASE
uitdrukking.
RAND() kan meer dan één keer worden geëvalueerd
Ik zie vaak mensen een eenvoudige . schrijven CASE
uitdrukking, zoals deze:
SELECT CASE @variable WHEN 1 THEN 'foo' WHEN 2 THEN 'bar' END
Het is belangrijk om te begrijpen dat dit zal worden uitgevoerd als een gezocht CASE
uitdrukking, zoals deze:
SELECT CASE WHEN @variable = 1 THEN 'foo' WHEN @variable = 2 THEN 'bar' END
De reden waarom het belangrijk is om te begrijpen dat de uitdrukking die wordt geëvalueerd, meerdere keren zal worden geëvalueerd, is omdat deze daadwerkelijk kan worden geëvalueerd meerdere keren. Als dit een variabele, of een constante, of een kolomverwijzing is, is dit waarschijnlijk geen echt probleem; dingen kunnen echter snel veranderen als het een niet-deterministische functie is. Bedenk dat deze uitdrukking een SMALLINT
. oplevert tussen 1 en 3; ga je gang en voer het vele malen uit, en je krijgt altijd een van die drie waarden:
SELECT CONVERT(SMALLINT, 1+RAND()*3);
Zet dit nu in een eenvoudige CASE
expressie, en voer het een tiental keer uit - uiteindelijk krijg je een resultaat van NULL
:
SELECT [result] = CASE CONVERT(SMALLINT, 1+RAND()*3) WHEN 1 THEN 'one' WHEN 2 THEN 'two' WHEN 3 THEN 'three' END;
Hoe gebeurde dit? Welnu, de hele CASE
uitdrukking wordt als volgt uitgebreid tot een gezochte uitdrukking:
SELECT [result] = CASE WHEN CONVERT(SMALLINT, 1+RAND()*3) = 1 THEN 'one' WHEN CONVERT(SMALLINT, 1+RAND()*3) = 2 THEN 'two' WHEN CONVERT(SMALLINT, 1+RAND()*3) = 3 THEN 'three' ELSE NULL -- this is always implicitly there END;
Wat er op zijn beurt gebeurt, is dat elke WHEN
clausule evalueert en roept RAND()
. op onafhankelijk van elkaar – en in elk geval kan het een andere waarde opleveren. Laten we zeggen dat we de uitdrukking invoeren, en we controleren de eerste WHEN
clausule, en het resultaat is 3; we slaan die clausule over en gaan verder. Het is denkbaar dat de volgende twee clausules beide een 1 retourneren wanneer RAND()
wordt opnieuw geëvalueerd - in welk geval geen van de voorwaarden wordt geëvalueerd als waar, dus de ELSE
neemt het over.
Andere uitdrukkingen kunnen meer dan één keer worden geëvalueerd
Dit probleem is niet beperkt tot de RAND()
functie. Stel je voor dat dezelfde stijl van niet-determinisme voortkomt uit deze bewegende doelen:
SELECT [crypt_gen] = 1+ABS(CRYPT_GEN_RANDOM(10) % 20), [newid] = LEFT(NEWID(),2), [checksum] = ABS(CHECKSUM(NEWID())%3);
Deze uitdrukkingen kunnen uiteraard een andere waarde opleveren als ze meerdere keren worden geëvalueerd. En met een gezochte CASE
uitdrukking, zullen er momenten zijn dat elke herevaluatie uit de zoekopdracht valt die specifiek is voor de huidige WHEN
, en druk uiteindelijk op de ELSE
clausule. Om jezelf hiertegen te beschermen, is een optie om altijd je eigen expliciete ELSE
. te coderen; wees voorzichtig met de fallback-waarde die u wilt retourneren, omdat dit een scheef effect heeft als u op zoek bent naar een gelijkmatige verdeling. Een andere optie is om gewoon de laatste WHEN
. te veranderen clausule naar ELSE
, maar dit zal nog steeds leiden tot ongelijke verdeling. De voorkeursoptie is naar mijn mening om te proberen SQL Server te dwingen de voorwaarde één keer te evalueren (hoewel dit niet altijd mogelijk is binnen een enkele query). Vergelijk bijvoorbeeld deze twee resultaten:
-- Query A: expression referenced directly in CASE; no ELSE: SELECT x, COUNT(*) FROM ( SELECT x = CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' END FROM sys.all_columns ) AS y GROUP BY x; -- Query B: additional ELSE clause: SELECT x, COUNT(*) FROM ( SELECT x = CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' ELSE '2' END FROM sys.all_columns ) AS y GROUP BY x; -- Query C: Final WHEN converted to ELSE: SELECT x, COUNT(*) FROM ( SELECT x = CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' ELSE '2' END FROM sys.all_columns ) AS y GROUP BY x; -- Query D: Push evaluation of NEWID() to subquery: SELECT x, COUNT(*) FROM ( SELECT x = CASE x WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' END FROM ( SELECT x = ABS(CHECKSUM(NEWID())%3) FROM sys.all_columns ) AS x ) AS y GROUP BY x;
Distributie:
Waarde | Query A | Query B | Query C | Query D |
---|---|---|---|---|
NULL | 2.572 | – | – | – |
0 | 2.923 | 2.900 | 2.928 | 2.949 |
1 | 1946 | 1.959 | 1.927 | 2.896 |
2 | 1.295 | 3.877 | 3.881 | 2.891 |
Distributie van waarden met verschillende zoektechnieken
In dit geval vertrouw ik op het feit dat SQL Server ervoor heeft gekozen om de expressie in de subquery te evalueren en niet in te voeren in de gezochte CASE
expressie, maar dit is alleen maar om aan te tonen dat de verdeling kan worden gedwongen om gelijkmatiger te zijn. In werkelijkheid is dit misschien niet altijd de keuze die de optimizer maakt, dus leer alsjeblieft niet van deze kleine truc. :-)
CHOOSE() wordt ook beïnvloed
U zult merken dat als u de CHECKSUM(NEWID())
. vervangt uitdrukking met de RAND()
expressie, je krijgt heel andere resultaten; met name de laatste zal slechts één waarde retourneren. Dit komt omdat RAND()
, zoals GETDATE()
en enkele andere ingebouwde functies, krijgt een speciale behandeling als een runtime-constante en wordt slechts eenmaal per referentie geëvalueerd voor de hele rij. Merk op dat het nog steeds NULL
kan retourneren net als de eerste query in het voorgaande codevoorbeeld.
Dit probleem is ook niet beperkt tot de CASE
uitdrukking; u kunt vergelijkbaar gedrag zien met andere ingebouwde functies die dezelfde onderliggende semantiek gebruiken. Bijvoorbeeld CHOOSE
is slechts syntactische suiker voor een uitgebreider gezocht CASE
expressie, en dit levert ook NULL
. op af en toe:
SELECT [choose] = CHOOSE(CONVERT(SMALLINT, 1+RAND()*3),'one','two','three');
IIF()
is een functie waarvan ik verwachtte dat hij in dezelfde val zou trappen, maar deze functie is eigenlijk gewoon een gezochte CASE
uitdrukking met slechts twee mogelijke uitkomsten, en geen ELSE
– dus het is moeilijk om, zonder nesten en het introduceren van andere functies, een scenario voor te stellen waarin dit onverwachts kan breken. Terwijl het in het eenvoudige geval een behoorlijke afkorting is voor CASE
, is het ook moeilijk om er iets nuttigs mee te doen als je meer dan twee mogelijke uitkomsten nodig hebt. :-)
COALESCE() wordt ook beïnvloed
Ten slotte moeten we onderzoeken dat COALESCE
soortgelijke problemen kunnen hebben. Laten we aannemen dat deze uitdrukkingen equivalent zijn:
SELECT COALESCE(@variable, 'constant'); SELECT CASE WHEN @variable IS NOT NULL THEN @variable ELSE 'constant' END);
In dit geval @variable
zou twee keer worden geëvalueerd (zoals elke functie of subquery, zoals beschreven in dit Connect-item).
Ik kon echt wat verbaasde blikken krijgen toen ik het volgende voorbeeld naar voren bracht in een recente forumdiscussie. Laten we zeggen dat ik een tabel wil vullen met een verdeling van waarden van 1-5, maar wanneer ik een 3 tegenkom, wil ik in plaats daarvan -1 gebruiken. Geen erg realistisch scenario, maar eenvoudig te construeren en te volgen. Een manier om deze uitdrukking te schrijven is:
SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);
(In het Engels, van binnen naar buiten werken:converteer het resultaat van de uitdrukking 1+RAND()*5
tot een kleintje; als het resultaat van die conversie 3 is, stel het dan in op NULL
; als het resultaat daarvan NULL
is , zet deze op -1. Je zou dit kunnen schrijven met een meer uitgebreide CASE
uitdrukking, maar beknopt lijkt de koning te zijn.)
Als je dat een aantal keren uitvoert, zou je een bereik van waarden van 1-5 en -1 moeten zien. U zult enkele gevallen van 3 zien, en het is u misschien ook opgevallen dat u af en toe NULL
. ziet , hoewel u geen van beide resultaten zou verwachten. Laten we eens kijken naar de distributie:
USE tempdb; GO CREATE TABLE dbo.dist(TheNumber SMALLINT); GO INSERT dbo.dist(TheNumber) SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1); GO 10000 SELECT TheNumber, occurences = COUNT(*) FROM dbo.dist GROUP BY TheNumber ORDER BY TheNumber; GO DROP TABLE dbo.dist;
Resultaten (uw resultaten zullen zeker variëren, maar de basistrend zou vergelijkbaar moeten zijn):
TheNumber | voorvallen |
---|---|
NULL | 1.654 |
-1 | 2.002 |
1 | 1290 |
2 | 1.266 |
3 | 1.287 |
4 | 1.251 |
5 | 1250 |
Distributie van TheNumber met COALESCE
Een gezochte CASE-uitdrukking opsplitsen
Ben je je hoofd al aan het krabben? Hoe werken de waarden NULL
en 3 verschijnen, en waarom is de distributie voor NULL
en -1 aanzienlijk hoger? Welnu, ik zal de eerste rechtstreeks beantwoorden en hypothesen voor de laatste uitnodigen.
De uitdrukking wordt ruwweg uitgebreid tot het volgende, logisch, aangezien RAND()
wordt tweemaal geëvalueerd binnen NULLIF
en vermenigvuldig dat vervolgens met twee evaluaties voor elke tak van de COALESCE
functie. Ik heb geen debugger bij de hand, dus dit is niet noodzakelijk *exact* wat er in SQL Server wordt gedaan, maar het zou equivalent genoeg moeten zijn om het punt uit te leggen:
SELECT CASE WHEN CASE WHEN CONVERT(SMALLINT,1+RAND()*5) = 3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END IS NOT NULL THEN CASE WHEN CONVERT(SMALLINT,1+RAND()*5) = 3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END ELSE -1 END END
U kunt dus zien dat meerdere keren evalueren snel een Choose Your Own Adventure™-boek kan worden, en hoe beide NULL
en 3 zijn mogelijke uitkomsten die niet mogelijk lijken bij het onderzoeken van de oorspronkelijke verklaring. Een interessante kanttekening:dit gebeurt niet helemaal hetzelfde als je het bovenstaande distributiescript neemt en COALESCE
vervangt met ISNULL
. In dat geval is er geen mogelijkheid voor een NULL
uitgang; de verdeling is ongeveer als volgt:
TheNumber | voorvallen |
---|---|
-1 | 1.966 |
1 | 1.585 |
2 | 1.644 |
3 | 1.573 |
4 | 1.598 |
5 | 1.634 |
Distributie van TheNumber met ISNULL
Nogmaals, uw werkelijke resultaten zullen zeker variëren, maar niet veel. Het punt is dat we nog steeds kunnen zien dat 3 vrij vaak door de kieren valt, maar ISNULL
elimineert op magische wijze het potentieel voor NULL
om er helemaal doorheen te komen.
Ik heb het gehad over enkele van de andere verschillen tussen COALESCE
en ISNULL
in een tip, getiteld "Beslissen tussen COALESCE en ISNULL in SQL Server." Toen ik dat schreef, was ik een groot voorstander van het gebruik van COALESCE
behalve in het geval dat het eerste argument een subquery was (opnieuw vanwege deze bug "functiehiaat"). Nu weet ik niet zo zeker of ik daar zo sterk over denk.
Eenvoudige CASE-expressies kunnen genest worden over gekoppelde servers
Een van de weinige beperkingen van de CASE
expressie is die beperkt is tot 10 nestniveaus. In dit voorbeeld op dba.stackexchange.com demonstreert Paul White (met behulp van Plan Explorer) dat een eenvoudige uitdrukking als deze:
SELECT CASE column_name WHEN '1' THEN 'a' WHEN '2' THEN 'b' WHEN '3' THEN 'c' ... END FROM ...
Wordt door de parser uitgebreid naar het gezochte formulier:
SELECT CASE WHEN column_name = '1' THEN 'a' WHEN column_name = '2' THEN 'b' WHEN column_name = '3' THEN 'c' ... END FROM ...
Maar kan in feite worden verzonden via een gekoppelde serververbinding als de volgende, veel uitgebreidere vraag:
SELECT CASE WHEN column_name = '1' THEN 'a' ELSE CASE WHEN column_name = '2' THEN 'b' ELSE CASE WHEN column_name = '3' THEN 'c' ELSE ... ELSE NULL END END END FROM ...
In deze situatie, ook al had de oorspronkelijke zoekopdracht maar één CASE
expressie met 10+ mogelijke uitkomsten, wanneer verzonden naar de gekoppelde server, had deze 10+ geneste CASE
uitdrukkingen. Als zodanig heeft het, zoals je zou verwachten, een fout geretourneerd:
Statement(en) konden niet worden voorbereid.
Msg 125, Level 15, State 4
Case-expressies mogen alleen worden genest op level 10.
In sommige gevallen kun je het herschrijven zoals Paul suggereerde, met een uitdrukking als deze (ervan uitgaande dat column_name
is een varchar-kolom):
SELECT CASE CONVERT(VARCHAR(MAX), SUBSTRING(column_name, 1, 255)) WHEN 'a' THEN '1' WHEN 'b' THEN '2' WHEN 'c' THEN '3' ... END FROM ...
In sommige gevallen is alleen de SUBSTRING
kan nodig zijn om de locatie te wijzigen waar de uitdrukking wordt geëvalueerd; in andere, alleen de CONVERT
. Ik heb geen uitgebreide tests uitgevoerd, maar dit kan te maken hebben met de provider van de gekoppelde server, opties zoals Collation Compatible en Use Remote Collation, en de versie van SQL Server aan beide uiteinden van de pijp.
Om een lang verhaal kort te maken, het is belangrijk om te onthouden dat uw CASE
expressie voor u kan worden herschreven zonder waarschuwing, en dat elke tijdelijke oplossing die u gebruikt later kan worden overruled door de optimizer, zelfs als het nu voor u werkt.
CASE Expression Laatste gedachten en aanvullende bronnen
Ik hoop dat ik wat stof tot nadenken heb gegeven over enkele van de minder bekende aspecten van de CASE
uitdrukking, en enig inzicht in situaties waarin CASE
– en sommige functies die dezelfde onderliggende logica gebruiken – leveren onverwachte resultaten op. Enkele andere interessante scenario's waarin dit type probleem zich voordoet:
- Stack Overflow:Hoe bereikt deze CASE-expressie de ELSE-clausule?
- Stack Overflow:CRYPT_GEN_RANDOM() Vreemde effecten
- Stack Overflow:CHOOSE() Werkt niet zoals bedoeld
- Stack Overflow:CHECKSUM(NewId()) wordt meerdere keren per rij uitgevoerd
- Verbind #350485 :Bug met NEWID() en Table Expressions