НЕ В ПРОТИВ НЕ СУЩЕСТВУЕТ


538

Какой из этих запросов быстрее?

НЕ СУЩЕСТВУЕТ:

SELECT ProductID, ProductName 
FROM Northwind..Products p
WHERE NOT EXISTS (
    SELECT 1 
    FROM Northwind..[Order Details] od 
    WHERE p.ProductId = od.ProductId)

Или НЕ В:

SELECT ProductID, ProductName 
FROM Northwind..Products p
WHERE p.ProductID NOT IN (
    SELECT ProductID 
    FROM Northwind..[Order Details])

План выполнения запроса говорит, что они оба делают одно и то же. Если это так, то какая форма рекомендуется?

Это основано на базе данных NorthWind.

[Редактировать]

Только что нашел эту полезную статью: http://weblogs.sqlteam.com/mladenp/archive/2007/05/18/60210.aspx

Я думаю, что я буду придерживаться НЕ СУЩЕСТВУЕТ.


3
вы пробовали план с использованием левого соединения где ноль?
Себас

1
NOT IN и NOT EXISTS не идентичны. Посмотрите на эту ссылку для различия между ними: weblogs.sqlteam.com/mladenp/archive/2007/05/18/60210.aspx
Gokhale

2
Интересно, отличаются ли базы данных, но в моем последнем тесте с PostgreSQL этот NOT INзапрос: SELECT "A".* FROM "A" WHERE "A"."id" NOT IN (SELECT "B"."Aid" FROM "B" WHERE "B"."Uid" = 2)почти в 30 раз быстрее, чем этот NOT EXISTS:SELECT "A".* FROM "A" WHERE (NOT (EXISTS (SELECT 1 FROM "B" WHERE "B"."user_id" = 2 AND "B"."Aid" = "A"."id")))
Phương Nguyễn


1
@rcdmk Вы проверили дату на вопросы?
ilitirit

Ответы:


693

Я всегда по умолчанию NOT EXISTS.

Планы выполнения могут быть одинаковыми в данный момент , но если один столбец изменен в будущем , чтобы NULLВЛЯЕТСЯ NOT INверсию нужно будет делать больше работы (даже если нет NULLs не фактически присутствует в данных) и семантика NOT INесли NULLs есть настоящий вряд ли будут те, которые вы хотите в любом случае.

Когда ни 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) 

Точный план может отличаться, но для моего примера я получаю следующее.

Ни один из NULL

Кажется, довольно распространенным заблуждением является то, что коррелированные подзапросы всегда «плохие» по сравнению с объединениями. Они, конечно, могут быть, когда они вынуждают план вложенных циклов (подзапрос оценивается строка за строкой), но этот план включает логический оператор анти-полусоединения. Анти-полусоединения не ограничиваются вложенными циклами, но могут также использовать хеш-функции или объединения слиянием (как в этом примере).

/*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 ProductIds, не должна возвращать результатов. См. Дополнительную анти-полусоединение и катушку с подсчетом строк, чтобы убедиться, что это добавлено в план.

Один NULL

Если Products.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)
       AND NOT EXISTS (SELECT *
                       FROM   (SELECT TOP 1 *
                               FROM   [Order Details]) S
                       WHERE  p.ProductID IS NULL) 

Причина этого в том, что a NULL Products.ProductIdне должно возвращаться в результатах, кроме случаев, когда NOT INподзапрос вообще не возвращал результатов (т. Е. [Order Details]Таблица пуста). В каком случае это должно быть. В плане для моих образцов данных это реализовано добавлением еще одного анти-полусоединения, как показано ниже.

Оба NULL

Эффект этого показан в сообщении в блоге, уже связанном Бакли . В этом примере количество логических операций чтения увеличилось с 400 до 500 000.

Кроме того, тот факт, что один NULLможет уменьшить количество строк до нуля, очень затрудняет оценку количества элементов. Если SQL Server предполагает, что это произойдет, но на самом деле в данных не было NULLстрок, остальная часть плана выполнения может быть катастрофически хуже, если это только часть более крупного запроса, с ненадлежащими вложенными циклами, вызывающими повторное выполнение дорогостоящей подпрограммы. дерево например .

Однако это не единственно возможный план выполнения для столбца NOT INon NULL-able. Эта статья показывает еще один для запроса к AdventureWorks2008базе данных.

Для столбца NOT INon NOT NULLили NOT EXISTSпротив столбца, обнуляемого или не обнуляемого, он дает следующий план.

Не существует

Когда столбец меняется на NULL-able, NOT INплан теперь выглядит

Не В - Нуль

Это добавляет дополнительный внутренний оператор соединения к плану. Этот аппарат объясняется здесь . Это все, что нужно для преобразования предыдущего поиска по одному коррелированному индексу Sales.SalesOrderDetail.ProductID = <correlated_product_id>в два поиска по внешней строке. Дополнительный включен WHERE Sales.SalesOrderDetail.ProductID IS NULL.

Так как это находится под анти-полусоединением, если этот возвращает какие-либо строки, второй поиск не произойдет. Однако, если Sales.SalesOrderDetailон не содержит NULL ProductIDs, он удвоит количество требуемых операций поиска.


4
Могу ли я спросить, как получить график профилирования, как показано?
XIs

5
@xis Это планы выполнения, открытые в проводнике планов SQL Sentry. Вы также можете просматривать планы выполнения графически в SSMS.
Мартин Смит

Я ценю это по той единственной причине, что: NOT EXISTSфункционирую так, как я ожидаю NOT IN(а это не так).
Левининя

С NOT EXISTS я пытаюсь использовать SELECT 1, такой как NOT EXISTS (SELECT 1 From Sometable WHERE что-то), чтобы базе данных фактически не нужно было возвращать столбцы с диска. Использование EXPLAIN для определения, имеет ли это значение в вашем случае, вероятно, хорошая идея.
Майур Патель

4
@ Mayur Нет необходимости в этом в SQL Server. stackoverflow.com/questions/1597442/…
Мартин Смит

84

Также имейте в виду, что NOT IN не эквивалентен NOT EXISTS, когда дело доходит до нуля.

Этот пост объясняет это очень хорошо

http://sqlinthewild.co.za/index.php/2010/02/18/not-exists-vs-not-in/

Когда подзапрос возвращает хотя бы один ноль, NOT IN не будет соответствовать ни одной строке.

Причину этого можно узнать, посмотрев детали того, что на самом деле означает операция NOT IN.

Скажем, в целях иллюстрации, что в таблице 4 строки с именем t, есть столбец ID со значениями 1..4

WHERE SomeValue NOT IN (SELECT AVal FROM t)

эквивалентно

WHERE SomeValue != (SELECT AVal FROM t WHERE ID=1)
AND SomeValue != (SELECT AVal FROM t WHERE ID=2)
AND SomeValue != (SELECT AVal FROM t WHERE ID=3)
AND SomeValue != (SELECT AVal FROM t WHERE ID=4)

Далее давайте скажем, что AVal равен NULL, где ID = 4. Следовательно, сравнение! = Возвращает НЕИЗВЕСТНО. Логическая таблица истинности для AND гласит, что UNKNOWN и TRUE - UNKNOWN, UNKNOWN и FALSE - FALSE. Нет значения, которое может быть AND'd с НЕИЗВЕСТНЫМ для получения результата ИСТИНА

Следовательно, если какая-либо строка этого подзапроса возвращает NULL, весь оператор NOT IN будет иметь значение FALSE или NULL, и никакие записи не будут возвращены.


24

Если планировщик выполнения говорит, что они одинаковы, они одинаковы. Используйте тот, который сделает ваше намерение более очевидным - в данном случае второй.


3
Время в планировщике выполнения может быть таким же, но результаты выполнения могут отличаться, поэтому есть разница. NOT IN приведет к неожиданным результатам, если в вашем наборе данных будет NULL (см. Ответ Бакли). Лучше всего использовать NOT EXISTS по умолчанию.
nanonerd

15

На самом деле, я считаю, что это будет самым быстрым:

SELECT ProductID, ProductName 
    FROM Northwind..Products p  
          outer join Northwind..[Order Details] od on p.ProductId = od.ProductId)
WHERE od.ProductId is null

2
Может быть не самым быстрым, когда оптимизатор делает свою работу, но, безусловно, будет быстрее, когда это не так.
Cade Roux

2
Возможно, он тоже упростил свой запрос на этот пост
Кип

1
Согласитесь, левое внешнее соединение часто выполняется быстрее, чем подзапрос.
HLGEM

7
@HLGEM Не согласен. По моему опыту, лучший случай для LOJ состоит в том, что они одинаковы, а SQL Server преобразует LOJ в анти-полусоединение. В худшем случае SQL Server LEFT объединяет все и фильтрует значения NULL, после чего может быть гораздо более неэффективным. Пример этого в нижней части этой статьи
Мартин Смит

12

У меня есть таблица, которая содержит около 120 000 записей, и мне нужно выбрать только те, которые не существуют (сопоставленные с столбцом varchar) в четырех других таблицах с числом строк приблизительно 1500, 4000, 40000, 200. Все участвующие таблицы имеют уникальный индекс на соответствующей Varcharколонке.

NOT INзаняло около 10 минут, NOT EXISTSзаняло 4 секунды.

У меня есть рекурсивный запрос, который мог иметь какой-то ненастроенный раздел, который мог бы внести вклад в 10 минут, но другой вариант, занимающий 4 секунды, объясняет, по крайней мере мне, что NOT EXISTSэто намного лучше или, по крайней мере, это INи EXISTSне совсем то же самое и всегда стоит проверьте, прежде чем идти вперед с кодом.


8

В вашем конкретном примере они одинаковы, потому что оптимизатор выяснил, что вы пытаетесь сделать, то же самое в обоих примерах. Но возможно, что в нетривиальных примерах оптимизатор может этого не делать, и в этом случае есть причины отдавать предпочтение одному другому.

NOT INдолжно быть предпочтительным, если вы тестируете несколько строк в вашем внешнем выборе. Подзапрос внутри NOT INоператора может быть оценен в начале выполнения, и временная таблица может быть проверена по каждому значению во внешнем выборе, вместо того, чтобы повторно выполнять подвыбор каждый раз, как того требует NOT EXISTSоператор.

Если подзапрос должен быть соотнесен с внешним выбором, то это NOT EXISTSможет быть предпочтительным, поскольку оптимизатор может обнаружить упрощение, которое предотвращает создание каких-либо временных таблиц для выполнения той же функции.


6

Я использовал

SELECT * from TABLE1 WHERE Col1 NOT IN (SELECT Col1 FROM TABLE2)

и обнаружил, что это дает неправильные результаты (Под неправильным я подразумеваю никаких результатов). Как был NULL в TABLE2.Col1.

При изменении запроса на

SELECT * from TABLE1 T1 WHERE NOT EXISTS (SELECT Col1 FROM TABLE2 T2 WHERE T1.Col1 = T2.Col2)

дал мне правильные результаты.

С тех пор я начал использовать NOT EXISTS везде, где.


5

Они очень похожи, но не совсем одинаковы.

С точки зрения эффективности, я обнаружил, что левое соединение является пустым выражением более эффективным (когда нужно выбрать множество строк, то есть)


2

Если оптимизатор говорит, что они одинаковы, учитывайте человеческий фактор. Я предпочитаю видеть не существует :)


1

Это очень хороший вопрос, поэтому я решил написать очень подробную статью на эту тему в своем блоге.

Модель таблицы базы данных

Предположим, у нас есть две таблицы в нашей базе данных, которые образуют отношение таблицы «один ко многим».

SQL EXISTS таблицы

studentТаблица является родителем, и student_gradeявляется дочерней таблицей , так как он имеет student_id столбец внешнего ключа , ссылающийся Ид столбец первичного ключа в таблице студента.

student tableСодержит следующие две записи:

| id | first_name | last_name | admission_score |
|----|------------|-----------|-----------------|
| 1  | Alice      | Smith     | 8.95            |
| 2  | Bob        | Johnson   | 8.75            |

И в student_gradeтаблице хранятся оценки, полученные студентами:

| id | class_name | grade | student_id |
|----|------------|-------|------------|
| 1  | Math       | 10    | 1          |
| 2  | Math       | 9.5   | 1          |
| 3  | Math       | 9.75  | 1          |
| 4  | Science    | 9.5   | 1          |
| 5  | Science    | 9     | 1          |
| 6  | Science    | 9.25  | 1          |
| 7  | Math       | 8.5   | 2          |
| 8  | Math       | 9.5   | 2          |
| 9  | Math       | 9     | 2          |
| 10 | Science    | 10    | 2          |
| 11 | Science    | 9.4   | 2          |

SQL СУЩЕСТВУЕТ

Допустим, мы хотим, чтобы все ученики получили 10 баллов по математике.

Если нас интересует только идентификатор студента, мы можем выполнить запрос, подобный следующему:

SELECT
    student_grade.student_id
FROM
    student_grade
WHERE
    student_grade.grade = 10 AND
    student_grade.class_name = 'Math'
ORDER BY
    student_grade.student_id

Но приложение заинтересовано в отображении полного имени, а studentне только идентификатора, поэтому нам также нужна информация из studentтаблицы.

Чтобы отфильтровать studentзаписи, которые имеют 10 баллов по математике, мы можем использовать оператор EXISTS SQL, например:

SELECT
    id, first_name, last_name
FROM
    student
WHERE EXISTS (
    SELECT 1
    FROM
        student_grade
    WHERE
        student_grade.student_id = student.id AND
        student_grade.grade = 10 AND
        student_grade.class_name = 'Math'
)
ORDER BY id

При выполнении запроса выше, мы видим, что выбрана только строка Алиса:

| id | first_name | last_name |
|----|------------|-----------|
| 1  | Alice      | Smith     |

Внешний запрос выбирает studentстолбцы строк, которые мы хотим вернуть клиенту. Однако в предложении WHERE используется оператор EXISTS со связанным внутренним подзапросом.

Оператор EXISTS возвращает true, если подзапрос возвращает хотя бы одну запись, и false, если строка не выбрана. Механизм базы данных не должен полностью выполнять подзапрос. Если сопоставляется одна запись, оператор EXISTS возвращает true и выбирается связанная строка запроса.

Внутренний подзапрос коррелируется, потому что столбец student_id student_gradeтаблицы сопоставляется со столбцом id внешней таблицы студентов.

SQL НЕ СУЩЕСТВУЕТ

Давайте рассмотрим, что мы хотим выбрать всех учеников, у которых оценка не ниже 9. Для этого мы можем использовать NOT EXISTS, что сводит на нет логику оператора EXISTS.

Следовательно, оператор NOT EXISTS возвращает true, если базовый подзапрос не возвращает запись. Однако, если внутреннему подзапросу соответствует одна запись, оператор NOT EXISTS вернет false, и выполнение подзапроса может быть остановлено.

Чтобы сопоставить все записи учеников, у которых нет связанных student_grade со значением ниже 9, мы можем выполнить следующий запрос SQL:

SELECT
    id, first_name, last_name
FROM
    student
WHERE NOT EXISTS (
    SELECT 1
    FROM
        student_grade
    WHERE
        student_grade.student_id = student.id AND
        student_grade.grade < 9
)
ORDER BY id

При выполнении запроса выше мы видим, что сопоставляется только запись Алисы:

| id | first_name | last_name |
|----|------------|-----------|
| 1  | Alice      | Smith     |

Таким образом, преимущество использования операторов SQL EXISTS и NOT EXISTS состоит в том, что выполнение внутреннего подзапроса может быть остановлено, если найдена соответствующая запись.


-1

Это зависит..

SELECT x.col
FROM big_table x
WHERE x.key IN( SELECT key FROM really_big_table );

не будет относительно медленным, не слишком ограничивает размер проверки запроса, чтобы увидеть, если они вводят ключ. EXISTS будет предпочтительнее в этом случае.

Но, в зависимости от оптимизатора СУБД, это может быть не иначе.

Как пример того, когда EXISTS лучше

SELECT x.col
FROM big_table x
WHERE EXISTS( SELECT key FROM really_big_table WHERE key = x.key);
  AND id = very_limiting_criteria

1
INи EXISTS получить тот же план в SQL Server . Вопрос в любом случае о NOT INпротив NOT EXISTS.
Мартин Смит
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.