SQL'不是'比''更''昂贵'吗?

Joh*_*ert 14 sql sql-server performance

除了可能在表中的行数之外,这些示例查询中的一个会比另一个更昂贵吗?

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)
Run Code Online (Sandbox Code Playgroud)

Mar*_*ith 17

一般来说,NOT IN虽然肯定有可能构建相反的情景,但是会更昂贵.

首先,假设这AccountIdAccounts表的主键.

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')
Run Code Online (Sandbox Code Playgroud)

返回

index_depth page_count
----------- --------------------
2           358
2           1
Run Code Online (Sandbox Code Playgroud)

看病态不同的情况,其中所有的行都符合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')
Run Code Online (Sandbox Code Playgroud)

返回

index_depth page_count
----------- --------------------
3           358
3           2
3           1
Run Code Online (Sandbox Code Playgroud)

(因为uniquifier已添加到聚簇索引键,索引是deper)

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))  
Run Code Online (Sandbox Code Playgroud)

用剩余谓词

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 )  
Run Code Online (Sandbox Code Playgroud)

因此可以看出,即使在这种极端情况下,最好的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
Run Code Online (Sandbox Code Playgroud)

给出了这些结果

AccountID   NumPages
----------- -----------
4           72
6           72
7           73
9           72
10          73
Run Code Online (Sandbox Code Playgroud)

加起来NumPages总共有362个反映了一些叶子页面包含2个不同AccountId值的事实.搜索将访问这些页面两次.

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

NumPages
-----------
287
Run Code Online (Sandbox Code Playgroud)

所以,

对于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相比)

那么对于未说明的IO呢?

事实证明,这些与read-ahead机制有关.(在开发实例上)可以使用跟踪标志来禁用此机制,并验证逻辑读取现在按照上面的描述按预期报告.本博客文章的评论中对此进行了进一步讨论.


Ali*_*tad 8

NOT IN基本上意味着全表扫描 - 大部分时间.例外情况是当你有一个索引并且索引的分布很差时(索引中只有很少的值)并且大多数值都处于NOT IN条件状态.

IN如果您没有索引,也表示全表扫描.有了索引,它将主要使用索引.另外一个例外是分布不佳的索引或表格中的几行,其中全表扫描更快.