CASE
Expression е една от любимите ми конструкции в T-SQL. Той е доста гъвкав и понякога е единственият начин да се контролира реда, в който SQL Server ще оценява предикатите.
Въпреки това често се разбира погрешно.
Какво е T-SQL CASE Expression?
В T-SQL, CASE
е израз, който оценява един или повече възможни изрази и връща първия подходящ израз. Терминът израз може да е малко претоварен тук, но основно това е всичко, което може да бъде оценено като единична скаларна стойност, като променлива, колона, низов литерал или дори изходът на вградена или скаларна функция .
Има две форми на CASE в T-SQL:
- Прост CASE израз – когато трябва само да оцените равенството:
CASE WHEN
THEN … [ELSE ] END - Търсен CASE израз – когато трябва да оцените по-сложни изрази, като неравенство, LIKE или IS NOT NULL:
CASE WHEN
THEN … [ELSE ] ENDкод>
Изразът за връщане винаги е една стойност, а типът на изходните данни се определя от приоритета на типа данни.
Както казах, изразът CASE често се разбира погрешно; ето няколко примера:
CASE е израз, а не израз
Вероятно не е важно за повечето хора и може би това е само моята педантична страна, но много хора го наричат CASE
изявление – включително Microsoft, чиято документация използва изявление и изразяване взаимозаменяеми на моменти. Намирам това за леко досадно (като ред/запис и колона/поле ) и въпреки че е предимно семантика, но има важна разлика между израз и израз:изразът връща резултат. Когато хората мислят за CASE
като изявление , това води до експерименти в съкращаването на кода като това:
SELECT CASE [status] WHEN 'A' THEN StatusLabel ='Authorized', LastEvent =AuthorizedTime WHEN 'C' THEN StatusLabel ='Completed', LastEvent =CompletedTime ENDFROM dbo.some_table;
Или това:
ИЗБЕРЕТЕ СЛУЧАЙ, КОГАТО @foo =1 THEN (ИЗБЕРЕТЕ foo, bar ОТ dbo.fizzbuzz)ELSE (ИЗБЕРЕТЕ блат, mort ОТ dbo.splunge)END;
Този тип логика за управление на потока може да е възможна с CASE
изявления на други езици (като VBScript), но не и в CASE
на Transact-SQL израз . За да използвате CASE
в рамките на същата логика на заявка, ще трябва да използвате CASE
израз за всяка изходна колона:
SELECT StatusLabel =CASE [status] WHEN 'A' THEN 'Authorized' WHEN 'C' THEN 'Completed' END, LastEvent =CASE [status] WHEN 'A' THEN AuthorizedTime WHEN 'C' THEN CompletedTime ENDFROM dbo.some_table;
CASE не винаги ще има късо съединение
Официалната документация веднъж предполагаше, че целият израз ще бъде на късо съединение, което означава, че ще оцени израза отляво надясно и ще спре да оценява, когато удари съвпадение:
Операторът CASE [sic!] оценява своите условия последователно и спира с първото условие, чието условие е изпълнено.Това обаче не винаги е вярно. И за чест, в по-актуална версия, страницата продължи, за да се опита да обясни един сценарий, при който това не е гарантирано. Но това получава само част от историята:
В някои ситуации изразът се оценява преди оператор CASE [sic!] да получи резултатите от израза като свой вход. Възможни са грешки при оценката на тези изрази. Агрегираните изрази, които се появяват в аргументите WHEN към оператор CASE [sic!], се оценяват първо, след което се предоставят на израза CASE [sic!]. Например следната заявка създава грешка при деление на нула при генериране на стойността на MAX агрегата. Това се случва преди оценка на израза CASE.Примерът за деление на нула е доста лесен за възпроизвеждане и го демонстрирах в този отговор на dba.stackexchange.com:
ДЕКЛАРИРАНЕ @i INT =1;ИЗБЕРЕТЕ СЛУЧАЙ, КОГАТО @i =1 СЛЕД 1 ДРУГА МИН(1/0) КРАЙ;
Резултат:
Съобщение 8134, ниво 16, състояние 1Открита е грешка при разделяне на нула.
Има тривиални решения (като ELSE (SELECT MIN(1/0)) END
), но това е истинска изненада за мнозина, които не са запомнили горните изречения от Books Online. За първи път бях уведомен за този специфичен сценарий в разговор в частен списък за разпространение на електронна поща от Ицик Бен-Ган (@ItzikBenGan), който от своя страна първоначално беше уведомен от Хайме Лафарг. Съобщих за грешка в Connect #690017:CASE / COALESCE не винаги ще се оценява в текстов ред; той бързо беше затворен като „По проект“. Пол Уайт (блог | @SQL_Kiwi) впоследствие подаде Connect #691535 :Агрегатите не следват семантиката на CASE и то беше затворено като „фиксирано“. Поправката в този случай беше изясняване в статията на Books Online; а именно фрагмента, който копирах по-горе.
Това поведение може да се отрази и в някои други, по-малко очевидни сценарии. Например, Connect #780132 :FREETEXT() не спазва реда на оценка в операторите CASE (без включени агрегати) показва, че CASE
редът на оценка също не е гарантиран, че ще бъде отляво надясно, когато се използват определени функции за пълен текст. По този елемент Пол Уайт коментира, че също е наблюдавал нещо подобно, използвайки новия LAG()
функция, въведена в SQL Server 2012. Нямам под ръка репродукция, но му вярвам и не мисля, че сме открили всички крайни случаи, в които това може да се случи.
Така че, когато се включват агрегати или услуги, които не са местни като пълнотекстово търсене, моля, не правете никакви предположения за късо съединение в CASE
израз.
RAND() може да се оценява повече от веднъж
Често виждам хора да пишат простичко CASE
израз, като този:
ИЗБЕРЕТЕ CASE @променлива WHEN 1 THEN 'foo' WHEN 2 THEN 'bar'END
Важно е да се разбере, че това ще бъде изпълнено като търсен CASE
израз, като този:
ИЗБЕРЕТЕ СЛУЧАЙ, КОГАТО @variable =1 THEN 'foo' WHEN @variable =2 THEN 'bar'END
Причината, поради която е важно да се разбере, че изразът, който се оценява, ще бъде оценен многократно, е защото всъщност може да бъде оценен много пъти. Когато това е променлива, или константа, или препратка към колона, това е малко вероятно да е истински проблем; обаче нещата могат да се променят бързо, когато е недетерминирана функция. Имайте предвид, че този израз дава SMALLINT
между 1 и 3; продължете напред и го стартирайте много пъти и винаги ще получите една от тези три стойности:
ИЗБЕРЕТЕ CONVERT(SMALLINT, 1+RAND()*3);
Сега поставете това в прост CASE
израз и го стартирайте десетина пъти – в крайна сметка ще получите резултат от NULL
:
SELECT [резултат] =CASE CONVERT(SMALLINT, 1+RAND()*3) WHEN 1 THEN 'един' WHEN 2 THEN 'dwo' WHEN 3 THEN 'three'END;
Как се случва това? Е, целият CASE
изразът се разширява до търсен израз, както следва:
ИЗБЕРЕТЕ [резултат] =СЛУЧАЙ, КОГАТО КОНВЕРТИРАНЕ(SMALLINT, 1+RAND()*3) =1 СЛЕД 'едно', КОГАТО КОНВЕРТИРАНЕ(SMALLINT, 1+RAND()*3) =2 СЛЕД 'два', КОГАТО КОНВЕРТИРАНЕ( SMALLINT, 1+RAND()*3) =3 ТОГАВА 'три' ДРУГО NULL -- това винаги е имплицитно тамEND;
От своя страна това, което се случва е, че всеки WHEN
клаузата оценява и извиква RAND()
независимо – и във всеки случай може да даде различна стойност. Да кажем, че въвеждаме израза и проверяваме първия WHEN
клауза, а резултатът е 3; пропускаме тази клауза и продължаваме напред. Възможно е следващите две клаузи да върнат 1, когато RAND()
се оценява отново – в този случай нито едно от условията не се оценява като истина, така че ELSE
поема.
Други изрази могат да бъдат оценявани повече от веднъж
Този проблем не е ограничен до RAND()
функция. Представете си същия стил на недетерминизъм, идващ от тези движещи се цели:
SELECT [crypt_gen] =1+ABS(CRYPT_GEN_RANDOM(10) % 20), [newid] =LEFT(NEWID(),2), [контролна сума] =ABS(CHECKSUM(NEWID())%3);предварително>Тези изрази очевидно могат да дадат различна стойност, ако се оценяват многократно. И с търсен
CASE
израз, ще има моменти, когато всяка повторна оценка се случва да отпадне от търсенето, специфично за текущияWHEN
, и в крайна сметка натиснетеELSE
клауза. За да се предпазите от това, една от опциите е винаги да кодирате твърдо своя собствен изриченELSE
; просто внимавайте за резервната стойност, която изберете да върнете, защото това ще има някакъв изкривен ефект, ако търсите равномерно разпределение. Друга възможност е просто да промените последнияWHEN
клауза къмELSE
, но това все пак ще доведе до неравномерно разпределение. Предпочитаната опция, според мен, е да се опитате да принудите SQL Server да оцени условието веднъж (въпреки че това не винаги е възможно в рамките на една заявка). Например, сравнете тези два резултата:-- Заявка A:израз, посочен директно в CASE; не ELSE:SELECT x, COUNT(*) FROM( SELECT x =CASE ABS(КОНТРОЛНА СУМА(NEWID())%3) КОГАТО 0 ТОГАВА '0' КОГА 1 ТОГА '1' КОГА 2 ТОГА '2' КРАЙ ОТ sys.all_columns ) КАТО y ГРУПА ПО x; -- Заявка Б:допълнителна клауза ELSE: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) КАТО y GROUP BY x; -- Заявка C:Final WHEN преобразувана в ELSE:SELECT x, COUNT(*) FROM( SELECT x =CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' ELSE '2 ' КРАЙ ОТ sys.all_columns) КАТО y ГРУПА ПО x; -- Заявка D:Изпратете оценката на NEWID() към подзаявка: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(КОНТРОЛНА СУМА(НОВИД())%3) ОТ sys.all_columns ) КАТО x) КАТО y GROUP BY x;Разпределение:
Стойност | Запитване A | Заявка B | Запитване C | Заявка D |
---|---|---|---|---|
NULL | 2 572 | – | – | – |
0 | 2923 | 2900 | 2928 | 2949 |
1 | 1946 | 1959 | 1927 | 2896 |
2 | 1295 | 3877 | 3881 | 2891 |
Разпределение на стойности с различни техники за заявка
В този случай разчитам на факта, че SQL Server е избрал да оцени израза в подзаявката и да не го въвежда в търсения CASE
израз, но това е само за да демонстрира, че разпределението може да бъде принудено да бъде по-равномерно. В действителност това може да не винаги е изборът, който оптимизаторът прави, така че, моля, не се поучете от този малък трик. :-)
CHOOSE() също е засегната
Ще забележите, че ако замените КОНТРОЛНА СУМА(NEWID())
израз с RAND()
израз, ще получите напълно различни резултати; най-вече, последният винаги ще върне само една стойност. Това е така, защото RAND()
, като GETDATE()
и някои други вградени функции, се третира специално като константа по време на изпълнение и се оценява само веднъж на препратка за целия ред. Имайте предвид, че все още може да върне NULL
точно като първата заявка в предходния примерен код.
Този проблем също не е ограничен до CASE
изразяване; можете да видите подобно поведение с други вградени функции, които използват същата основна семантика. Например, ИЗБЕРЕТЕ
е просто синтактична захар за по-сложно търсене CASE
израз и това също ще доведе до NULL
от време на време:
ИЗБЕРЕТЕ [изберете] =ИЗБЕРЕТЕ(КОНВЕРТИРАНЕ(SMALLINT, 1+RAND()*3),'едно','две','три');
IIF()
е функция, която очаквах да попадне в същия капан, но тази функция наистина е просто търсен CASE
израз само с два възможни резултата и без ELSE
– така че е трудно, без влагане и въвеждане на други функции, да си представим сценарий, при който това може да се счупи неочаквано. Докато в простия случай това е прилична стенография за CASE
, също така е трудно да направите нещо полезно с него, ако имате нужда от повече от два възможни резултата. :-)
COALESCE() също е засегнато
И накрая, трябва да разгледаме това COALESCE
може да има подобни проблеми. Нека приемем, че тези изрази са еквивалентни:
SELECT COALESCE(@variable, 'constant'); ИЗБЕРЕТЕ СЛУЧАЙ, КОГАТО @variable НЕ Е NULL ТОГАВА @variable ELSE 'constant' END);
В този случай @variable
ще бъде оценено два пъти (както и всяка функция или подзаявка, както е описано в този елемент за свързване).
Наистина успях да получа някои озадачени погледи, когато посочих следния пример в скорошна дискусия във форума. Да кажем, че искам да попълня таблица с разпределение на стойности от 1-5, но винаги, когато се срещне 3, искам да използвам -1 вместо това. Не е много реален сценарий, но лесен за конструиране и следване. Един от начините да напишете този израз е:
ИЗБЕРЕТЕ КОАЛЕСЦИЯ(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);
(На английски, работейки отвътре навън:преобразувайте резултата от израза 1+RAND()*5
до дребно; ако резултатът от това преобразуване е 3, задайте го на NULL
; ако резултатът от това е NULL
, задайте го на -1. Можете да напишете това с по-подробен CASE
израз, но лаконичен изглежда е крал.)
Ако стартирате това няколко пъти, трябва да видите диапазон от стойности от 1-5, както и -1. Ще видите някои случаи на 3 и може да сте забелязали, че понякога виждате NULL
, въпреки че може да не очаквате нито един от тези резултати. Нека проверим разпределението:
ИЗПОЛЗВАЙТЕ tempdb;GOCREATE TABLE dbo.dist(TheNumber SMALLINT);GOINSERT dbo.dist(TheNumber) SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);GO 10000SELECT TheNumber, събития =COUNT(*) ОТ dbo.dist ГРУПРезултати (резултатите ви със сигурност ще варират, но основната тенденция трябва да е подобна):
TheNumber | появи |
---|---|
NULL | 1654 |
-1 | 2002 |
1 | 1290 |
2 | 1266 |
3 | 1287 |
4 | 1251 |
5 | 1250 |
Разпределение на TheNumber чрез COALESCE
Разбиване на търсен CASE израз
Чешеш ли се вече по главата? Как се правят стойностите NULL
и 3 се показват и защо е разпределението за NULL
и -1 значително по-високо? Е, ще отговоря директно на първото и ще предложа хипотези за второто.
Изразът грубо се разширява до следното, логично, тъй като RAND()
се оценява два пъти в NULLIF
, и след това умножете това по две оценки за всеки клон на COALESCE
функция. Нямам удобен инструмент за отстраняване на грешки, така че това не е непременно *точно* това, което се прави вътре в SQL Server, но трябва да е достатъчно еквивалентен, за да обясни идеята:
ИЗБЕРЕТЕ РЕГИСТРАЦИЯ КОГАТО СЛУЧАЙ КОГАТО КОНВЕРТИРАНЕ(SMALLINT,1+RAND()*5) =3 ТОГАВА NULL ELSE CONVERT(SMALLINT,1+RAND()*5) КРАЙ НЕ Е NULL ТОГАВА СЛУЧАЙ КОГАТО КОНВЕРТИРАНЕ(SMALLINT,1+ RAND()*5) =3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END ELSE -1 ENDEND
Така че можете да видите, че многократната оценка може бързо да се превърне в книга Choose Your Own Adventure™ и как и двете NULL
и 3 са възможните резултати, които не изглеждат възможни при разглеждане на оригиналното твърдение. Интересна странична забележка:това не се случва съвсем същото, ако вземете горния скрипт за разпространение и замените COALESCE
с ISNULL
. В този случай няма възможност за NULL
изход; разпределението е приблизително както следва:
TheNumber | появи |
---|---|
-1 | 1966 |
1 | 1585 |
2 | 1644 |
3 | 1573 |
4 | 1598 |
5 | 1634 |
Разпространение на TheNumber чрез ISNULL
Отново, вашите действителни резултати със сигурност ще варират, но не би трябвало много. Въпросът е, че все още можем да видим, че 3 доста често пада през пукнатините, но ISNULL
магически елиминира потенциала за NULL
за да го направим докрай.
Говорих за някои от другите разлики между COALESCE
и ISNULL
в съвет, озаглавен „Вземане на решение между COALESCE и ISNULL в SQL Server“. Когато написах това, бях силно за използването на COALESCE
освен в случая, когато първият аргумент е подзаявка (отново поради този бъг „пропуск на характеристиките“). Сега не съм толкова сигурен, че се чувствам толкова силно за това.
Простите CASE изрази могат да бъдат вложени върху свързани сървъри
Едно от малкото ограничения на CASE
изразът е, че е ограничен до 10 нива на гнездене. В този пример на dba.stackexchange.com, Пол Уайт демонстрира (с помощта на Plan Explorer), че прост израз като този:
SELECT CASE column_name WHEN '1' THEN 'a' WHEN '2' THEN 'b' WHEN '3' THEN 'c' ...ENDFROM ...
Разгръща се от анализатора до търсената форма:
ИЗБЕРЕТЕ СЛУЧАЙ, КОГАТО column_name ='1' THEN 'a' WHEN column_name ='2' THEN 'b' WHEN column_name ='3' THEN 'c' ...ENDFROM ...
Но всъщност може да се предава през свързана сървърна връзка като следната, много по-подробна заявка:
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 ENDFROM .. .
В тази ситуация, въпреки че оригиналната заявка имаше само един CASE
израз с 10+ възможни резултата, когато е изпратен до свързания сървър, той е имал 10+ вложени CASE
изрази. Като такъв, както може да очаквате, той върна грешка:
Декларация(и) не можа да бъде(а) подготвена.
Съобщение 125, Ниво 15, Състояние 4
Изразите за случаи могат да бъдат вложени само в ниво 10.
В някои случаи можете да го пренапишете, както предложи Пол, с израз като този (приемайки column_name
е колона varchar):
ИЗБЕРЕТЕ CASE CONVERT(VARCHAR(MAX), SUBSTRING(име_на_колона, 1, 255)) WHEN 'a' THEN '1' WHEN 'b' THEN '2' WHEN 'c' THEN '3' ...ENDFROM . ..
В някои случаи само SUBSTRING
може да се изисква да се промени мястото, където се оценява изразът; в други, само CONVERT
. Не извърших изчерпателно тестване, но това може да е свързано с доставчика на свързани сървъри, опции като Съвместима с Collation Compatible и Use Remote Collation и версията на SQL Server в двата края на тръбата.
Накратко, важно е да запомните, че вашият CASE
изразът може да бъде пренаписан вместо вас без предупреждение и че всяко заобиколно решение, което използвате, може по-късно да бъде отменено от оптимизатора, дори ако работи за вас сега.
Заключителни мисли и допълнителни ресурси за CASE Expression
Надявам се, че съм дал малко храна за размисъл върху някои от по-малко известните аспекти на CASE
израз и известна представа за ситуациите, в които CASE
– и някои от функциите, които използват същата основна логика – връщат неочаквани резултати. Някои други интересни сценарии, при които този тип проблем се е появил:
- Stack Overflow :Как този израз CASE достига до клаузата ELSE?
- Препълване на стека:CRYPT_GEN_RANDOM() Странни ефекти
- Препълване на стека:CHOOSE() не работи според предназначението
- Препълване на стека:CHECKSUM(NewId()) се изпълнява няколко пъти на ред
- Свързване #350485 :Грешка с NEWID() и изрази на таблица