Я часто читал, когда нужно проверять наличие строки, всегда нужно делать с 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
результату поиска. Обычно оптимизатор рассматривает возможность сделать это (даже без дополнительного предиката в запросе). Он распознает подразумеваемые предикаты ( BETWEEN
on T1
и предикат соединения между T1
и T2
вместе подразумевают BETWEEN
on 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
этот способ, и производительность страдает из-за отсутствия поиска на стороне зонда полусоединения слиянием, вы можете рассмотреть возможность написания запроса с использованием другого синтаксиса, который дает правильные результаты и более эффективный план выполнения.