Я часто читал, когда нужно проверять наличие строки, всегда нужно делать с EXISTS, а не с COUNT.
Очень редко что-либо всегда может быть правдой, особенно когда речь идет о базах данных. Существует множество способов выразить одну и ту же семантику в SQL. Если есть полезное практическое правило, это может быть для написания запросов с использованием наиболее естественного доступного синтаксиса (и, да, это субъективно), и рассматривать возможность переписывания только в том случае, если план запроса или производительность, которые вы получаете, неприемлемы.
Что бы это ни стоило, мой собственный взгляд на проблему заключается в том, что запросы существования наиболее естественно выражаются с использованием EXISTS. Также мой опыт EXISTS имеет тенденцию оптимизировать лучше, чем OUTER JOINотклонить NULLальтернативу. Использование COUNT(*)и фильтрация =0- это еще одна альтернатива, которая имеет некоторую поддержку в оптимизаторе запросов SQL Server, но я лично обнаружил, что это ненадежно в более сложных запросах. В любом случае, EXISTSпросто кажется намного более естественным (для меня), чем любая из этих альтернатив.
Мне было интересно, есть ли неявный недостаток с EXISTS, который дал бы совершенно смысл измерениям, которые я сделал
Ваш конкретный пример интересен, потому что он показывает, как оптимизатор работает с подзапросами в CASEвыражениях (и, EXISTSв частности, с тестами).
Подзапросы в выражениях CASE
Рассмотрим следующий (совершенно законный) запрос:
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;
В СемантикаCASE том , что WHEN/ELSEоговорки , как правило оценены в текстовом порядке. В приведенном выше запросе было бы неправильно для SQL Server возвращать ошибку, если ELSEподзапрос возвратил более одной строки, если WHENусловие выполнено. Для соблюдения этой семантики оптимизатор создает план, который использует сквозные предикаты:

Внутренняя сторона объединений вложенных циклов оценивается, только когда предикат сквозного соединения возвращает false. Общий эффект состоит в том, что CASEвыражения проверяются по порядку, а подзапросы оцениваются только в том случае, если предыдущее выражение не было удовлетворено.
CASE-выражения с подзапросом EXISTS
Когда используется CASEподзапрос EXISTS, тест логического существования реализуется как полусоединение, но строки, которые обычно отклоняются полусоединением, должны быть сохранены на случай, если они понадобятся более позднему предложению. Строки, проходящие через этот особый тип полусоединения, получают флаг, указывающий, найдено ли полусоединение совпадением или нет. Этот флаг известен как столбец зонда .
Детали реализации состоят в том, что логический подзапрос заменяется коррелированным соединением ('apply') со столбцом исследования. Работа выполняется по правилу упрощения в оптимизаторе запросов RemoveSubqInPrj(удаление подзапроса в проекции). Мы можем увидеть детали, используя флаг трассировки 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);
Часть дерева ввода, отображающая EXISTSтест, показана ниже:
ScaOp_Exists
LogOp_Project
LogOp_Select
LogOp_Get TBL: #T2
ScaOp_Comp x_cmpEq
ScaOp_Identifier [T2].ID
ScaOp_Identifier [T1].ID
Это преобразовывается RemoveSubqInPrjв структуру, возглавляемую:
LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)
Это левое полусоединение с зондом, описанным ранее. Это первоначальное преобразование является единственным доступным в оптимизаторах запросов SQL Server на сегодняшний день, и компиляция просто не удастся, если это преобразование отключено.
Одной из возможных форм плана выполнения для этого запроса является прямая реализация этой логической структуры:

Окончательный вычисляемый скаляр оценивает результат CASEвыражения, используя значение столбца исследования:

Базовая форма дерева плана сохраняется, когда оптимизатор учитывает другие типы физического соединения для полу соединения. Только объединение слиянием поддерживает пробный столбец, поэтому хеш-полусоединение, хотя и логически возможно, не рассматривается:

Обратите внимание, что слияние выводит выражение, помеченное Expr1008(то, что имя совпадает с предыдущим, является совпадением), хотя никакого определения для него нет ни у одного оператора в плане. Это опять столбец зондов. Как и раньше, в окончательном вычисляющем скаляре используется значение этого зонда для оценки CASE.
Проблема заключается в том, что оптимизатор не в полной мере исследует альтернативы, которые становятся полезными только при слиянии (или хэше) полу соединения. В плане вложенных циклов нет смысла проверять, соответствуют ли строки в T2диапазоне на каждой итерации. С планом слияния или хэширования это может быть полезной оптимизацией.
Если мы добавим в запрос совпадающий BETWEENпредикат T2, все, что происходит, - это то, что эта проверка выполняется для каждой строки как остаток при объединении с полусоединением (сложно определить в плане выполнения, но он есть):
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;

Мы надеемся, что BETWEENпредикат вместо этого будет приведен к T2результату поиска. Обычно оптимизатор рассматривает возможность сделать это (даже без дополнительного предиката в запросе). Он распознает подразумеваемые предикаты ( BETWEENon T1и предикат соединения между T1и T2вместе подразумевают BETWEENon T2) без их присутствия в исходном тексте запроса. К сожалению, шаблон apply-probe означает, что это не исследовано.
Есть способы написать запрос для создания поиска на обоих входах в объединение с полусоединением. Один из способов заключается в написании запроса довольно неестественным способом (победив причину, которую я обычно предпочитаю 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;

Я не был бы рад написать этот запрос в производственной среде, просто чтобы продемонстрировать, что желаемая форма плана возможна. Если реальный запрос, который вам нужно написать, использует именно CASEэтот способ, и производительность страдает из-за отсутствия поиска на стороне зонда полусоединения слиянием, вы можете рассмотреть возможность написания запроса с использованием другого синтаксиса, который дает правильные результаты и более эффективный план выполнения.