CASE
udtryk er en af mine yndlingskonstruktioner i T-SQL. Det er ret fleksibelt og er nogle gange den eneste måde at kontrollere den rækkefølge, som SQL Server vil evaluere prædikater i.
Det bliver dog ofte misforstået.
Hvad er T-SQL CASE-udtrykket?
I T-SQL, CASE
er et udtryk, der evaluerer et eller flere mulige udtryk og returnerer det første passende udtryk. Udtrykket udtryk kan være lidt overbelastet her, men grundlæggende er det alt, der kan evalueres som en enkelt skalarværdi, såsom en variabel, en kolonne, en streng-literal eller endda output fra en indbygget eller skalar funktion .
Der er to former for CASE i T-SQL:
- Simpelt CASE-udtryk – når du kun skal evaluere lighed:
CASE NÅR
SÅ … [ELSE ] END - Søgt CASE-udtryk – når du har brug for at evaluere mere komplekse udtryk, såsom ulighed, LIKE eller IS NOT NULL:
CASE WHEN
THEN … [ELSE ] END
Returudtrykket er altid en enkelt værdi, og outputdatatypen bestemmes af datatypeforrang.
Som sagt bliver CASE-udtrykket ofte misforstået; her er nogle eksempler:
CASE er et udtryk, ikke et udsagn
Sandsynligvis ikke vigtigt for de fleste mennesker, og måske er dette bare min pedantiske side, men mange mennesker kalder det en CASE
erklæring – inklusive Microsoft, hvis dokumentation bruger erklæring og udtryk i flæng til tider. Jeg synes, det er en smule irriterende (som række/optag og kolonne/felt ) og selvom det for det meste er semantik, men der er en vigtig skelnen mellem et udtryk og et udsagn:et udtryk returnerer et resultat. Når folk tænker på CASE
som en erklæring , fører det til eksperimenter med kodeforkortning som denne:
SELECT CASE [status] WHEN 'A' THEN StatusLabel = 'Authorized', LastEvent = AuthorizedTime WHEN 'C' THEN StatusLabel = 'Completed', LastEvent = CompletedTime END FROM dbo.some_table;
Eller denne:
SELECT CASE WHEN @foo = 1 THEN (SELECT foo, bar FROM dbo.fizzbuzz) ELSE (SELECT blat, mort FROM dbo.splunge) END;
Denne type kontrol-af-flow-logik kan være mulig med CASE
erklæringer på andre sprog (som VBScript), men ikke i Transact-SQL's CASE
udtryk . For at bruge CASE
inden for den samme forespørgselslogik skal du bruge en CASE
udtryk for hver outputkolonne:
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 kortslutter ikke altid
Den officielle dokumentation antydede engang, at hele udtrykket vil kortslutte, hvilket betyder, at det vil evaluere udtrykket fra venstre mod højre og stoppe med at evaluere, når det rammer et match:
CASE-sætningen [sic!] evaluerer dens betingelser sekventielt og stopper med den første betingelse, hvis betingelse er opfyldt.Dette er dog ikke altid sandt. Og til sin kredit, i en mere aktuel version, fortsatte siden med at forsøge at forklare et scenarie, hvor dette ikke er garanteret. Men det får kun en del af historien:
I nogle situationer evalueres et udtryk, før en CASE-sætning [sic!] modtager resultaterne af udtrykket som input. Fejl ved evaluering af disse udtryk er mulige. Aggregerede udtryk, der optræder i WHEN-argumenter til en CASE-sætning [sic!] evalueres først, derefter leveres til CASE-sætningen [sic!]. For eksempel producerer følgende forespørgsel en divider med nul-fejl, når værdien af MAX-aggregatet produceres. Dette sker før evaluering af CASE-udtrykket.Divider med nul-eksemplet er ret nemt at gengive, og jeg demonstrerede det i dette svar 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 1Divider med nul fejl fundet.
Der er trivielle løsninger (såsom ELSE (SELECT MIN(1/0)) END
), men dette kommer som en virkelig overraskelse for mange, der ikke har lært ovenstående sætninger udenad fra Books Online. Jeg blev først gjort opmærksom på dette specifikke scenarie i en samtale på en privat e-mail distributionsliste af Itzik Ben-Gan (@ItzikBenGan), som til gengæld oprindeligt blev underrettet af Jaime Lafargue. Jeg rapporterede fejlen i Connect #690017:CASE / COALESCE vil ikke altid evaluere i tekstmæssig rækkefølge; det blev hurtigt lukket som "By Design." Paul White (blog | @SQL_Kiwi) indgav efterfølgende Connect #691535 :Aggregates Don't Follow the Semantics Of CASE, og det blev lukket som "Fixed." Rettelsen, i dette tilfælde, var en afklaring i Books Online-artiklen; nemlig uddraget, jeg kopierede ovenfor.
Denne adfærd kan også give sig selv i nogle andre, mindre indlysende scenarier. For eksempel, Connect #780132 :FREETEXT() overholder ikke evalueringsrækkefølgen i CASE-udsagn (ingen aggregater involveret) viser, at, ja, CASE
Evalueringsrækkefølgen er heller ikke garanteret fra venstre mod højre ved brug af visse fuldtekstfunktioner. På det punkt kommenterede Paul White, at han også observerede noget lignende ved hjælp af den nye LAG()
funktion introduceret i SQL Server 2012. Jeg har ikke en repro handy, men jeg tror på ham, og jeg tror ikke, vi har afdækket alle de kanttilfælde, hvor dette kan forekomme.
Så når aggregater eller ikke-indfødte tjenester som f.eks. fuldtekstsøgning er involveret, skal du ikke gøre nogen antagelser om kortslutning i en CASE
udtryk.
RAND() kan evalueres mere end én gang
Jeg ser ofte folk skrive en simpel CASE
udtryk som dette:
SELECT CASE @variable WHEN 1 THEN 'foo' WHEN 2 THEN 'bar' END
Det er vigtigt at forstå, at dette vil blive udført som en søgt CASE
udtryk som dette:
SELECT CASE WHEN @variable = 1 THEN 'foo' WHEN @variable = 2 THEN 'bar' END
Grunden til, at det er vigtigt at forstå, at det udtryk, der evalueres, vil blive evalueret flere gange, er fordi det faktisk kan evalueres flere gange. Når dette er en variabel, en konstant eller en kolonnereference, er det usandsynligt, at dette er et reelt problem; men tingene kan ændre sig hurtigt, når det er en ikke-deterministisk funktion. Overvej, at dette udtryk giver en SMALLINT
mellem 1 og 3; gå videre og kør det mange gange, og du vil altid få en af disse tre værdier:
SELECT CONVERT(SMALLINT, 1+RAND()*3);
Indsæt nu dette i en simpel CASE
udtryk, og kør det et dusin gange – til sidst vil du få et resultat af NULL
:
SELECT [result] = CASE CONVERT(SMALLINT, 1+RAND()*3) WHEN 1 THEN 'one' WHEN 2 THEN 'two' WHEN 3 THEN 'three' END;
Hvordan sker det? Nå, hele CASE
udtryk udvides til et søgt udtryk som følger:
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;
Til gengæld er det, der sker, at hver HVORNÅR
klausul evaluerer og påkalder RAND()
uafhængigt – og i hvert enkelt tilfælde kan det give en anden værdi. Lad os sige, at vi indtaster udtrykket, og vi tjekker den første HVORNÅR
klausul, og resultatet er 3; vi springer den klausul over og går videre. Det er tænkeligt, at de næste to sætninger begge vil returnere 1, når RAND()
evalueres igen – i hvilket tilfælde ingen af betingelserne vurderes til sande, så ELSE
tager over.
Andre udtryk kan evalueres mere end én gang
Dette problem er ikke begrænset til RAND()
fungere. Forestil dig den samme stil af ikke-determinisme, der kommer fra disse bevægelige mål:
SELECT [crypt_gen] = 1+ABS(CRYPT_GEN_RANDOM(10) % 20), [newid] = LEFT(NEWID(),2), [checksum] = ABS(CHECKSUM(NEWID())%3);
Disse udtryk kan naturligvis give en anden værdi, hvis de evalueres flere gange. Og med en søgt CASE
udtryk, vil der være tidspunkter, hvor hver re-evaluering falder ud af søgningen, der er specifik for den aktuelle HVORNÅR
, og til sidst ramte ELSE
klausul. For at beskytte dig selv mod dette, er en mulighed altid at hårdkode din egen eksplicitte ELSE
; Bare vær forsigtig med den reserveværdi, du vælger at returnere, for dette vil have en skæv effekt, hvis du leder efter jævn fordeling. En anden mulighed er blot at ændre den sidste HVORNÅR
klausul til ELSE
, men dette vil stadig føre til ujævn fordeling. Den foretrukne mulighed er efter min mening at prøve at tvinge SQL Server til at evaluere tilstanden én gang (selvom dette ikke altid er muligt inden for en enkelt forespørgsel). Sammenlign f.eks. disse to resultater:
-- 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ærdi | Forespørgsel A | Forespørgsel B | Forespørgsel C | Forespørgsel 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 |
Fordeling af værdier med forskellige forespørgselsteknikker
I dette tilfælde stoler jeg på, at SQL Server valgte at evaluere udtrykket i underforespørgslen og ikke introducere det til den søgte CASE
udtryk, men dette er blot for at demonstrere, at fordelingen kan tvinges til at være mere jævn. I virkeligheden er dette måske ikke altid det valg, optimizeren træffer, så vær sød ikke at lære af dette lille trick. :-)
CHOOSE() er også påvirket
Du vil bemærke, at hvis du erstatter CHECKSUM(NEWID())
udtryk med RAND()
udtryk, får du helt andre resultater; mest bemærkelsesværdigt vil sidstnævnte kun nogensinde returnere én værdi. Dette er fordi RAND()
, som GETDATE()
og nogle andre indbyggede funktioner, får særlig behandling som en køretidskonstant og evalueres kun én gang pr. reference for hele rækken. Bemærk, at den stadig kan returnere NULL
ligesom den første forespørgsel i det foregående kodeeksempel.
Dette problem er heller ikke begrænset til CASE
udtryk; du kan se lignende adfærd med andre indbyggede funktioner, der bruger den samme underliggende semantik. For eksempel VÆLG
er blot syntaktisk sukker for en mere omfattende søgt CASE
udtryk, og dette vil også give NULL
lejlighedsvis:
SELECT [choose] = CHOOSE(CONVERT(SMALLINT, 1+RAND()*3),'one','two','three');
IIF()
er en funktion, som jeg forventede ville falde i den samme fælde, men denne funktion er egentlig bare en søgt CASE
udtryk med kun to mulige udfald og ingen ELSE
– så det er svært, uden at bygge ind og introducere andre funktioner, at forestille sig et scenarie, hvor dette kan gå i stykker uventet. Mens det i det simple tilfælde er en anstændig forkortelse for CASE
, er det også svært at gøre noget nyttigt med det, hvis du har brug for mere end to mulige udfald. :-)
COALESCE() er også påvirket
Til sidst bør vi undersøge den COALESCE
kan have lignende problemer. Lad os overveje, at disse udtryk er ækvivalente:
SELECT COALESCE(@variable, 'constant'); SELECT CASE WHEN @variable IS NOT NULL THEN @variable ELSE 'constant' END);
I dette tilfælde @variable
vil blive evalueret to gange (som enhver funktion eller underforespørgsel, som beskrevet i dette Connect-element).
Jeg var virkelig i stand til at få nogle forvirrede blikke, da jeg bragte følgende eksempel op i en nylig forumdiskussion. Lad os sige, at jeg vil udfylde en tabel med en fordeling af værdier fra 1-5, men hver gang en 3 støder på, vil jeg bruge -1 i stedet for. Ikke et scenarie i den virkelige verden, men let at konstruere og følge. En måde at skrive dette udtryk på er:
SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);
(På engelsk, arbejde indefra og ud:konverter resultatet af udtrykket 1+RAND()*5
til en smallint; hvis resultatet af denne konvertering er 3, skal du indstille det til NULL
; hvis resultatet af det er NULL
, indstil den til -1. Du kunne skrive dette med en mere udførlig CASE
udtryk, men kortfattet synes at være konge.)
Hvis du kører det en masse gange, bør du se en række værdier fra 1-5 samt -1. Du vil se nogle forekomster af 3, og du har måske også bemærket, at du af og til ser NULL
, selvom du måske ikke forventer nogen af disse resultater. Lad os tjekke fordelingen:
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;
Resultater (dine resultater vil helt sikkert variere, men den grundlæggende tendens bør være ens):
TheNumber | forekomster |
---|---|
NULL | 1.654 |
-1 | 2.002 |
1 | 1.290 |
2 | 1.266 |
3 | 1.287 |
4 | 1.251 |
5 | 1.250 |
Distribution af TheNumber ved hjælp af COALESCE
Nedbrydning af et søgt CASE-udtryk
Kløer du dig i hovedet endnu? Hvordan fungerer værdierne NULL
og 3 vises, og hvorfor er fordelingen for NULL
og -1 væsentligt højere? Nå, jeg vil besvare førstnævnte direkte og indbyde hypoteser til sidstnævnte.
Udtrykket udvides groft til følgende, logisk, da RAND()
evalueres to gange inde i NULLIF
, og gange det derefter med to evalueringer for hver gren af COALESCE
fungere. Jeg har ikke en debugger ved hånden, så dette er ikke nødvendigvis *præcis*, hvad der gøres inde i SQL Server, men det burde være tilsvarende nok til at forklare pointen:
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, at det at blive evalueret flere gange hurtigt kan blive en Vælg dit eget eventyr™-bog, og hvordan både NULL
og 3 er mulige udfald, der ikke synes mulige, når man undersøger den oprindelige erklæring. En interessant sidebemærkning:dette sker ikke helt det samme, hvis du tager ovenstående distributionsscript og erstatter COALESCE
med ISNULL
. I så fald er der ingen mulighed for en NULL
produktion; fordelingen er nogenlunde som følger:
TheNumber | forekomster |
---|---|
-1 | 1.966 |
1 | 1.585 |
2 | 1.644 |
3 | 1.573 |
4 | 1.598 |
5 | 1.634 |
Distribution af TheNumber ved hjælp af ISNULL
Igen, dine faktiske resultater vil helt sikkert variere, men burde ikke være meget. Pointen er, at vi stadig kan se, at 3 falder gennem sprækkerne ret ofte, men ISNULL
på magisk vis eliminerer potentialet for NULL
for at klare det hele vejen igennem.
Jeg talte om nogle af de andre forskelle mellem COALESCE
og ISNULL
i et tip med titlen "Beslutning mellem COALESCE og ISNULL i SQL Server." Da jeg skrev det, gik jeg stærkt ind for at bruge COALESCE
undtagen i det tilfælde, hvor det første argument var en underforespørgsel (igen, på grund af denne fejl "funktionsgab"). Nu er jeg ikke så sikker på, at jeg føler så stærkt over det.
Simple CASE-udtryk kan blive indlejret over sammenkædede servere
En af de få begrænsninger af CASE
udtrykket er, at det er begrænset til 10 redeniveauer. I dette eksempel på dba.stackexchange.com demonstrerer Paul White (ved hjælp af Plan Explorer), at et simpelt udtryk som dette:
SELECT CASE column_name WHEN '1' THEN 'a' WHEN '2' THEN 'b' WHEN '3' THEN 'c' ... END FROM ...
Bliver udvidet af parseren til den søgte form:
SELECT CASE WHEN column_name = '1' THEN 'a' WHEN column_name = '2' THEN 'b' WHEN column_name = '3' THEN 'c' ... END FROM ...
Men kan faktisk overføres via en forbundet serverforbindelse som følgende, meget mere detaljerede forespørgsel:
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 denne situation, selvom den oprindelige forespørgsel kun havde en enkelt CASE
udtryk med 10+ mulige udfald, når det blev sendt til den linkede server, havde det 10+ indlejret CASE
udtryk. Som sådan, som du kunne forvente, returnerede den en fejl:
Erklæring(er) kunne ikke udarbejdes.
Besked 125, niveau 15, tilstand 4
Kasusudtryk må kun indlejres til niveau 10.
I nogle tilfælde kan du omskrive det, som Paul foreslog, med et udtryk som dette (forudsat kolonnenavn
er en varchar-kolonne):
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 nogle tilfælde er det kun SUBSTRING
kan være påkrævet for at ændre det sted, hvor udtrykket evalueres; i andre, kun CONVERT
. Jeg udførte ikke udtømmende test, men dette kan have at gøre med den linkede serverudbyder, muligheder som Collation Compatible og Use Remote Collation og versionen af SQL Server i hver ende af røret.
Lang historie kort, det er vigtigt at huske, at din CASE
udtryk kan omskrives for dig uden varsel, og at enhver løsning, du bruger, senere kan blive tilsidesat af optimeringsværktøjet, selvom det virker for dig nu.
CASE-udtryk endelige tanker og yderligere ressourcer
Jeg håber, at jeg har givet stof til eftertanke om nogle af de mindre kendte aspekter af CASE
udtryk og en vis indsigt i situationer, hvor CASE
– og nogle af de funktioner, der bruger den samme underliggende logik – giver uventede resultater. Nogle andre interessante scenarier, hvor denne type problemer er dukket op:
- Stack Overflow:Hvordan når dette CASE-udtryk ELSE-sætningen?
- Stakoverløb:CRYPT_GEN_RANDOM() mærkelige effekter
- Stakoverløb:CHOOSE() virker ikke efter hensigten
- Stakoverløb:CHECKSUM(NewId()) udføres flere gange pr. række
- Forbind #350485:Fejl med NEWID() og tabeludtryk