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 WHENTHEN … [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 WHENTHEN … [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