Является ли SQL 'не более' дорогим ', чем SQL' in '?

Помимо числа строк, которые могут быть в таблице, будет ли один из этих примерных запросов более дорогостоящим, чем другой?

SELECT * FROM dbo.Accounts WHERE AccountID IN (4,6,7,9,10) 

SELECT * FROM dbo.Accounts WHERE AccountID NOT IN (4,6,7,9,10)

Ответы

Ответ 1

В общем случае a NOT IN будет дороже, хотя, безусловно, возможно построить сценарии, где истинно противоположное.

Во-первых, считая, что AccountId является основным ключом для таблицы Accounts.

IN (4,6,7,9,10) потребует 5 указателей, что означает, что логическое значение IO равно 5 * глубине индекса (каждый поиск должен перемещаться от корня до промежуточных страниц и ровно к одной странице листа).

NOT IN (4,6,7,9,10) потребует полного сканирования и фильтра (pushable non sargable predicate означает, что он вставляется в сканирование, а не как отдельный оператор), что означает, что логический IO будет равен количеству страниц в листе node индекс + число нелистовых уровней.

Чтобы увидеть это

CREATE  TABLE #Accounts
(
AccountID INT IDENTITY(1,1) PRIMARY KEY,
Filler CHAR(1000)
)
INSERT INTO #Accounts(Filler)
SELECT 'A'
FROM master..spt_values

SET STATISTICS IO ON


SELECT * FROM #Accounts WHERE AccountID IN (4,6,7,9,10) 
/* Scan count 5, logical reads 10*/

SELECT * FROM #Accounts WHERE AccountID NOT IN (4,6,7,9,10)
/*Scan count 1, logical reads 359*/

SELECT index_depth, page_count
FROM
sys.dm_db_index_physical_stats (2,object_id('tempdb..#Accounts')
                                     , DEFAULT,DEFAULT, 'DETAILED')

Возвращает

index_depth page_count
----------- --------------------
2           358
2           1

Глядя на патологически иной случай, когда все из строк соответствуют предложению IN, и поэтому ни один из них не имеет значения NOT IN

SET STATISTICS IO OFF


CREATE  TABLE #Accounts
(
AccountID INT ,
Filler CHAR(1000)
)

CREATE CLUSTERED INDEX ix ON #Accounts(AccountID)

;WITH Top500 AS
(
SELECT TOP 500 * FROM master..spt_values
), Vals(C) AS
(
SELECT 4 UNION ALL
SELECT 6 UNION ALL
SELECT 7 UNION ALL
SELECT 9 UNION ALL
SELECT 10
)

INSERT INTO #Accounts(AccountID)
SELECT C
FROM Top500, Vals

SET STATISTICS IO ON

SELECT * FROM #Accounts WHERE AccountID IN (4,6,7,9,10) 
/*Scan count 5, logical reads 378*/

SELECT * FROM #Accounts WHERE AccountID NOT IN (4,6,7,9,10)
/*Scan count 2, logical reads 295*/

SELECT index_depth,page_count
FROM
sys.dm_db_index_physical_stats (2,OBJECT_ID('tempdb..#Accounts'), DEFAULT,DEFAULT, 'DETAILED')

Возвращает

index_depth page_count
----------- --------------------
3           358
3           2
3           1

(индекс меньше, поскольку уникальный ключ добавлен в кластеризованный индексный ключ)

IN по-прежнему реализуется по мере того, как ищет 5 равенств, но на этот раз количество листовых страниц, прочитанных в каждом поиске, значительно больше, чем 1. Страницы листа расположены в связанном списке, и SQL Server продолжает перемещаться по этому пути до тех пор, пока он встречает строку, не соответствующую поиску.

NOT IN теперь реализован как 2 диапазона ищет

[1] Seek Keys[1]: END: #Accounts.AccountID < Scalar Operator((4)), 
[2] Seek Keys[1]: START: #Accounts.AccountID > Scalar Operator((4))  

С остаточным предикатом

WHERE  ( #Accounts.AccountID < 6 
          OR #Accounts.AccountID > 6 ) 
       AND ( #Accounts.AccountID < 7 
              OR #Accounts.AccountID > 7 ) 
       AND ( #Accounts.AccountID < 9 
              OR #Accounts.AccountID > 9 ) 
       AND ( #Accounts.AccountID < 10 
              OR #Accounts.AccountID > 10 )  

Итак, можно заметить, что даже в этом крайнем случае лучший SQL Server может сделать это, чтобы пропустить просмотр листовых страниц только для одного из значений NOT IN. Несколько неожиданно, даже когда я перекосил распределение таким образом, что записи AccountID=7 были в 6 раз более распространенными, чем теги AccountID=4, они все же дали один и тот же план и не переписывали его, поскольку диапазон стремится к любой стороне от 7, аналогично, при уменьшении число AccountID=4 записывает в 1 план, возвращенный к кластерному сканированию индекса, поэтому кажется ограниченным рассматривать это преобразование только для первого значения в индексе.

Добавление

В первой половине моего ответа числа складываются точно так, как они ожидали от моего описания и глубины индекса.

Во второй части мой ответ не объяснил точно, почему индекс с 3 уровнями и 358 листовыми страницами должен вызывать вполне точное количество логических чтений, которые он делает, по той очень хорошей причине, что я был не совсем уверен! Однако я заполнил недостающий бит знаний.

Сначала этот запрос (только синтаксис SQL Server 2008+)

SELECT AccountID, COUNT(DISTINCT P.page_id) AS NumPages
FROM #Accounts
CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%) P
GROUP BY AccountID
ORDER BY AccountID

Дает эти результаты

AccountID   NumPages
----------- -----------
4           72
6           72
7           73
9           72
10          73

Добавление NumPages дает в общей сложности 362, отражающий тот факт, что некоторые листовые страницы содержат 2 разных значения AccountId. Эти страницы будут посещены дважды поисками.

SELECT COUNT(DISTINCT P.page_id) AS NumPages
FROM #Accounts
CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%) P
WHERE AccountID <> 4

дает

NumPages
-----------
287

Итак,

Для версии IN:

поиск =4 посещает 1 корневую страницу, 1 промежуточную страницу и 72 листа страниц (74)

поиск =6 посещает 1 корневую страницу, 1 промежуточную страницу и 72 листа страниц (74)

поиск =7 посещает 1 корневую страницу, 1 промежуточную страницу и 73 листа страниц (75)

поиск =9 посещает 1 корневую страницу, 1 промежуточную страницу и 72 листа страниц (74)

поиск =10 посещает 1 корневую страницу, 1 промежуточную страницу и 73 листа страниц (75)

Всего: (372) (против 378, зарегистрированных в статистике IO)

И для версии NOT IN:

поиск <4 посещает 1 корневую страницу, 1 промежуточную страницу и 1 лист страницы (3)

поиск >4 посещает 1 корневую страницу, 1 промежуточную страницу и 287 листовых страниц (289)

Итого: (292) (против 295, зарегистрированных в статистике IO)

А как насчет неучтенного для IO?

Оказывается, что они связаны с механизмом read-ahead. Можно (на экземпляре разработки) использовать флаг трассировки, чтобы отключить этот механизм и убедиться, что логические чтения теперь сообщаются как ожидалось из приведенного выше описания. Это обсуждается далее в комментариях к этому сообщению в блоге.

Ответ 2

NOT IN в основном означает полное сканирование таблицы - большую часть времени. Исключение - это когда индекс индекса и индекс индекса слабый (в индексе есть только несколько значений), а большинство значений находятся в состоянии NOT IN.

IN также означает полное сканирование таблицы, если у вас нет индекса. Имея индекс на нем, он будет в основном использовать индекс. В результате исключение имеет индекс с плохим распределением или несколько строк в таблице, где полное сканирование таблицы выполняется быстрее.

Ответ 3

Разные СУБД и даже разные версии одной и той же СУБД могут различаться по деталям. Традиционный алгоритм учебника говорит, что IN будет лучше, чем NOT IN в этих случаях: 1. В столбце есть индекс дерева B +. 2. селективность низкая. (для значений, перечисленных в IN) На практике предпочтительным является решение Мартина: мы должны измерить фактическую систему.