为什么优化者会选择聚簇索引+排序而不是非聚簇索引?


11

给出下一个示例:

IF OBJECT_ID('dbo.my_table') IS NOT NULL
    DROP TABLE [dbo].[my_table];
GO

CREATE TABLE [dbo].[my_table]
(
    [id]    int IDENTITY (1,1)  NOT NULL PRIMARY KEY,
    [foo]   int                 NULL,
    [bar]   int                 NULL,
    [nki]   int                 NOT NULL
);
GO

/* Insert some random data */
INSERT INTO [dbo].[my_table] (foo, bar, nki)
SELECT TOP (100000)
    ABS(CHECKSUM(NewId())) % 14,
    ABS(CHECKSUM(NewId())) % 20,
    n = CONVERT(INT, ROW_NUMBER() OVER (ORDER BY s1.[object_id]))
FROM 
    sys.all_objects AS s1 
CROSS JOIN 
    sys.all_objects AS s2
GO

CREATE UNIQUE NONCLUSTERED INDEX [IX_my_table]
    ON [dbo].[my_table] ([nki] ASC);
GO

如果我获取[nki](非聚集索引)排序的所有记录:

SET STATISTICS TIME ON;
SELECT id, foo, bar, nki FROM my_table ORDER BY nki;
SET STATISTICS TIME OFF;

SQL Server Execution Times: CPU time = 266 ms, elapsed time = 493 ms

优化器选择聚簇索引,然后应用排序算法。

在此处输入图片说明

Execution plan

但是,如果我强迫它使用非聚集索引:

SET STATISTICS TIME ON;
SELECT id, foo, bar, nki FROM my_table WITH(INDEX(IX_my_TABLE));
SET STATISTICS TIME OFF;

SQL Server Execution Times: CPU time = 311 ms, elapsed time = 188 ms

然后,它使用带有键查找的非聚集索引:

在此处输入图片说明

Execution plan

显然,如果将非聚集索引转换为覆盖索引:

CREATE UNIQUE NONCLUSTERED INDEX [IX_my_table]
    ON [dbo].[my_table] ([nki] ASC)
    INCLUDE (id, foo, bar);
GO

然后,它仅使用此索引:

SET STATISTICS TIME ON;
SELECT id, foo, bar, nki FROM my_table ORDER BY nki;
SET STATISTICS TIME OFF;

SQL Server Execution Times: CPU time = 32 ms, elapsed time = 106 ms

在此处输入图片说明

Execution plan


  • 为什么即使在后一种情况下执行时间快了38%,SQL Server为何也使用聚簇索引加排序算法而不是非聚簇索引?

1
您是要在您的强制索引查询中放弃ORDER BY吗?
福雷斯特

Answers:


9

为什么即使在后一种情况下执行时间快了38%,SQL Server为何也使用聚簇索引加排序算法而不是非聚簇索引?

因为SQL Server使用基于成本的优化器,但不是基于运行时信息,而是基于统计信息。

在此查询的成本估算过程中,它实际上会评估查找计划,但估算会花费更多的精力。(将鼠标悬停在执行计划中的SELECT上时,请注意“ Estimated Subtree Cost”)。这也不一定是一个错误的假设-在我的测试机上,查找计划需要6倍于排序/扫描的CPU。

请看Rob Farley的答案,为什么SQL Server可能会使查找计划的成本更高。


9

如果将100,000次查找所需的读取次数与进行排序所涉及的内容进行比较,您可能会很快了解Query Optimizer为何认为CIX + Sort是最佳选择。

由于正在读取的页面都在内存中,因此执行查找的速度更快,即使您清除了缓存(即使清除了缓存,每页也有很多行,因此您要一遍又一遍地读取相同的页面,但是会有不同的碎片量)或与其他活动不同的内存压力,则可能并非如此)。更快地运行CIX + Sort并不需要花那么多钱,但是您看到的是因为读取成本没有考虑重复击中相同页面的相对便宜。


4

我决定对这个问题进行一些探讨,发现了一些有趣的文档,这些文档讨论了如何以及何时使用或更有效,而不是(非强制)使用非聚集索引。

正如John Eisbrener的评论所建议的那样,金伯利·特里普(Kimberly L. Tripp)的这篇有趣的文章(即使在其他博客中也被引用最多)之一:

但这不是唯一的一个,如果您有兴趣,可以看看以下页面:

如您所见,它们都围绕着引爆点的概念移动。

引用KL Tripp的文章

临界点是什么?

在这一点上,返回的行数“ 不再具有足够的选择性 ”。SQL Server选择不使用非聚集索引来查找相应的数据行,而是执行表扫描。

当SQL Server在堆上使用非聚集索引时,基本上它会获得指向基表页面的指针的列表。然后,它使用这些指针通过一系列称为行ID查找(RID)的操作来检索行。这意味着至少它将使用与返回的行数一样多的页面读取,也许还有更多。该过程与基本表的聚集索引有点类似,但结果相同:读取更多。

但是,何时发生临界点?

当然,就像这辈子的大多数事情一样,这取决于...

不用说,它发生在表中页数的25%到33%之间,具体取决于每页多少行。但是,您应该考虑更多因素:

引用ITPRoToday文章

影响转折点的其他因素尽管RID查找的成本是影响转折点的最重要因素,但还有许多其他因素:

  • 扫描聚簇索引时,物理I / O效率更高。聚簇索引数据按索引顺序顺序放置在磁盘上。因此,磁盘上的横向磁头移动非常少,从而提高了I / O性能。
  • 当数据库引擎正在扫描聚簇索引时,它知道磁盘轨道上的后几页很有可能仍包含其所需的数据。因此,它开始以64KB的块而不是正常的8KB页面开始读取。这也导致更快的I / O。

现在,如果我再次使用统计数据IO执行查询:

SET STATISTICS IO ON;
SELECT id, foo, bar, nki FROM my_table WHERE nki < 20000 ORDER BY nki ;
SET STATISTICS IO OFF;

Logical reads: 312

SET STATISTICS IO ON;
SELECT id, foo, bar, nki FROM my_table WITH(INDEX(IX_my_TABLE));
SET STATISTICS IO OFF;

Logical reads: 41293

第二个查询比第一个查询需要更多的逻辑读取。

我应该避免非聚集索引吗?

不可以,聚集索引可能有用,但是值得花费时间,并花更多的精力来分析您要使用它来实现的目标。

引用KL Tripp的文章

那你该怎么办?这取决于。如果您很了解数据并且进行了广泛的测试,则可以考虑使用提示(您可以在sps中以编程方式进行一些聪明的事情,我将尽力在此发表一篇文章)。但是,一个更好的选择(如果可能的话)是考虑覆盖(这实际上是我的重点:)。在我的查询中,覆盖是不现实的,因为我的查询需要所有列(邪恶的SELECT *),但是,如果您的查询较窄且优先级较高,则最好在提示上使用覆盖索引(在许多情况下),因为涵盖查询的索引,从不提示。

到目前为止,这就是难题的答案,但是肯定还有很多事情可以做。引爆点可能是一件非常好事–通常效果很好。但是,如果您发现可以强制建立索引并获得更好的性能,则可能需要进行一些调查,看看是否是这样。然后考虑提示有多大可能帮助您,现在您知道可以集中精力了。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.