除了表中可能包含的行数之外,这些示例查询中的一个会比另一个更昂贵吗?
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)
答案 0 :(得分:17)
一般来说,NOT IN
会更贵,尽管可能会构建相反的情景。
首先,假设AccountId
是Accounts
表的主键。
IN (4,6,7,9,10)
将需要5个索引搜索,这意味着逻辑IO是索引深度的5 *(每个搜索需要从根目录向下导航到中间页面,然后到达一个叶子页面)。
NOT IN (4,6,7,9,10)
将需要完整扫描和过滤器(可推送的非sargable谓词意味着它被推入扫描而不是作为单独的运算符),这意味着逻辑IO将等于叶节点中的页数索引+非叶级别的数量。
要看到这个
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
(索引是deper,因为uniquifier已添加到聚簇索引键中)
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
记录比AccountID=4
记录的普遍性高6倍,它仍然给出相同的计划,并且没有重写它,因为范围寻求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)(与IO统计数据中报告的378相比)
NOT IN
版本:寻找<4
次访问1个根页,1个中间页和1个叶页(3)
寻求>4
次访问1个根页,1个中间页和287个叶页(289)
总计:(292)(与IO统计数据中报告的295相比)
事实证明,这些与read-ahead
机制有关。 (在开发实例上)可以使用跟踪标志来禁用此机制,并验证逻辑读取现在按照上面的描述按预期报告。 This is discussed further in the comments to this blog post.
答案 1 :(得分:8)
NOT IN
基本上意味着全表扫描 - 大部分时间。 异常是指索引和索引分布不佳(索引中的值很少)且大多数值都处于NOT IN
条件。
IN
也表示全表扫描。有了索引,它将主要使用索引。另外一个例外是分布不佳的索引或表中的行数很少,全表扫描速度更快。
答案 2 :(得分:0)
不同的DBMS甚至同一DBMS的不同版本可能会因细节而异。 在这些情况下,传统的教科书算法表示IN将优于NOT IN: 1.列上有B +树索引。 2.选择性低。 (对于IN中列出的值) 在实践中,马丁的解决方案是首选:我们应该衡量实际系统。