WSKAZÓWKA
wyrażenie jest jedną z moich ulubionych konstrukcji w T-SQL. Jest dość elastyczny i czasami jest jedynym sposobem kontrolowania kolejności, w jakiej SQL Server będzie oceniać predykaty.
Jednak często jest źle rozumiany.
Co to jest wyrażenie T-SQL CASE?
W T-SQL, CASE
to wyrażenie, które oblicza jedno lub więcej możliwych wyrażeń i zwraca pierwsze odpowiednie wyrażenie. Termin wyrażenie może być tutaj nieco przeładowany, ale w zasadzie jest to wszystko, co można ocenić jako pojedynczą wartość skalarną, taką jak zmienna, kolumna, literał ciągu, a nawet wyjście wbudowanej lub skalarnej funkcji .
Istnieją dwie formy CASE w T-SQL:
- Proste wyrażenie CASE – gdy trzeba tylko ocenić równość:
CASE WHEN
THEN … [ELSE ] END - Przeszukiwane wyrażenie CASE – gdy trzeba ocenić bardziej złożone wyrażenia, takie jak nierówność, LIKE lub IS NOT NULL:
CASE WHEN
THEN … [ELSE ] END
Zwracane wyrażenie jest zawsze pojedynczą wartością, a typ danych wyjściowych jest określany przez pierwszeństwo typu danych.
Jak powiedziałem, wyrażenie CASE jest często źle rozumiane; oto kilka przykładów:
CASE to wyrażenie, a nie instrukcja
Prawdopodobnie nie jest to ważne dla większości ludzi i być może jest to tylko moja pedantyczna strona, ale wiele osób nazywa to CASE
oświadczenie – w tym Microsoft, którego dokumentacja wykorzystuje oświadczenie i wyrażenie czasami zamiennie. Uważam to za trochę irytujące (jak row/record i kolumna/pole ) i chociaż jest to głównie semantyka, ale istnieje ważna różnica między wyrażeniem a instrukcją:wyrażenie zwraca wynik. Kiedy ludzie myślą o CASE
jako oświadczenie , prowadzi to do eksperymentów w skracaniu kodu w ten sposób:
SELECT CASE [status] WHEN 'A' THEN StatusLabel = 'Authorized', LastEvent = AuthorizedTime WHEN 'C' THEN StatusLabel = 'Completed', LastEvent = CompletedTime END FROM dbo.some_table;
Albo to:
SELECT CASE WHEN @foo = 1 THEN (SELECT foo, bar FROM dbo.fizzbuzz) ELSE (SELECT blat, mort FROM dbo.splunge) END;
Ten typ logiki kontroli przepływu może być możliwy dzięki CASE
wypowiedzi w innych językach (takich jak VBScript), ale nie w CASE
Transact-SQL wyrażenie . Aby użyć CASE
w ramach tej samej logiki zapytania musiałbyś użyć CASE
wyrażenie dla każdej kolumny wyjściowej:
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 nie zawsze będzie zwierać
Oficjalna dokumentacja kiedyś sugerowała, że całe wyrażenie ulegnie zwarciu, co oznacza, że oceni wyrażenie od lewej do prawej i przestanie oceniać, gdy trafi na dopasowanie:
Instrukcja CASE [sic!] ocenia swoje warunki sekwencyjnie i zatrzymuje się na pierwszym warunku, którego warunek jest spełniony.Jednak nie zawsze tak jest. I trzeba przyznać, że w bardziej aktualnej wersji strona próbowała wyjaśnić jeden scenariusz, w którym nie jest to gwarantowane. Ale to tylko część historii:
W niektórych sytuacjach wyrażenie jest oceniane, zanim instrukcja CASE [sic!] otrzyma wyniki wyrażenia jako dane wejściowe. Możliwe są błędy w ocenie tych wyrażeń. Wyrażenia agregujące pojawiające się w argumentach WHEN instrukcji CASE [sic!] są najpierw oceniane, a następnie dostarczane do instrukcji CASE [sic!]. Na przykład poniższe zapytanie generuje błąd dzielenia przez zero podczas generowania wartości agregatu MAX. Dzieje się tak przed oceną wyrażenia CASE.Przykład dzielenia przez zero jest dość łatwy do odtworzenia i zademonstrowałem to w tej odpowiedzi na dba.stackexchange.com:
DECLARE @i INT = 1; SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;
Wynik:
Msg 8134, poziom 16, stan 1Wystąpił błąd dzielenia przez zero.
Istnieją trywialne obejścia (takie jak ELSE (SELECT MIN(1/0)) END
), ale jest to prawdziwa niespodzianka dla wielu, którzy nie zapamiętali powyższych zdań z Books Online. Po raz pierwszy dowiedziałem się o tym konkretnym scenariuszu podczas rozmowy na prywatnej liście dystrybucyjnej poczty e-mail przez Itzika Ben-Gana (@ItzikBenGan), którego z kolei początkowo powiadomił Jaime Lafargue. Zgłosiłem błąd w Connect #690017 :CASE / COALESCE nie zawsze będzie oceniane w kolejności tekstowej; został szybko zamknięty jako „Według projektu”. Paul White (blog | @SQL_Kiwi) następnie zgłosił Connect #691535 :Agregaty nie podążają za semantyką CASE i został zamknięty jako „Naprawiony”. W tym przypadku poprawką było wyjaśnienie w artykule Books Online; mianowicie fragment, który skopiowałem powyżej.
Takie zachowanie może się również zmienić w innych, mniej oczywistych scenariuszach. Na przykład Connect #780132 :FREETEXT() nie honoruje kolejności oceny w instrukcjach CASE (bez zaangażowanych agregatów) pokazuje, że, cóż, CASE
Nie ma gwarancji, że kolejność oceny będzie od lewej do prawej podczas korzystania z niektórych funkcji pełnotekstowych. W tej pozycji Paul White skomentował, że zaobserwował również coś podobnego przy użyciu nowej LAG()
funkcja wprowadzona w SQL Server 2012. Nie mam pod ręką repliki, ale wierzę mu i nie sądzę, że odkryliśmy wszystkie skrajne przypadki, w których może się to zdarzyć.
Tak więc, gdy w grę wchodzą agregacje lub usługi inne niż natywne, takie jak wyszukiwanie pełnotekstowe, nie rób żadnych założeń dotyczących zwarcia w CASE
wyrażenie.
RAND() może być oceniany więcej niż raz
Często widzę ludzi piszących proste SPRAWA
wyrażenie, jak to:
SELECT CASE @variable WHEN 1 THEN 'foo' WHEN 2 THEN 'bar' END
Ważne jest, aby zrozumieć, że zostanie to wykonane jako przeszukane SPRAWA
wyrażenie, jak to:
SELECT CASE WHEN @variable = 1 THEN 'foo' WHEN @variable = 2 THEN 'bar' END
Powodem, dla którego ważne jest zrozumienie, że oceniane wyrażenie będzie oceniane wielokrotnie, jest to, że faktycznie może być oceniane wiele razy. Kiedy jest to zmienna, stała lub odwołanie do kolumny, jest mało prawdopodobne, aby był to prawdziwy problem; jednak rzeczy mogą się szybko zmienić, gdy jest to funkcja niedeterministyczna. Weź pod uwagę, że to wyrażenie daje SMALLINT
od 1 do 3; śmiało uruchamiaj go wiele razy, a zawsze otrzymasz jedną z tych trzech wartości:
SELECT CONVERT(SMALLINT, 1+RAND()*3);
Teraz umieść to w prostym CASE
wyrażenie i uruchom go kilkanaście razy – w końcu otrzymasz wynik NULL
:
SELECT [result] = CASE CONVERT(SMALLINT, 1+RAND()*3) WHEN 1 THEN 'one' WHEN 2 THEN 'two' WHEN 3 THEN 'three' END;
Jak to się stało? Cóż, cały CASE
wyrażenie jest rozwijane do szukanego wyrażenia w następujący sposób:
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;
Z kolei dzieje się tak, że każde KIEDY
klauzula ocenia i wywołuje RAND()
niezależnie – i w każdym przypadku może to dać inną wartość. Załóżmy, że wprowadzamy wyrażenie i sprawdzamy pierwsze KIEDY
klauzula, a wynikiem jest 3; pomijamy tę klauzulę i przechodzimy dalej. Można sobie wyobrazić, że następne dwie klauzule obie zwrócą 1, gdy RAND()
jest oceniany ponownie – w takim przypadku żaden z warunków nie jest oceniany jako prawdziwy, więc ELSE
przejmuje kontrolę.
Inne wyrażenia mogą być oceniane więcej niż raz
Ten problem nie ogranicza się do RAND()
funkcjonować. Wyobraź sobie ten sam styl niedeterminizmu pochodzący z tych ruchomych celów:
SELECT [crypt_gen] = 1+ABS(CRYPT_GEN_RANDOM(10) % 20), [newid] = LEFT(NEWID(),2), [checksum] = ABS(CHECKSUM(NEWID())%3);
Wyrażenia te mogą oczywiście dać inną wartość, jeśli zostaną ocenione wielokrotnie. I z wyszukanym CASE
wyrażenie, będą chwile, kiedy każda ponowna ocena wypadnie z wyszukiwania specyficznego dla bieżącego KIEDY
i ostatecznie naciśnij ELSE
klauzula. Aby się przed tym uchronić, jedną z opcji jest stałe zakodowanie własnego, wyraźnego INNEGO
; po prostu uważaj na wartość rezerwową, którą chcesz zwrócić, ponieważ będzie to miało pewien efekt przekrzywienia, jeśli szukasz równomiernego rozkładu. Inną opcją jest zmiana ostatniego KIEDY
klauzula ELSE
, ale nadal będzie to prowadzić do nierównej dystrybucji. Moim zdaniem preferowaną opcją jest próba wymuszenia na SQL Server jednorazowej oceny warunku (chociaż nie zawsze jest to możliwe w ramach pojedynczego zapytania). Na przykład porównaj te dwa wyniki:
-- 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;
Dystrybucja:
Wartość | Zapytanie A | Zapytanie B | Zapytanie C | Zapytanie D |
---|---|---|---|---|
NULL | 2572 | – | – | – |
0 | 2923 | 2,900 | 2928 | 2949 |
1 | 1946 | 1959 | 1927 | 2896 |
2 | 1295 | 3877 | 3881 | 2891 |
Dystrybucja wartości przy użyciu różnych technik zapytań
W tym przypadku polegam na tym, że SQL Server wybrał ocenę wyrażenia w podzapytaniu i nie wprowadzał go do przeszukiwanego CASE
ekspresji, ale to tylko po to, by zademonstrować, że dystrybucję można zmusić do bardziej równomiernego rozmieszczenia. W rzeczywistości nie zawsze jest to wybór dokonywany przez optymalizator, więc proszę nie wyciągać wniosków z tej małej sztuczki. :-)
WYBIERZ()
Zauważysz, że jeśli zastąpisz CHECKSUM(NEWID())
wyrażenie z RAND()
wyrażenie, otrzymasz zupełnie inne wyniki; przede wszystkim ten ostatni zwróci tylko jedną wartość. Dzieje się tak, ponieważ RAND()
, jak GETDATE()
i kilka innych funkcji wbudowanych, jest traktowana w specjalny sposób jako stała czasu działania i jest oceniana tylko raz na odwołanie dla całego rzędu. Zauważ, że nadal może zwrócić NULL
tak jak pierwsze zapytanie w poprzednim przykładzie kodu.
Ten problem nie ogranicza się również do CASE
wyrażenie; możesz zobaczyć podobne zachowanie z innymi wbudowanymi funkcjami, które używają tej samej podstawowej semantyki. Na przykład WYBIERZ
jest tylko cukrem składniowym dla bardziej wyszukanego szukanego CASE
wyrażenie, a to również da NULL
czasami:
SELECT [choose] = CHOOSE(CONVERT(SMALLINT, 1+RAND()*3),'one','two','three');
IIF()
to funkcja, która, jak się spodziewałem, wpadnie w tę samą pułapkę, ale ta funkcja to tak naprawdę tylko przeszukany CASE
wyrażenie z tylko dwoma możliwymi wynikami i bez ELSE
– więc bez zagnieżdżania i wprowadzania innych funkcji trudno jest wyobrazić sobie scenariusz, w którym może się to niespodziewanie zepsuć. Podczas gdy w prostym przypadku jest to przyzwoity skrót dla CASE
, trudno jest też zrobić z nim cokolwiek pożytecznego, jeśli potrzebujesz więcej niż dwóch możliwych wyników. :-)
COALESCE()
Na koniec powinniśmy sprawdzić, że POŁĄCZENIE
może mieć podobne problemy. Załóżmy, że te wyrażenia są równoważne:
SELECT COALESCE(@variable, 'constant'); SELECT CASE WHEN @variable IS NOT NULL THEN @variable ELSE 'constant' END);
W tym przypadku @zmienna
zostaną ocenione dwukrotnie (jak każda funkcja lub podzapytanie, jak opisano w tym elemencie Connect).
Naprawdę udało mi się uzyskać trochę zdziwionych spojrzeń, kiedy przytoczyłem następujący przykład w niedawnej dyskusji na forum. Załóżmy, że chcę wypełnić tabelę rozkładem wartości od 1 do 5, ale za każdym razem, gdy zostanie napotkane 3, chcę zamiast tego użyć -1. Niezbyt realny scenariusz, ale łatwy do skonstruowania i naśladowania. Jednym ze sposobów zapisania tego wyrażenia jest:
SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);
(W języku angielskim, praca od środka:przekonwertuj wynik wyrażenia 1+RAND()*5
do malucha; jeśli wynikiem tej konwersji jest 3, ustaw go na NULL
; jeśli wynikiem tego jest NULL
, ustaw go na -1. Możesz napisać to bardziej szczegółowo CASE
wyrażenie, ale zwięzłość wydaje się być królem.)
Jeśli uruchomisz to kilka razy, powinieneś zobaczyć zakres wartości od 1-5, a także -1. Zobaczysz kilka wystąpień liczby 3 i mogłeś również zauważyć, że czasami widzisz NULL
, choć możesz nie oczekiwać żadnego z tych wyników. Sprawdźmy dystrybucję:
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;
Wyniki (Twoje wyniki z pewnością będą się różnić, ale podstawowy trend powinien być podobny):
Liczba | wydarzenia |
---|---|
NULL | 1654 |
-1 | 2002 |
1 | 1290 |
2 | 1266 |
3 | 1287 |
4 | 1251 |
5 | 1250 |
Dystrybucja Numeru za pomocą COALESCE
Podział wyszukiwanego wyrażenia CASE
Drapiesz się już po głowie? Jak działają wartości NULL
i 3 pojawiają się i dlaczego jest dystrybucja dla NULL
a -1 znacznie wyższy? Cóż, na pierwsze odpowiem bezpośrednio, a na drugie poproszę hipotezy.
Wyrażenie z grubsza rozwija się do następującego, logicznie, ponieważ RAND()
jest oceniany dwukrotnie wewnątrz NULLIF
, a następnie pomnóż to przez dwie oceny dla każdej gałęzi COALESCE
funkcjonować. Nie mam pod ręką debuggera, więc niekoniecznie jest to *dokładnie* to, co jest robione wewnątrz SQL Server, ale powinno być wystarczająco równoważne, aby wyjaśnić tę kwestię:
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
Możesz więc zobaczyć, że wielokrotne ocenianie może szybko stać się książką Wybierz własną przygodę™ i jak zarówno NULL
a 3 to możliwe wyniki, które nie wydają się możliwe podczas badania oryginalnego stwierdzenia. Ciekawa uwaga na marginesie:to nie stanie się tak samo, jeśli weźmiesz powyższy skrypt dystrybucyjny i zastąpisz COALESCE
z ISNULL
. W takim przypadku nie ma możliwości, aby NULL
wyjście; rozkład jest mniej więcej następujący:
Liczba | wydarzenia |
---|---|
-1 | 1966 |
1 | 1585 |
2 | 1644 |
3 | 1573 |
4 | 1598 |
5 | 1634 |
Dystrybucja Numeru za pomocą ISNULL
Ponownie, twoje rzeczywiste wyniki z pewnością będą się różnić, ale nie powinny być zbyt duże. Chodzi o to, że nadal widzimy, że 3 dość często wypada przez pęknięcia, ale ISNULL
magicznie eliminuje potencjał NULL
aby przejść przez całą drogę.
Mówiłem o niektórych innych różnicach między COALESCE
i ISNULL
we wskazówce zatytułowanej „Decydowanie między COALESCE i ISNULL w programie SQL Server”. Kiedy to pisałem, bardzo opowiedziałem się za użyciem COALESCE
z wyjątkiem przypadku, gdy pierwszym argumentem było podzapytanie (ponownie, z powodu tego błądu „przerwa między funkcjami”). Teraz nie jestem pewien, czy tak mocno to odczuwam.
Proste wyrażenia CASE mogą być zagnieżdżane na połączonych serwerach
Jedno z niewielu ograniczeń CASE
wyrażenie jest ograniczone do 10 poziomów zagnieżdżenia. W tym przykładzie na dba.stackexchange.com Paul White demonstruje (za pomocą Eksploratora planu), że proste wyrażenie takie jak to:
SELECT CASE column_name WHEN '1' THEN 'a' WHEN '2' THEN 'b' WHEN '3' THEN 'c' ... END FROM ...
Zostaje rozwinięty przez parser do przeszukiwanego formularza:
SELECT CASE WHEN column_name = '1' THEN 'a' WHEN column_name = '2' THEN 'b' WHEN column_name = '3' THEN 'c' ... END FROM ...
Ale w rzeczywistości mogą być przesyłane przez połączenie z połączonym serwerem jako następujące, znacznie bardziej szczegółowe zapytanie:
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 ...
W tej sytuacji, mimo że oryginalne zapytanie miało tylko jeden CASE
wyrażenie z co najmniej 10 możliwymi wynikami, po wysłaniu do połączonego serwera miało ponad 10 zagnieżdżonych SPRAWA
wyrażenia. W związku z tym, jak można się spodziewać, zwrócił błąd:
Nie można przygotować instrukcji.
Msg 125, Level 15, State 4
Wyrażenia case mogą być zagnieżdżone tylko na poziomie 10.
W niektórych przypadkach możesz przepisać je zgodnie z sugestią Paula, używając wyrażenia takiego jak to (zakładając, że nazwa_kolumny
jest kolumną varcharową):
SELECT CASE CONVERT(VARCHAR(MAX), SUBSTRING(column_name, 1, 255)) WHEN 'a' THEN '1' WHEN 'b' THEN '2' WHEN 'c' THEN '3' ... END FROM ...
W niektórych przypadkach tylko SUBSTRING
może być wymagana zmiana lokalizacji, w której wyrażenie jest oceniane; w innych tylko CONVERT
. Nie przeprowadziłem wyczerpujących testów, ale może to mieć związek z dostawcą serwera połączonego, opcjami takimi jak Kompatybilność z sortowaniem i Użyj zdalnego sortowania oraz wersją SQL Server na obu końcach potoku.
Krótko mówiąc, ważne jest, aby pamiętać, że Twój CASE
wyrażenie może zostać przepisane bez ostrzeżenia, a każde zastosowane obejście może później zostać pominięte przez optymalizator, nawet jeśli zadziała teraz.
Końcowe przemyślenia na temat wyrażenia CASE i dodatkowe zasoby
Mam nadzieję, że dałem trochę do myślenia na temat niektórych mniej znanych aspektów CASE
wyrażenie i trochę wglądu w sytuacje, w których CASE
– a niektóre funkcje korzystające z tej samej logiki podstawowej – zwracają nieoczekiwane wyniki. Kilka innych interesujących scenariuszy, w których pojawił się ten rodzaj problemu:
- Przepełnienie stosu:w jaki sposób to wyrażenie CASE osiąga klauzulę ELSE?
- Przepełnienie stosu:CRYPT_GEN_RANDOM() Dziwne efekty
- Przepełnienie stosu:WYBIERZ() nie działa zgodnie z przeznaczeniem
- Przepełnienie stosu:CHECKSUM(NewId()) wykonuje wiele razy w rzędzie
- Połącz #350485:Błąd z NEWID() i wyrażeniami tabelowymi