Często czytam, kiedy trzeba sprawdzić istnienie wiersza, zawsze powinno się to odbywać za pomocą ISTNIENIA zamiast LICZENIA.
Bardzo rzadko zdarza się, aby wszystko było zawsze prawdziwe, szczególnie jeśli chodzi o bazy danych. Istnieje wiele sposobów wyrażania tego samego semantycznego w SQL. Jeśli istnieje użyteczna praktyczna zasada, może być pisanie zapytań przy użyciu najbardziej naturalnej dostępnej składni (i tak, to jest subiektywne) i rozważanie przepisania tylko wtedy, gdy otrzymany plan zapytań lub wydajność jest niedopuszczalna.
Jeśli chodzi o to, co warto, moje własne podejście do problemu polega na tym, że zapytania o istnienie są najbardziej naturalnie wyrażane za pomocą EXISTS
. Z moich doświadczeń wynika, że EXISTS
lepiej optymalizować niż alternatywa OUTER JOIN
odrzucania NULL
. Używanie COUNT(*)
i filtrowanie =0
to kolejna alternatywa, która ma pewne wsparcie w optymalizatorze zapytań SQL Server, ale osobiście uważam, że jest to niewiarygodne w bardziej złożonych zapytaniach. W każdym razie EXISTS
wydaje mi się to bardziej naturalne (dla mnie) niż jedna z tych alternatyw.
Zastanawiałem się, czy istnieje jakaś nieznana wada z ISTNIENIAMI, która doskonale nadała sens wykonanym pomiarom
Twój konkretny przykład jest interesujący, ponieważ podkreśla sposób, w jaki optymalizator radzi sobie z podzapytaniami w CASE
wyrażeniach (a EXISTS
zwłaszcza w testach).
Podkwerendy w wyrażeniach CASE
Rozważ następujące (całkowicie legalne) zapytanie:
DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);
SELECT
CASE
WHEN (SELECT W.b FROM @When AS W) = 1
THEN (SELECT T.c FROM @Then AS T)
ELSE (SELECT E.d FROM @Else AS E)
END
FROM @Base AS B;
W semantykaCASE
to, że WHEN/ELSE
klauzule są na ogół oceniana w celu tekstowej. W powyższym zapytaniu zwracanie błędu przez SQL Server byłoby błędne, jeśli ELSE
podzapytanie zwróciło więcej niż jeden wiersz, jeśli WHEN
klauzula była spełniona. Aby uszanować tę semantykę, optymalizator tworzy plan wykorzystujący predykaty przekazywania:
Wewnętrzna strona zagnieżdżonych połączeń pętli jest oceniana tylko wtedy, gdy predykat tranzytu zwraca wartość false. Ogólnym efektem jest to, że CASE
wyrażenia są testowane w kolejności, a podzapytania są oceniane tylko wtedy, gdy poprzednie wyrażenie nie zostało spełnione.
Wyrażenia CASE z podzapytaniem EXISTS
W przypadku zastosowania CASE
podzapytania EXISTS
logiczny test istnienia jest implementowany jako połączenie częściowe, ale wiersze, które normalnie byłyby odrzucane przez połączenie częściowe, muszą zostać zachowane na wypadek, gdyby potrzebowała ich późniejsza klauzula. Rzędy przepływające przez ten szczególny rodzaj złączenia łączącego zdobywają flagę wskazującą, czy łączenie łączące znalazło dopasowanie, czy nie. Ta flaga jest znana jako kolumna sondy .
Szczegóły implementacji polegają na tym, że podkwerenda logiczna zostaje zastąpiona połączeniem skorelowanym („zastosuj”) z kolumną sondy. Praca jest wykonywana przez regułę uproszczenia w optymalizatorze zapytań o nazwie RemoveSubqInPrj
(usuń podzapytanie w projekcji). Możemy zobaczyć szczegóły za pomocą flagi śledzenia 8606:
SELECT
T1.ID,
CASE
WHEN EXISTS
(
SELECT 1
FROM #T2 AS T2
WHERE T2.ID = T1.ID
) THEN 1
ELSE 0
END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);
Część drzewa wprowadzania pokazująca EXISTS
test pokazano poniżej:
ScaOp_Exists
LogOp_Project
LogOp_Select
LogOp_Get TBL: #T2
ScaOp_Comp x_cmpEq
ScaOp_Identifier [T2].ID
ScaOp_Identifier [T1].ID
Przekształca RemoveSubqInPrj
się to w strukturę kierowaną przez:
LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)
Jest to lewe połączenie częściowe z sondą opisaną wcześniej. Ta początkowa transformacja jest jedyną dostępną do tej pory w optymalizatorach zapytań SQL Server, a kompilacja zakończy się niepowodzeniem, jeśli ta transformacja zostanie wyłączona.
Jednym z możliwych kształtów planu wykonania dla tego zapytania jest bezpośrednia implementacja tej logicznej struktury:
Końcowy skalar obliczeniowy ocenia wynik CASE
wyrażenia przy użyciu wartości kolumny sondy:
Podstawowy kształt drzewa planu zostaje zachowany, gdy optymalizacja uwzględnia inne typy połączeń fizycznych dla połączenia częściowego. Tylko łączenie przez scalenie obsługuje kolumnę sondy, więc łączenie pół-skrótowe, choć logicznie możliwe, nie jest brane pod uwagę:
Zwróć uwagę, że dane wyjściowe scalania zawierają wyrażenie oznaczone Expr1008
(że nazwa jest taka sama jak poprzednio, to przypadek), chociaż żadna definicja tego planu nie pojawia się na żadnym operatorze. To znowu tylko kolumna sondy. Tak jak poprzednio, końcowy skalar obliczeniowy używa tej wartości sondy do oceny CASE
.
Problem polega na tym, że optymalizator nie w pełni odkrywa alternatywy, które stają się opłacalne tylko przy łączeniu scalającym (lub mieszaniu). W planie zagnieżdżonych pętli nie ma żadnej korzyści ze sprawdzania, czy wiersze w wierszu T2
pasują do zakresu na każdej iteracji. W przypadku planu scalania lub mieszania może to być przydatna optymalizacja.
Jeśli dodamy BETWEEN
do T2
zapytania pasujący predykat , wszystko co się stanie, to sprawdzenie zostanie wykonane dla każdego wiersza jako reszta na łączeniu częściowym scalania (trudne do wykrycia w planie wykonania, ale tam jest):
SELECT
T1.ID,
CASE
WHEN EXISTS
(
SELECT 1
FROM #T2 AS T2
WHERE T2.ID = T1.ID
AND T2.ID BETWEEN 5000 AND 7000 -- New
) THEN 1
ELSE 0
END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;
Mamy nadzieję, że BETWEEN
orzeczenie zostanie zamiast tego sprowadzone do T2
poszukiwania. Zwykle optymalizator rozważałby to (nawet bez dodatkowego predykatu w zapytaniu). Rozpoznaje implikowane predykaty ( BETWEEN
on T1
i predykat łączenia między T1
i T2
razem implikują BETWEEN
on T2
) bez ich obecności w oryginalnym tekście zapytania. Niestety, wzór zastosowanej sondy oznacza, że nie jest to badane.
Istnieją sposoby na napisanie zapytania w celu utworzenia poszukiwań dla obu danych wejściowych do połączenia półspołączenia. Jednym ze sposobów jest napisanie zapytania w dość nienaturalny sposób (pokonanie powodu, który ogólnie wolę EXISTS
):
WITH T2 AS
(
SELECT TOP (9223372036854775807) *
FROM #T2 AS T2
WHERE ID BETWEEN 5000 AND 7000
)
SELECT
T1.ID,
DoesExist =
CASE
WHEN EXISTS
(
SELECT * FROM T2
WHERE T2.ID = T1.ID
) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;
Nie byłbym szczęśliwy, pisząc to zapytanie w środowisku produkcyjnym, aby pokazać, że pożądany kształt planu jest możliwy. Jeśli prawdziwe zapytanie, które musisz napisać, korzysta CASE
w ten właśnie sposób, a wydajność spada, ponieważ po stronie próbnej połączenia częściowego łączenia nie występuje poszukiwanie, możesz rozważyć napisanie zapytania przy użyciu innej składni, która daje prawidłowe wyniki i bardziej wydajny plan realizacji.