Я всегда по умолчанию NOT EXISTS
.
Планы выполнения могут быть такими же на данный момент, но если в будущем любой столбец будет изменен, чтобы разрешить NULL
s, версия NOT IN
должна будет выполнять больше работы (даже если на самом деле нет NULL
s в данных), и семантика NOT IN
, если NULL
s присутствует , вряд ли будет той, которую вы хотите в любом случае.
Если ни Products.ProductID
, ни [Order Details].ProductID
не позволяют NULL
с, NOT IN
будет обрабатываться идентично следующему запросу.
SELECT ProductID,
ProductName
FROM Products p
WHERE NOT EXISTS (SELECT *
FROM [Order Details] od
WHERE p.ProductId = od.ProductId)
Точный план может отличаться, но для моего примера я получаю следующее.
Довольно распространенным заблуждением является то, что коррелированные подзапросы всегда "плохие" по сравнению с объединениями. Они, конечно, могут быть, когда они вынуждают план вложенных циклов (подзапрос оценивается строка за строкой), но этот план включает логический оператор анти-полусоединения. Анти-полусоединения не ограничиваются вложенными циклами, но могут также использовать хеш-функции или объединения слиянием (как в этом примере).
/*Not valid syntax but better reflects the plan*/
SELECT p.ProductID,
p.ProductName
FROM Products p
LEFT ANTI SEMI JOIN [Order Details] od
ON p.ProductId = od.ProductId
Если [Order Details].ProductID
- NULL
-able, запрос становится
SELECT ProductID,
ProductName
FROM Products p
WHERE NOT EXISTS (SELECT *
FROM [Order Details] od
WHERE p.ProductId = od.ProductId)
AND NOT EXISTS (SELECT *
FROM [Order Details]
WHERE ProductId IS NULL)
Причина этого заключается в том, что правильная семантика, если [Order Details]
содержит какие-либо NULL
ProductId
s, не должна возвращать результатов. См. Дополнительную анти-полусоединение и катушку с подсчетом строк, чтобы убедиться, что это добавлено в план.
Если Products.ProductID
также изменяется на NULL
, то запрос становится
SELECT ProductID,
ProductName
FROM Products p
WHERE NOT EXISTS (SELECT *
FROM [Order Details] od
WHERE p.ProductId = od.ProductId)
AND NOT EXISTS (SELECT *
FROM [Order Details]
WHERE ProductId IS NULL)
AND NOT EXISTS (SELECT *
FROM (SELECT TOP 1 *
FROM [Order Details]) S
WHERE p.ProductID IS NULL)
Причина этого в том, что NULL
Products.ProductId
не должен возвращаться в результатах , за исключением , если подзапрос NOT IN
вообще не должен давать результатов (то есть * 1051) * таблица пуста). В каком случае это должно быть. В плане для моих образцов данных это реализовано добавлением еще одного анти-полусоединения, как показано ниже.
Эффект этого показан в блоге, уже связанном с Бакли . В этом примере количество логических операций чтения увеличилось с 400 до 500 000.
Кроме того, тот факт, что один NULL
может уменьшить количество строк до нуля, очень затрудняет оценку количества элементов. Если SQL Server предполагает, что это произойдет, но на самом деле в данных не было строк NULL
, остальная часть плана выполнения может быть катастрофически хуже, если это только часть более крупного запроса, с неподходящими вложенными циклами, вызывающими повторное выполнение дорогого поддерева, например .
Однако это не единственный возможный план выполнения для NOT IN
в столбце с поддержкой NULL
. В этой статье показан еще один для запроса к базе данных AdventureWorks2008
.
Для столбца NOT IN
в столбце NOT NULL
или NOT EXISTS
для столбца, который может иметь значение NULL или NULL, он дает следующий план.
Когда столбец меняется на NULL
, при этом план NOT IN
теперь выглядит как
Добавляет дополнительный оператор внутреннего соединения в план. Этот аппарат объяснен здесь . Это все, что нужно для преобразования предыдущего поиска с одним коррелированным индексом по Sales.SalesOrderDetail.ProductID = <correlated_product_id>
в два поиска на внешнюю строку. Дополнительный на WHERE Sales.SalesOrderDetail.ProductID IS NULL
.
Так как это находится под анти-полусоединением, если этот возвращает какие-либо строки, второй поиск не произойдет Однако, если Sales.SalesOrderDetail
не содержит NULL
ProductID
с, это удвоит количество требуемых операций поиска.