Database
 sql >> Baza danych >  >> RDS >> Database

Brudne tajemnice wyrażenia CASE

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 1
Wystą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. :-)

Dotyczy to również

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. :-)

Dotyczy to również

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:

Msg 8180, Level 16, State 1
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

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Elastyczne i łatwe w zarządzaniu projekty zestawień materiałowych (BOM)

  2. Analizuj Big Data za pomocą narzędzi Microsoft Azure

  3. Operator SQL AND dla początkujących

  4. Jak używać „Lubię to” w SQL

  5. Zarabiaj pieniądze na niewykorzystanych rzeczach:model danych gospodarki współdzielenia