当数据按照聚集索引排序时,使用覆盖索引是否划算?

7
在我的场景中,我有文章,这些文章被分组在类别中。我想要在类别的概述列表中显示与类别相同的前10篇文章的摘要(而不是显示完整数据的类别详细视图)。前10篇文章由来自另一个表的分数(实际上是一个索引视图 - 但这在此处并不重要)确定。
表结构如下:
CREATE TABLE [dbo].[Categories]
(
    [Id] INT NOT NULL IDENTITY CONSTRAINT [PK_Categories] PRIMARY KEY,
    [Key] CHAR(10) CONSTRAINT [UK_Categories_Key] UNIQUE,
    [Caption] NVARCHAR(500) NOT NULL,
    [Description] NVARCHAR(4000) NULL
)
GO

CREATE TABLE [dbo].[Posts]
(
    [Id] INT NOT NULL IDENTITY CONSTRAINT [PK_Posts] PRIMARY KEY,
    [CategoryId] INT NOT NULL CONSTRAINT [FK_Posts_Category] FOREIGN KEY REFERENCES [dbo].[Categories] ([Id]),
    [Key] CHAR(10) CONSTRAINT [UK_Post_Key] UNIQUE,
    [Text] NVARCHAR(4000) NULL,
    [SummaryText] AS
        CASE WHEN LEN([Text]) <= 400
            THEN CAST([Text] AS NVARCHAR(400))
            ELSE CAST(SUBSTRING([Text], 0, 399) + NCHAR(8230) AS NVARCHAR(400)) --First 399 characters and ellipsis
        END
        PERSISTED
)
GO

CREATE TABLE [dbo].[Scores] (
    [Id] INT NOT NULL IDENTITY CONSTRAINT [PK_Scores] PRIMARY KEY,
    [CategoryId] INT NOT NULL CONSTRAINT [FK_Scores_Category] FOREIGN KEY REFERENCES [dbo].[Categories] ([Id]),
    [PostId] INT NOT NULL CONSTRAINT [FK_Scores_Post] FOREIGN KEY REFERENCES [dbo].[Posts] ([Id]),
    [Value] INT NOT NULL
)
GO

CREATE INDEX [IX_Scores_CategoryId_Value_PostId]
    ON [dbo].[Scores] ([CategoryId], [Value] DESC, [PostId])
GO

我现在可以使用一个视图来获取每个类别的前十篇帖子:
CREATE VIEW [dbo].[TopPosts]
AS
SELECT c.Id AS [CategoryId], cp.PostId, p.[Key], p.SummaryText, cp.Value AS [Score]
FROM [dbo].[Categories] c
CROSS APPLY (
    SELECT TOP 10 s.PostId, s.Value
    FROM [dbo].[Scores] s
    WHERE s.CategoryId = c.Id
    ORDER BY s.Value DESC
) AS cp
INNER JOIN [dbo].[Posts] p ON cp.PostId = p.Id

我知道CROSS APPLY将使用覆盖索引IX_Scores_CategoryId_Value_PostId,因为它包含类别ID(用于WHERE),值(用于ORDER BYSELECT)以及文章ID(用于SELECT),因此查询速度会比较快。

现在的问题是:INNER JOIN呢?Join谓词使用的是文章ID,这是Post表的聚集索引(主键)的关键字。当我创建一个包括SELECT所有字段的覆盖索引(见下文)时,即使访问聚集索引已经是一个相当快的操作,能否显著提高查询性能(通过更好的执行计划、减少I/O、索引缓存等)?

覆盖索引应该长这样:

CREATE INDEX [IX_Posts_Covering]
    ON [dbo].[Posts] ([Id], [Key], [SummaryText])
GO

更新:

由于我的问题方向似乎不是很清晰,让我更详细地阐述一下我的想法。我想知道覆盖索引(或包含列的索引)是否会因为以下原因而更快(并且性能提升值得):

  1. 硬盘访问。 第二个索引将比聚集索引小得多,SQL Server需要在硬盘上浏览较少的页面,这将产生更好的读取性能。这是正确的吗?您会看到差异吗?
  2. 内存消耗。 为了将数据加载到内存中,我认为SQL Server必须将整行加载到内存中,然后选择所需的列。这会增加内存消耗吗?
  3. CPU。 我的假设是,您不会看到CPU使用率的可测量差异,因为从列中提取行本身不是一个CPU操作。对吗?
  4. 缓存。 我的理解是,您不会看到太多缓存方面的差异,因为SQL Server只会缓存返回的数据,而不是整行数据。或者我错了吗?

这些基本上是(或多或少有教养的)假设。如果有人能让我对这个确实非常特定的问题有所启发,我将不胜感激。


3
让SSMS为您展示两个选项的实际执行计划(使用和不使用额外索引),您将立即看到a)优化器是否会选择索引进行联接,以及b)在您的SQL Server版本上是否存在显著的性能提升。我的猜测是会有,因为索引优化向导的自动建议通常在像您这样的场景中包含这种类型的索引。 - Cee McSharpface
1
执行计划将向我展示是否使用索引。是否值得使用索引取决于表中的数据量。我希望避免在生产数据库中遇到性能问题。因此,如果您(或其他人)在类似情况下有经验,那将对我很有帮助。 - Sefe
请查看@dlatikay上面的评论,那是您所寻求的答案。 - Neo
2
@dlatikay:是的,根据当前性能和经验数据设置索引是常见做法。我们也在这样做。我想进一步尝试“理解”什么是更好的解决方案。我越了解DBMS,就越能设计出更好的数据库。我很乐意走经验路线,但我想知道为什么SQL服务器会表现出某种行为。覆盖索引会减少表上的I/O吗?它会在DB服务器上使用更少的内存吗?它会更好地缓存吗?等等。 - Sefe
好的,只是想找出是否有未解决的问题。 - usr
显示剩余3条评论
3个回答

5
这是一个有趣的问题,因为你提出的所有四个子问题都可以回答“取决于”,这通常是主题有趣的好迹象。
首先,如果您对SQL Server如何在内部工作有着不健康的痴迷(就像我一样),那么最好的信息来源是Delaney等人的“Microsoft SQL Server Internals”。 您不需要阅读全部 ~1000页,仅存储引擎章节就足够有趣了。
我不会触及这个特定覆盖索引是否在此特定情况下有用的问题,因为我认为其他答案已经很好地涵盖了这一点(没有双关语),包括使用INCLUDE为不需要自己被索引的列进行建议。
第二个索引将比聚集索引小得多,SQL Server需要通过较少的HD页面,这将产生更好的读取性能。 是否正确并且您会看到差异?
如果您假设选择要么读取聚集索引的页面,要么读取覆盖索引的页面,则覆盖索引较小1,这意味着较少的I / O,更好的性能等美好的东西。 但是查询不会在真空中执行-如果这不是表上唯一的查询,则缓冲池可能已经包含大多数或全部聚集索引,在这种情况下,通过读取不太常用的覆盖索引也可能会对磁盘读取性能产生负面影响。 总体性能也可能因数据页总增加而降低。 优化器仅考虑单个查询;它不会基于所有组合的查询仔细调整缓冲池使用(删除页面通过简单的LRU策略进行)。 因此,如果您过度创建索引,特别是不经常使用的索引,则整体性能将受到影响。 这甚至没有考虑插入或更新数据时索引固有的开销。
即使我们假设覆盖索引是净收益,问题“您是否会看到差异”(例如,性能是否可衡量地增加)只能通过实证回答。 SET STATISTICS IO ON 在这里是您的朋友(以及测试环境中的DBCC DROPCLEANBUFFERS )。 您可以尝试根据假设进行猜测,但由于结果取决于执行计划,索引的大小,SQL Server总共具有的内存量,I / O特征,所有数据库的负载和应用程序的查询模式,因此我不会超出球场猜测索引可能有用的范围。 通常来说,如果您有一个非常宽的表和一个小的覆盖索引,那么很容易看到这是如何支付的。 通常来说,您将更快地从缺少索引而不是从太多索引中看到糟糕的性能。 但是真实的数据库不运行在概括上。
要将数据加载到内存中,我假设SQL Server必须将整个行加载到内存中,然后选择所需的列。 那不会增加内存消耗吗?
请参见上文。聚集索引所占用的页面比覆盖索引多,但内存使用情况是否受到积极或消极影响取决于每个索引的使用方式。在最坏的情况下,聚集索引被其他查询强烈使用,这些查询不从您的覆盖索引中获益,而覆盖索引只对少数查询有帮助,因此所有覆盖索引所做的就是导致缓冲池翻转,从而减慢大部分工作负载的速度。这种情况很少见,表明您的服务器可能需要进行内存升级,但这确实是可能的。
我的假设是,你不会看到可衡量的CPU使用差异,因为从列中提取行本身不是CPU操作。正确吗?
CPU使用通常不会受到行大小的影响。执行时间会受到影响(反过来,这会根据您要并行运行多少查询而影响使用情况)。一旦通过为服务器提供足够的内存来解决I/O瓶颈,仍然存在扫描内存中的数据的问题。
我理解的是,你不会看到太大的缓存差异,因为SQL Server只会缓存它返回的数据,而不是整行数据。或者我错了吗?
行存储在页面上,SQL Server缓存它读取到的页面。它不会缓存结果集或查询执行期间生成的任何中间数据或单个行。如果在最初的空缓冲池上执行两次查询,则第二次查询通常更快,因为它需要的页面已经在内存中,但这是唯一的加速来源。
考虑到这一点,请参见您第一个问题的答案-是的,缓存会受到影响,因为如果使用覆盖索引,则其页面与使用聚集索引的页面分别缓存。
1 覆盖索引实际上可能不比由于页面拆分而严重碎片化而变小。但这是一个学术观点,因为它实际上并不关心哪个索引物理上更大,而是实际访问每个索引的页面数量。

聚集索引并不是真正的索引,它确保表以B树的形式组织,而非聚集索引则保存在单独的页面中。正确吗? - shawn
1
@shawn:聚集索引和非聚集索引非常相似。区别在于,聚集索引实际上也是“表格”——它不仅仅是一个索引,而且还是所有行数据的容器,而非聚集索引只包含它们的索引键(加上包含的列)和聚集索引中的键作为指针。MSDN有一篇很好的文章,解释了这一点。 - Jeroen Mostert
当然,事情并不像你希望的那么简单。我认为,像调整查询一样,没有简单的答案。无论如何,我的原始问题已经消失了,因为现在我不是连接一个表,而是一个索引视图(另一个),所以有另一个合理小的聚集索引。 - Sefe

4

不需要这个覆盖索引。

限制每个表的索引数量:一个表可以有任意数量的索引。但是,索引越多,修改表时产生的开销就越大。因此,检索表中数据的速度和更新表的速度之间存在一种权衡关系

您的情况更可能是 OLTP 系统而不是数据仓库,它将有大量的在线事务(插入、更新、删除)。因此,创建此覆盖索引将减慢修改操作。

更新:

是的,每个类别将有 10 条帖子。因此,如果您有 N 种类别类型,则返回结果集最多为 10*N 条帖子记录。

关于索引的另一个指导方针:如果您经常想要检索大型表中少于15%的行,请创建索引。(我的 SQL 调优讲师建议我们使用 5%)如果大于 15%,则在使用索引时最终执行计划将不是最优的。

让我们考虑您的 POST 表的两种极端情况:

  1. 帖子表只有10*N条记录,每个类别类型都被帖子记录命中了10次。因此,最终的执行计划将全表扫描帖子表而不使用任何索引。
  2. 帖子表的数量大于(10*N/15%),因此它将检索少于帖子表中15%的行。优化器将使用帖子ID字段进行连接操作。并且应该是哈希连接。

因此,即使您已经创建了一个覆盖索引,优化器也永远不会使用它,除非您使用提示。

更新:

聚集索引和非聚集索引的描述


1
谢谢回复。我知道读写性能的权衡。在这种情况下,数据被读取的频率远高于写入的频率。虽然我同意保持索引数量较低,但这个操作是系统中使用最多的操作之一(如果不是最多的),所以付出额外索引的大小代价可能是值得的(它几乎达到了900字节的大小限制)。但也许有人在类似的场景中有实际经验。而且对帖子表的访问将远远超过10次。每个类别将有10篇文章。 - Sefe
抱歉,我不想让您感到困扰,但我仍然不明白。为什么优化器会更喜欢聚集索引的扫描/搜索而不是索引的搜索?即使索引相当大,它仍然比聚集索引搜索产生更少的IO。当可以遍历索引时,它甚至不必触及表格,那么为什么会这样呢? - Sefe
@Sefe 我希望这个链接能帮助你:https://dev59.com/yXM_5IYBdhLWcg3wvFzJ - shawn
2
我知道聚集索引和非聚集索引之间的区别。并且它不正确地认为它在任何情况下都会访问聚集索引。这就是覆盖索引的全部意义:避免表格访问。这里的问题是,在这种特定情况下,访问覆盖索引是否比访问聚集索引更加便宜。 - Sefe
很复杂。要记住,由于缺少索引而导致的额外开销会从其他领域占用资源,例如可能需要更长时间的锁定,这反过来会阻止插入并最终升级为其他事情。这就是为什么每个人都建议进行大量基线和测量,并仅应用最小的更改并测量结果。 - Nick.McDermaid
显示剩余3条评论

4
你的非聚集覆盖索引可能比聚集索引多提供一些性能优势,但这取决于你查询的数据大小。如果行数相对较少,则可能没有什么有用的优势。
退一步说,考虑到你的联接谓词只是 [Posts].[Id],将 [Key] 和 [SummaryText] 列作为键列添加到索引中是不必要的。它们应该作为非键列添加:
CREATE NONCLUSTERED INDEX [IX_Posts_Covering]
    ON [dbo].[Posts] ([Id])
    INCLUDE ([Key], [SummaryText])
GO

根据微软的说法: MSDN - 使用包含列创建索引

重新设计非聚集索引,使只有用于搜索和查找的列成为关键列。将所有其他覆盖查询的列作为非关键列。通过这种方式,您将拥有覆盖查询所需的所有列,但索引键本身是小而高效的。

在非聚集索引中包含非关键列,以避免超出最大16个关键列和900字节最大索引键大小的当前索引大小限制。数据库引擎在计算索引关键列数或索引键大小时不考虑非关键列。

基本上,覆盖索引会复制 [dbo].[Posts] 表,但不包含 [CategoryId] 和 [Text] 列。由于覆盖索引中的列较少,SQL 可以在每个索引页中存储更多行。基于这个假设(可能需要审查),当 SQL 遍历 B 树以跨越页面查找匹配的行时,它可能在覆盖索引上表现得更好,因为它要加载和查找的页面较少。

无论索引选择如何,您可能还应考虑将加入[Posts]表的操作放入交叉应用程序中。这可能会强制执行搜索,但数据的构成将决定效率。
CREATE VIEW [dbo].[TopPosts]
AS
SELECT c.[Id] AS [CategoryId], cp.[PostId], 
    cp.[Key], cp.[SummaryText], cp.[Value] AS [Score]
FROM [dbo].[Categories] c
CROSS APPLY (
    SELECT TOP 10 s.[PostId], s.[Value], p.[Key], p.[SummaryText]
    FROM [dbo].[Scores] s
    INNER JOIN [dbo].[Posts] p ON s.[PostId] = p.[Id]
    WHERE s.[CategoryId] = c.[Id]
    ORDER BY s.[Value] DESC
) AS cp

最终取决于数据大小、磁盘IO、RAM等因素。您需要决定覆盖索引使用的额外空间是否能够证明名义上的性能提升是否合理。

索引使用的详细说明:https://dba.stackexchange.com/a/42568/2916


谢谢回复。我考虑过使用INCLUDEs。我花了半个小时尝试找到一篇文章,其中有人进行了性能测试,并且覆盖索引比包含INCLUDES的索引产生了更好的性能。但是我没有找到它。我的观点是,在表中具有大型NVARCHAR列的情况下,聚集索引查找可能需要浏览比覆盖索引更多的页面。这就是为什么我正在考虑使用覆盖索引的选项。 - Sefe
根据 MSDN 的设计建议,https://msdn.microsoft.com/en-us/library/ms190806.aspx,针对您提到的关键列与非关键列(包含列)问题,“重新设计具有大索引键大小的非聚集索引,以使仅用于搜索和查找的列成为关键列。将覆盖查询的所有其他列设置为非关键列。通过这种方式,您将拥有涵盖查询所需的所有列,但索引键本身是小而高效的。” - uhleeka
@sefe:关于您提到的大型NVARCHAR的问题,我原则上同意聚集索引需要遍历更多页面而非覆盖索引的观点。但是这似乎只有在您进行非常精细的查询调优时才会考虑实施。在我看来,缓存、RAM、I/O、碎片化、统计数据等因素都将成为调优到该级别时的重要因素。 - uhleeka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接