查看一个运行缓慢的查询的执行计划,我注意到一些节点是索引搜索,其中一些是索引扫描。
Index Seek vs Index Scan有什么区别?
哪个表现更好?
SQL 如何选择一个而不是另一个?
我意识到这是 3 个问题,但我认为回答第一个问题将解释其他问题。
简短版:seek 更好
不那么短的版本:seek 通常要好得多,但是大量的 seek(例如由于糟糕的相关子查询的不良查询设计,或者因为您在游标操作或其他循环中进行了许多查询)可能比扫描,特别是如果您的查询最终可能会从受影响的表中的大多数行返回数据。
它有助于覆盖整个家庭的数据查找操作,以充分了解性能影响。
Table Scans:**完全没有与您的查询相关的索引,规划器被迫使用表扫描,这意味着查看每一行。这可能导致从磁盘读取与表数据相关的每个页面,这通常是最坏的情况。请注意,对于某些查询,即使存在有用的索引,它也会使用表扫描 - 这通常是因为表中的数据非常小,以至于遍历索引更加麻烦(如果是这种情况,您会期望计划随着数据的增长而改变,假设指数的选择性衡量是好的)。
Index Scans with Row Lookups:如果没有找到可直接用于查找的索引,但存在包含正确列的索引,则可以使用索引扫描。例如,如果您有一个包含 20 列的大表,并且在 column1,col2,col3 上有一个索引并且您发出SELECT col4 FROM exampletable WHERE col2=616,在这种情况下,扫描索引以进行查询col2比扫描整个表要好。一旦找到匹配的行,则需要读取数据页以获取 col4 以进行输出(或进一步连接),这就是您在查询计划中看到它时的“书签查找”阶段。
SELECT col4 FROM exampletable WHERE col2=616
col2
Index Scans without Row Lookups:如果上面的示例是,SELECT col1, col2, col3 FROM exampletable WHERE col2=616那么不需要额外的努力来读取数据页:一旦col2=616找到匹配的索引行,所有请求的数据都是已知的。这就是为什么您有时会看到永远不会搜索的列,但可能会被请求输出,添加到索引的末尾 - 它可以节省行查找。当出于这个原因并且仅出于这个原因将列添加到索引时,将它们与INCLUDE子句一起添加以告诉引擎它不需要优化索引布局以基于这些列进行查询(这可以加快对这些列的更新) . 索引扫描也可能来自没有过滤子句的查询:SELECT col2 FROM exampletable将扫描此示例索引而不是表页面。
SELECT col1, col2, col3 FROM exampletable WHERE col2=616
col2=616
INCLUDE
SELECT col2 FROM exampletable
Index Seeks (有或没有行查找):在查找中,并非所有索引都被考虑。对于查询SELECT * FROM exampletable WHERE c1 BETWEEN 1234 AND 4567,查询引擎可以通过对索引进行基于树的搜索来找到将匹配的第一行,c1然后它可以按顺序导航索引,直到它到达范围的末尾(这与查询相同因为c1=1234即使对于操作,也可能有许多行与条件匹配=)。这意味着只需要读取相关的索引页面(加上初始搜索所需的一些),而不是索引(或表)中的每个页面。
SELECT * FROM exampletable WHERE c1 BETWEEN 1234 AND 4567
c1
c1=1234
=
Clustered Indexes: :**使用聚集索引,表数据存储在该索引的叶节点中,而不是存储在单独的堆结构中。这意味着无论需要什么列,在使用该索引查找行之后都不需要任何额外的行查找[除非您有离页数据,例如TEXT列或VARCHAR(MAX)包含长数据的列]。
TEXT
VARCHAR(MAX)
由于这个原因,您只能有一个聚集索引[1],聚集索引是您的表,而不是具有单独的堆结构,因此如果您使用一个[2],请仔细选择放置位置以获得最大收益。
还要注意聚集索引,因为“聚集键”为表而包含在表上的每一个非聚集索引中,所以广泛的聚集索引一般不是一个好主意。
[1] 实际上,您可以通过定义覆盖或包含表上每一列的非聚集索引来有效地拥有多个聚集索引,但这可能会浪费空间并影响写入性能,因此如果您考虑这样做,请确保你真的需要。
[2] 当我说“如果您使用聚集索引”时,请注意通常建议您在每个表上都有一个。与所有经验法则一样,也有例外,除了批量插入和无序读取(可能是 ETL 进程的暂存表)之外几乎看不到什么的表是最常见的反例。
附加点:不完整的扫描:
重要的是要记住,根据查询的其余部分,表/索引扫描实际上可能不会扫描整个表——如果逻辑允许,查询计划可能会导致它提前中止。最简单的例子是SELECT TOP(1) * FROM HugeTable- 如果您查看查询计划,您会看到扫描只返回了一行,如果您查看 IO 统计信息 ( SET STATISTICS IO ON; SELECT TOP(1) * FROM HugeTable),您会发现它只读取了一个非常小的数字页数(可能只有一个)。
SELECT TOP(1) * FROM HugeTable
SET STATISTICS IO ON; SELECT TOP(1) * FROM HugeTable
WHERE如果or子句的谓词JOIN ... ON可以与作为数据源的扫描同时运行,也会发生同样的情况。查询计划者/运行者有时可以非常聪明地将谓词推回数据源以允许以这种方式提前终止扫描(有时您可以巧妙地重新安排查询以帮助它这样做!)。虽然数据按照标准查询计划显示中的箭头从右到左流动,但逻辑从左到右运行,并且每个步骤(从右到左)不一定在下一个可以开始之前运行完成。在上面的简单示例中,如果您将查询计划中的每个块视为代理,代理会SELECT向代理询问TOP一行,而后者又会询问TABLE SCAN一个代理,然后SELECT代理要求另一个,但TOP代理知道没有必要甚至不费心询问表格阅读器,SELECT代理得到“不再相关”的响应并且知道所有工作都已完成。当然,许多操作会阻止这种优化,在更复杂的示例中,表/索引扫描确实会读取每一行,但请注意不要得出任何扫描都必须是昂贵操作的结论。
WHERE
JOIN ... ON
SELECT
TOP
TABLE SCAN