CASE
expression är en av mina favoritkonstruktioner i T-SQL. Det är ganska flexibelt och är ibland det enda sättet att styra i vilken ordning SQL Server ska utvärdera predikat.
Det missförstås dock ofta.
Vad är T-SQL CASE-uttrycket?
I T-SQL, CASE
är ett uttryck som utvärderar ett eller flera möjliga uttryck och returnerar det första lämpliga uttrycket. Termen uttryck kan vara lite överbelastad här, men i grund och botten är det allt som kan utvärderas som ett enda skalärt värde, till exempel en variabel, en kolumn, en bokstavlig sträng eller till och med utdata från en inbyggd eller skalär funktion .
Det finns två former av CASE i T-SQL:
- Enkelt CASE-uttryck – när du bara behöver utvärdera jämställdhet:
FALL NÄR
DÅ … [ELSE ] END - Sökade CASE-uttryck – när du behöver utvärdera mer komplexa uttryck, som olikhet, LIKE eller IS NOT NULL:
CASE WHEN
THEN … [ELSE ] END
Returuttrycket är alltid ett enskilt värde, och utdatatypen bestäms av datatypsprioritet.
Som sagt, uttrycket CASE missförstås ofta; här är några exempel:
CASE är ett uttryck, inte ett påstående
Förmodligen inte viktigt för de flesta, och kanske är detta bara min pedantiska sida, men många människor kallar det ett CASE
uttalande – inklusive Microsoft, vars dokumentation använder påstående och uttryck ibland omväxlande. Jag tycker att detta är lätt irriterande (som rad/skiva och kolumn/fält ) och även om det mestadels är semantik, men det finns en viktig skillnad mellan ett uttryck och ett påstående:ett uttryck returnerar ett resultat. När folk tänker på CASE
som ett påstående , leder det till experiment med kodförkortning så här:
SELECT CASE [status] WHEN 'A' THEN StatusLabel = 'Authorized', LastEvent = AuthorizedTime WHEN 'C' THEN StatusLabel = 'Completed', LastEvent = CompletedTime END FROM dbo.some_table;
Eller det här:
SELECT CASE WHEN @foo = 1 THEN (SELECT foo, bar FROM dbo.fizzbuzz) ELSE (SELECT blat, mort FROM dbo.splunge) END;
Denna typ av kontroll-av-flödeslogik kan vara möjlig med CASE
påståenden på andra språk (som VBScript), men inte i Transact-SQL:s CASE
uttryck . För att använda CASE
inom samma frågelogik skulle du behöva använda ett CASE
uttryck för varje utdatakolumn:
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 kortsluter inte alltid
Den officiella dokumentationen antydde en gång att hela uttrycket kommer att kortsluta, vilket betyder att det kommer att utvärdera uttrycket från vänster till höger och sluta utvärdera när det träffar en matchning:
CASE-satsen [sic!] utvärderar dess villkor sekventiellt och slutar med det första villkoret vars villkor är uppfyllt.Detta är dock inte alltid sant. Och till dess kredit, i en mer aktuell version, fortsatte sidan med att försöka förklara ett scenario där detta inte är garanterat. Men det får bara en del av historien:
I vissa situationer utvärderas ett uttryck innan en CASE-sats [sic!] tar emot resultatet av uttrycket som dess input. Fel vid utvärdering av dessa uttryck är möjliga. Aggregerade uttryck som förekommer i WHEN-argument till en CASE-sats [sic!] utvärderas först, sedan till CASE-satsen [sic!]. Till exempel producerar följande fråga ett divideringsfel med noll när värdet för MAX-aggregatet produceras. Detta inträffar innan CASE-uttrycket utvärderas.Dela med noll-exemplet är ganska lätt att återskapa, och jag visade det i det här svaret på dba.stackexchange.com:
DECLARE @i INT = 1; SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;
Resultat:
Msg 8134, Level 16, State 1Dela med noll fel påträffat.
Det finns triviala lösningar (som ELSE (SELECT MIN(1/0)) END
), men detta kommer som en riktig överraskning för många som inte har memorerat ovanstående meningar från Books Online. Jag blev först medveten om detta specifika scenario i en konversation på en privat e-postdistributionslista av Itzik Ben-Gan (@ItzikBenGan), som i sin tur först meddelades av Jaime Lafargue. Jag rapporterade felet i Connect #690017:CASE / COALESCE kommer inte alltid att utvärdera i textordning; det stängdes snabbt som "By Design." Paul White (blogg | @SQL_Kiwi) skickade därefter in Connect #691535:Aggregates Don't Follow the Semantics Of CASE, och det stängdes som "Fixed." Fixningen, i det här fallet, var förtydligande i Books Online-artikeln; nämligen utdraget som jag kopierade ovan.
Detta beteende kan ge efter sig i vissa andra, mindre uppenbara scenarier också. Till exempel, Connect #780132 :FREETEXT() respekterar inte utvärderingsordningen i CASE-satser (inga aggregat involverade) visar att, ja, CASE
Utvärderingsordningen är inte heller garanterad från vänster till höger när du använder vissa fulltextfunktioner. På det föremålet kommenterade Paul White att han också observerade något liknande med den nya LAG()
funktion introducerad i SQL Server 2012. Jag har ingen repro till hands, men jag tror på honom, och jag tror inte att vi har grävt fram alla kantfall där detta kan inträffa.
Så när aggregat eller icke-infödda tjänster som fulltextsökning är inblandade, gör inga antaganden om kortslutning i ett CASE
uttryck.
RAND() kan utvärderas mer än en gång
Jag ser ofta folk skriva en enkel CASE
uttryck, så här:
SELECT CASE @variable WHEN 1 THEN 'foo' WHEN 2 THEN 'bar' END
Det är viktigt att förstå att detta kommer att utföras som en sökt CASE
uttryck, så här:
SELECT CASE WHEN @variable = 1 THEN 'foo' WHEN @variable = 2 THEN 'bar' END
Anledningen till att det är viktigt att förstå att uttrycket som utvärderas kommer att utvärderas flera gånger, är för att det faktiskt kan utvärderas flera gånger. När detta är en variabel, en konstant eller en kolumnreferens är det osannolikt att detta är ett verkligt problem; men saker och ting kan förändras snabbt när det är en icke-deterministisk funktion. Tänk på att detta uttryck ger en SMALLINT
mellan 1 och 3; fortsätt och kör det många gånger, så får du alltid ett av dessa tre värden:
SELECT CONVERT(SMALLINT, 1+RAND()*3);
Lägg nu detta i ett enkelt CASE
uttryck och kör det ett dussin gånger – så småningom får du resultatet NULL
:
SELECT [result] = CASE CONVERT(SMALLINT, 1+RAND()*3) WHEN 1 THEN 'one' WHEN 2 THEN 'two' WHEN 3 THEN 'three' END;
Hur går det till? Tja, hela CASE
uttryck utökas till ett sökt uttryck enligt följande:
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;
Det som i sin tur händer är att varje NÄR
klausul utvärderar och anropar RAND()
oberoende – och i varje fall kan det ge ett annat värde. Låt oss säga att vi skriver in uttrycket och vi kontrollerar den första WHEN
klausul, och resultatet är 3; vi hoppar över den klausulen och går vidare. Det är tänkbart att de nästa två satserna båda returnerar 1 när RAND()
utvärderas igen – i vilket fall inget av villkoren utvärderas till sant, så ANSÅ
tar över.
Andra uttryck kan utvärderas mer än en gång
Det här problemet är inte begränsat till RAND()
fungera. Föreställ dig samma stil av icke-determinism som kommer från dessa rörliga mål:
SELECT [crypt_gen] = 1+ABS(CRYPT_GEN_RANDOM(10) % 20), [newid] = LEFT(NEWID(),2), [checksum] = ABS(CHECKSUM(NEWID())%3);
Dessa uttryck kan uppenbarligen ge ett annat värde om de utvärderas flera gånger. Och med ett sökt CASE
uttryck, det kommer att finnas tillfällen då varje omvärdering råkar falla utanför sökningen som är specifik för den aktuella WHEN
, och till slut trycker du på ANSE
klausul. För att skydda dig från detta är ett alternativ att alltid hårdkoda din egen explicita ANSÅ
; var bara försiktig med reservvärdet du väljer att returnera, eftersom detta kommer att få en skev effekt om du letar efter jämn fördelning. Ett annat alternativ är att bara ändra den sista NÄR
klausul till ANSÅ
, men detta kommer fortfarande att leda till ojämn fördelning. Det föredragna alternativet, enligt min mening, är att försöka tvinga SQL Server att utvärdera villkoret en gång (även om detta inte alltid är möjligt inom en enda fråga). Jämför till exempel dessa två resultat:
-- 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;
Distribution:
Värde | Fråga A | Fråga B | Fråga C | Fråga D |
---|---|---|---|---|
NULL | 2 572 | – | – | – |
0 | 2 923 | 2 900 | 2 928 | 2 949 |
1 | 1 946 | 1 959 | 1 927 | 2 896 |
2 | 1 295 | 3 877 | 3 881 | 2 891 |
Fördelning av värden med olika frågetekniker
I det här fallet förlitar jag mig på det faktum att SQL Server valde att utvärdera uttrycket i underfrågan och inte introducera det till det sökta CASE
uttryck, men detta är bara för att visa att fördelningen kan tvingas till att bli jämnare. I verkligheten är det kanske inte alltid det här valet som optimeraren gör, så lär dig inte av det här lilla tricket. :-)
CHOOSE() påverkas också
Du kommer att observera att om du ersätter CHECKSUM(NEWID())
uttryck med RAND()
uttryck får du helt andra resultat; framför allt kommer det senare bara att returnera ett värde. Detta beror på att RAND()
, som GETDATE()
och vissa andra inbyggda funktioner, behandlas särskilt som en körtidskonstant och utvärderas endast en gång per referens för hela raden. Observera att den fortfarande kan returnera NULL
precis som den första frågan i föregående kodexempel.
Det här problemet är inte heller begränsat till CASE
uttryck; du kan se liknande beteende med andra inbyggda funktioner som använder samma underliggande semantik. Till exempel, CHOOSE
är bara syntaktisk socker för ett mer detaljerat sökt CASE
uttryck, och detta kommer också att ge NULL
ibland:
SELECT [choose] = CHOOSE(CONVERT(SMALLINT, 1+RAND()*3),'one','two','three');
IIF()
är en funktion som jag förväntade mig att falla i samma fälla, men den här funktionen är egentligen bara ett sökt CASE
uttryck med endast två möjliga utfall och ingen ANSÅ
– så det är svårt, utan att kapsla och introducera andra funktioner, att föreställa sig ett scenario där detta kan gå sönder oväntat. Medan det i det enkla fallet är en anständig förkortning för CASE
, det är också svårt att göra något användbart med det om du behöver mer än två möjliga resultat. :-)
COALESCE() påverkas också
Slutligen bör vi undersöka den COALESCE
kan ha liknande problem. Låt oss tänka på att dessa uttryck är likvärdiga:
SELECT COALESCE(@variable, 'constant'); SELECT CASE WHEN @variable IS NOT NULL THEN @variable ELSE 'constant' END);
I det här fallet @variable
skulle utvärderas två gånger (liksom vilken funktion eller underfråga som helst, som beskrivs i detta Connect-objekt).
Jag kunde verkligen få några förbryllade blickar när jag tog upp följande exempel i en forumdiskussion nyligen. Låt oss säga att jag vill fylla i en tabell med en fördelning av värden från 1-5, men närhelst en 3:a påträffas vill jag använda -1 istället. Inte ett väldigt verkligt scenario, men lätt att konstruera och följa. Ett sätt att skriva detta uttryck är:
SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);
(På engelska, arbeta inifrån och ut:konvertera resultatet av uttrycket 1+RAND()*5
till en liten pinne; om resultatet av den konverteringen är 3, ställ in den på NULL
; om resultatet av det är NULL
, ställ in den på -1. Du kan skriva detta med ett mer utförligt CASE
uttryck, men kortfattat verkar vara kung.)
Om du kör det ett gäng gånger bör du se ett värdeintervall från 1-5, samt -1. Du kommer att se några instanser av 3, och du kanske också har märkt att du ibland ser NULL
, även om du kanske inte förväntar dig något av dessa resultat. Låt oss kontrollera distributionen:
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;
Resultat (dina resultat kommer säkert att variera, men den grundläggande trenden bör vara liknande):
TheNumber | förekomster |
---|---|
NULL | 1 654 |
-1 | 2 002 |
1 | 1 290 |
2 | 1 266 |
3 | 1 287 |
4 | 1 251 |
5 | 1 250 |
Distribution av TheNumber med COALESCE
Dela upp ett sökt CASE-uttryck
Kliar du dig i huvudet ännu? Hur fungerar värdena NULL
och 3 dyker upp, och varför är distributionen för NULL
och -1 betydligt högre? Tja, jag ska svara på det förra direkt och bjuda in hypoteser för det senare.
Uttrycket expanderar grovt till följande, logiskt, eftersom RAND()
utvärderas två gånger i NULLIF
, och multiplicera sedan det med två utvärderingar för varje gren av COALESCE
fungera. Jag har ingen debugger till hands, så det här är inte nödvändigtvis *exakt* vad som görs inuti SQL Server, men det borde vara tillräckligt likvärdigt för att förklara poängen:
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
Så du kan se att att utvärderas flera gånger snabbt kan bli en Välj ditt eget äventyr™-bok, och hur både NULL
och 3 är möjliga utfall som inte verkar möjliga när man granskar det ursprungliga påståendet. En intressant sidonotering:detta händer inte riktigt på samma sätt om du tar distributionsskriptet ovan och ersätter COALESCE
med ISNULL
. I så fall finns det ingen möjlighet för en NULL
produktion; fördelningen är ungefär som följer:
TheNumber | förekomster |
---|---|
-1 | 1 966 |
1 | 1 585 |
2 | 1 644 |
3 | 1 573 |
4 | 1 598 |
5 | 1 634 |
Distribution av TheNumber med ISNULL
Återigen, dina faktiska resultat kommer säkert att variera, men borde inte göra så mycket. Poängen är att vi fortfarande kan se att 3 faller mellan stolarna ganska ofta, men ISNULL
eliminerar på magiskt sätt potentialen för NULL
för att klara det hela.
Jag pratade om några av de andra skillnaderna mellan COALESCE
och ISNULL
i ett tips, med titeln "Bestämma mellan COALESCE och ISNULL i SQL Server." När jag skrev det var jag starkt för att använda COALESCE
förutom i fallet där det första argumentet var en underfråga (igen, på grund av denna bugg "funktionsgap"). Nu är jag inte så säker på att jag känner lika starkt för det.
Enkla CASE-uttryck kan bli kapslade över länkade servrar
En av de få begränsningarna för CASE
uttrycket är att det är begränsat till 10 bonivåer. I det här exemplet på dba.stackexchange.com visar Paul White (med hjälp av Plan Explorer) att ett enkelt uttryck som detta:
SELECT CASE column_name WHEN '1' THEN 'a' WHEN '2' THEN 'b' WHEN '3' THEN 'c' ... END FROM ...
Utvidgas av tolken till den sökta formen:
SELECT CASE WHEN column_name = '1' THEN 'a' WHEN column_name = '2' THEN 'b' WHEN column_name = '3' THEN 'c' ... END FROM ...
Men kan faktiskt överföras över en länkad serveranslutning som följande, mycket mer utförliga fråga:
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 ...
I den här situationen, även om den ursprungliga frågan bara hade en enda CASE
uttryck med 10+ möjliga utfall, när det skickades till den länkade servern hade det 10+ kapslade CASE
uttryck. Som sådan, som du kan förvänta dig, returnerade den ett fel:
Uttalanden kunde inte förberedas.
Meddelande 125, nivå 15, tillstånd 4
Case-uttryck får endast kapslas till nivå 10.
I vissa fall kan du skriva om det som Paul föreslog, med ett uttryck som detta (förutsatt att kolumnnamn
är en varchar-kolumn):
SELECT CASE CONVERT(VARCHAR(MAX), SUBSTRING(column_name, 1, 255)) WHEN 'a' THEN '1' WHEN 'b' THEN '2' WHEN 'c' THEN '3' ... END FROM ...
I vissa fall, endast SUBSTRING
kan behövas för att ändra platsen där uttrycket utvärderas; i andra, endast CONVERT
. Jag utförde inte uttömmande tester, men detta kan ha att göra med den länkade serverleverantören, alternativ som Collation Compatible och Use Remote Collation och versionen av SQL Server i vardera änden av röret.
Lång historia kort, det är viktigt att komma ihåg att ditt CASE
uttryck kan skrivas om åt dig utan förvarning, och att alla lösningar du använder senare kan åsidosättas av optimeraren, även om det fungerar för dig nu.
CASE Expression Slutliga tankar och ytterligare resurser
Jag hoppas att jag har funderat lite på några av de mindre kända aspekterna av CASE
uttryck och viss insikt i situationer där CASE
– och några av funktionerna som använder samma underliggande logik – ger oväntade resultat. Några andra intressanta scenarier där den här typen av problem har dykt upp:
- Stack Overflow :Hur når detta CASE-uttryck ELSE-satsen?
- Stack Overflow:CRYPT_GEN_RANDOM() Konstiga effekter
- Stack Overflow:CHOOSE() fungerar inte som avsett
- Stackoverflow :CHECKSUM(NewId()) körs flera gånger per rad
- Anslut #350485:Bugg med NEWID() och tabelluttryck