为什么这是一个索引扫描而不是索引寻求?

时间:2011-06-30 00:51:22

标签: performance tsql sql-server-2008 indexing sql-execution-plan

以下是查询:

SELECT      top 100 a.LocationId, b.SearchQuery, b.SearchRank
FROM        dbo.Locations a
INNER JOIN  dbo.LocationCache b ON a.LocationId = b.LocationId
WHERE       a.CountryId = 2
AND         a.Type = 7

位置索引:

PK_Locations:

  

LocationId

IX_Locations_CountryId_Type:

  

CountryId,Type

LocationCache索引:

PK_LocationCache:

  

LocationId

IX_LocationCache_LocationId_SearchQuery_SearchRank:

  

LocationId,SearchQuery,SearchRank

执行计划:

enter image description here

所以它在Locations上进行索引搜索,使用覆盖索引,很酷。

但是为什么它在覆盖索引的LocationCache上进行索引扫描

该覆盖索引在索引中具有LocationId,SearchQuery,SearchRank(而不是“包含的列”)。

将鼠标悬停在索引扫描上:

enter image description here

此查询需要进入由自动完成插件使用的SQL Server FTS目录提供的索引视图,因此需要100%优化。

此时上述查询需要3秒钟。它应该是< 0

有什么想法吗?

5 个答案:

答案 0 :(得分:32)

它主要使用索引扫描,因为它也使用合并连接。 Merge Join运算符需要两个输入流,这两个输入流都按照与Join条件兼容的顺序排序。

它正在使用Merge Join运算符来实现您的INNER JOIN,因为它相信它会比更典型的Nested Loop Join运算符更快。并且它可能是正确的(通常是),通过使用它选择的两个索引,它具有根据您的连接条件(LocationID)预先排序的输入流。当输入流像这样预先排序时,Merge Joins几乎总是比其他两个(Loop和Hash Joins)更快。

缺点是您已经注意到:它似乎是在扫描整个索引,所以如果它读取那么多可能永远不会被使用的记录,那怎么会更快?答案是,扫描(由于它们的顺序性质)可以读取任何数量的记录/秒的10到100倍。

现在寻求通常赢,因为它们是有选择性的:它们只获得你要求的行,而扫描是非选择性的:它们必须返回范围内的每一行。但是因为扫描具有更高的读取速率,只要丢弃行与匹配行的比率更低,它们就可以经常击败Seeks,而不是扫描行/秒的比率VS.寻找行数/秒。

有问题吗?


好的,我被要求更多地解释最后一句话:

“Discarded Row”是Scan读取的行(因为它必须读取索引中的所有内容),但是Merge Join运算符会拒绝它,因为它在另一侧没有匹配,可能是因为WHERE子句条件已经排除了它。

“匹配行”是它读取的那些实际上与Merge Join中的某些内容匹配的行。如果扫描由Seek替换,则这些行与Seek读取的行相同。

您可以通过查看查询计划中的统计信息来确定其中的内容。看到Index Scan左侧那个巨大的胖箭头?这表示优化器认为它将使用Scan读取的行数。您发布的索引扫描的统计信息框显示返回的实际行数约为5.4M(5,394,402)。这等于:

TotalScanRows = (MatchingRows + DiscardedRows)

(就我而言,无论如何)。要获得匹配行,请查看Merge Join运算符报告的“实际行”(您可能必须取下TOP 100才能准确地获得此值)。一旦你知道这一点,就可以通过以下方式获得Discarded行:

DiscardedRows = (TotalScanRows - MatchingRows)

现在你可以计算比率了。

答案 1 :(得分:8)

虽然请记住,当对其进行其他更改时,它会导致查询可能执行得很糟糕,但使用INNER LOOP JOIN会强制覆盖索引在dbo.LocationCache上使用。

SELECT      top 100 a.LocationId, b.SearchQuery, b.SearchRank
FROM        dbo.Locations a
INNER LOOP JOIN dbo.LocationCache b ON a.LocationId = b.LocationId
WHERE       a.CountryId = 2
AND         a.Type = 7

答案 2 :(得分:4)

您是否尝试更新统计信息?

UPDATE STATISTICS dbo.LocationCache

以下是一些很好的参考资料,介绍了查询优化程序将通过搜索选择扫描的原因和原因。

http://social.msdn.microsoft.com/Forums/en-CA/sqldatabaseengine/thread/82f49db8-0c77-4bce-b26c-1ad0a4af693b

摘要

  

有几件事需要考虑   考虑在这里。首先,当SQL   决定最好的(足够好)   计划使用,它查看查询,   然后还看看统计数据   它存储有关表格   参与。

     

然后决定是否更多   有效地寻找指数,或   扫描索引的整个叶级   (在这种情况下,它涉及触摸   表中的每一页,因为它是   聚集索引)它通过这样做   看着很多东西。   首先,它猜测了多少   它需要扫描的行/页面。这个   被称为引爆点,是一个   比你想象的更低的百分比。   看到这个伟大的金伯利特里普博客   http://www.sqlskills.com/BLOGS/KIMBERLY/category/The-Tipping-Point.aspx

     

如果你在...的范围内   引爆点,可能是因为你的   统计数据已过期,或者您的   索引严重分散。

     

可以强制SQL查找   使用FORCESEEK查询索引   提示,但请使用此   一般情况下,谨慎提供给您   保持所有维护,SQL   很擅长决定什么   最有效的计划将是!!

答案 3 :(得分:2)

简而言之:您没有对LocationCache进行过​​滤,应返回整个表内容。你有一个完全覆盖索引。索引SCAN(一次)是最便宜的操作,查询优化器会选择它。

优化: 您正在加入整个表格,之后只获得前100名结果。我不知道它们有多大,但是尝试子查询[Locations]表CountryId, Type然后只使用[LocationCache]加入结果。如果你有超过1000行,将会更快。 另外,如果可能,请尝试在连接之前添加一些限制性更强的过滤器。

索引扫描: 由于扫描触及表中的每一行,无论其是否合格,因此成本与表中的总行数成比例。因此,如果表很小或者大多数行符合谓词的条件,则扫描是一种有效的策略。

索引寻求: 由于搜索仅触及限定行和包含这些限定行的页面,因此成本与合格行和页面的数量成比例,而不是与表中的总行数成比例。

如果表上有索引,并且查询触及大量数据,这意味着查询正在检索超过50%或90%的数据,然后优化器将只扫描所有数据用于检索数据行的页面。

source

答案 4 :(得分:0)

我做了一个快速测试并想出了以下

CREATE TABLE #Locations
(LocationID INT NOT NULL ,
CountryID INT NOT NULL ,
[Type] INT NOT NULL 
CONSTRAINT PK_Locations
        PRIMARY KEY CLUSTERED ( LocationID ASC )
)

CREATE NONCLUSTERED INDEX [LocationsIndex01] ON #Locations
(
    CountryID ASC,
    [Type] ASC
)

CREATE TABLE #LocationCache
(LocationID INT NOT NULL ,
SearchQuery VARCHAR(50) NULL ,
SearchRank INT NOT NULL 
CONSTRAINT PK_LocationCache
        PRIMARY KEY CLUSTERED ( LocationID ASC )

)

CREATE NONCLUSTERED INDEX [LocationCacheIndex01] ON #LocationCache
(
    LocationID ASC,
    SearchQuery ASC,
    SearchRank ASC
)

INSERT INTO #Locations
SELECT 1,1,1 UNION
SELECT 2,1,4 UNION
SELECT 3,2,7 UNION
SELECT 4,2,7 UNION
SELECT 5,1,1 UNION
SELECT 6,1,4 UNION
SELECT 7,2,7 UNION
SELECT 8,2,7 --UNION

INSERT INTO #LocationCache
SELECT 4,'BlahA',10 UNION
SELECT 3,'BlahB',9 UNION
SELECT 2,'BlahC',8 UNION
SELECT 1,'BlahD',7 UNION
SELECT 8,'BlahE',6 UNION
SELECT 7,'BlahF',5 UNION
SELECT 6,'BlahG',4 UNION
SELECT 5,'BlahH',3 --UNION

SELECT * FROM #Locations
SELECT * FROM #LocationCache

SELECT      top 3 a.LocationId, b.SearchQuery, b.SearchRank
FROM        #Locations a
INNER JOIN  #LocationCache b ON a.LocationId = b.LocationId
WHERE       a.CountryId = 2
AND         a.[Type] = 7

DROP TABLE #Locations
DROP TABLE #LocationCache

对我来说,查询计划显示了使用嵌套循环内连接进行搜索。如果你运行这个,你会得到两个寻求吗?如果这样做,那么在您的系统上进行测试并创建Locations和LocationCache表的副本,并将它们称为Locations2和LocationCache2,并将所有索引复制到它们中。然后尝试查询新表?