当数据按聚簇索引的顺序排列时,覆盖索引是不是有用?
Posted
技术标签:
【中文标题】当数据按聚簇索引的顺序排列时,覆盖索引是不是有用?【英文标题】:Does a covering index pay off when the data is in order of the clustered index?当数据按聚簇索引的顺序排列时,覆盖索引是否有用? 【发布时间】:2016-11-14 13:15:37 【问题描述】:在我的场景中,我有帖子,这些帖子按类别分组。对于类别的概述列表,我想显示前 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 BY
和SELECT
)和帖子ID (对于SELECT
),因此会相当快。
现在的问题是:INNER JOIN
呢?连接谓词使用post ID,它是Post
表的聚集索引的键(主键)。当我创建一个包含SELECT
的所有字段的覆盖索引时(见下文),我是否可以显着提高查询性能(使用更好的执行计划、减少 I/O、索引缓存等),即使访问集群index 已经是一个相当快的操作了?
覆盖索引如下所示:
CREATE INDEX [IX_Posts_Covering]
ON [dbo].[Posts] ([Id], [Key], [SummaryText])
GO
更新:
由于我的问题的方向似乎并不完全清楚,让我更详细地写下我的想法。我想知道覆盖索引(或包含列的索引)是否会因为以下原因而更快(并且性能提升是值得的):
-
硬盘访问。第二个索引将比聚集索引小得多,SQL Server 将不得不在 HD 上通过更少的页面,这将产生更好的读取性能。这是正确的吗?您能看出其中的不同吗?
内存消耗。 要将数据加载到内存中,我假设 SQL Server 必须将整行加载到内存中,然后选择它需要的列。这不会增加内存消耗吗?
CPU。 我的假设是您不会看到 CPU 使用率的可测量差异,因为从列中提取行本身并不是 CPU 操作。正确吗?
缓存。我的理解是,您不会看到缓存有太大的不同,因为 SQL Server 只会缓存它返回的数据,而不是整行。还是我错了?
这些基本上是(或多或少受过教育的)假设。如果有人能就这个公认的非常具体的问题告诉我,我将不胜感激。
【问题讨论】:
让 SSMS 向您显示两个选项的实际执行计划(有和没有附加索引),您将立即看到是否 a) 优化器将选择 JOIN 的索引和 b) 那里是您的 SQL Server 版本的显着性能提升。我的猜测是肯定的,因为索引优化向导的自动建议通常在像你这样的场景中包含那种索引。 执行计划会告诉我 if 它使用索引。是否值得使用索引将取决于表中的数据量。我想避免在生产数据库中遇到性能问题。因此,如果您(或其他人)有类似情况的经验,那将对我有很大帮助。 参见上面的@dlatikay 评论。这就是你得到你所寻求的答案的方式,先生。 @Sefe 这就是为什么索引并不总是,也不一定是数据库模式的静态部分。添加它会有一个折衷:查询速度/插入速度/更新速度和存储大小,尤其是当您包含有效负载时 (SummaryText
)。有疑问,不要现在创建它,而是从生产数据库的 DBA 那里获得反馈,如果经验获得的执行计划建议,让他们创建索引。
@dlatikay:是的,通常的做法是根据当前的表现和经验数据设置索引。我们也在这样做。我想更进一步,并尝试了解什么是更好的解决方案。我对 DBMS 了解得越多,我就越能设计 DB。我很乐意采用经验方法,但我想知道为什么 SQL Server 会以某种方式运行。覆盖索引会减少表上的 I/O 吗?它会在数据库服务器上使用更少的内存吗?它会更好地缓存吗?等等。
【参考方案1】:
这是一个有趣的问题,因为您提出的所有四个子问题都可以用“视情况而定”来回答,这通常表明主题很有趣。
首先,如果您对 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:聚集索引和非聚集索引非常相似。不同之处在于,聚集索引实际上也是“表”——它不仅仅是一个索引,而是所有行数据的容器,而非聚集索引只包含它们的索引键(加上包含列)和聚集索引中的键作为指针。 MSDN 有一个很好的 article 来解释它。 当然,事情并不像你希望的那么简单。我认为,与查询调优一样,没有简单的答案。无论如何,我原来的问题已经消失了,因为现在我不是加入一个表,而是一个索引视图(另一个),所以还有另一个相当小的聚集索引。【参考方案2】:不,你不需要这个覆盖索引。
限制每个表的索引数量:一个表可以有任意数量的索引。但是,索引越多,修改表时产生的开销就越大。因此,在从表中检索数据的速度和更新表的速度之间需要权衡取舍。
您的场景更有可能是一个 OLTP 系统而不是数据仓库,它将有大量的在线事务(插入、更新、删除)。所以创建这个覆盖索引会减慢你的修改操作。
更新:
是的,每个类别将有 10 个帖子。所以如果你有N个分类类型,返回结果集最多是10*N个帖子记录。
关于索引的另一条准则:如果您经常要检索大表中少于 15% 的行,请创建索引。 (我的 SQL Tuning 教练建议我们 5%)如果大于 15%,当我们使用 Index 时,最终的执行计划将不是最优的。
让我们考虑一下关于 POST 表的两种极端情况:
-
post 表只有 10*N 条记录,每个类别类型被 post 记录命中 10 次。所以最终的执行计划会全盘扫描 POST 表,而不是使用任何索引。
Post 表的数量大于 (10 * N / 15%),因此它将检索不到 15% 的 Post 表中的行。优化器将使用 Post ID 字段进行连接操作。它应该是一个哈希连接。
所以即使你创建了一个覆盖索引,优化器也不会使用它,除非你使用提示。
更新:
Clustered and Nonclustered Indexes Described
【讨论】:
感谢您的回复。我知道读/写性能权衡。在这种情况下,读取数据的频率高于写入数据的频率。虽然我同意将索引数量保持在较低水平,但此操作是系统中最常用的操作之一(如果不是 the 最常用的),因此可能值得为索引的大小付出代价附加索引(几乎达到 900 字节的大小限制)。但也许有人在类似的情况下有一些实践经验。并且将有超过 10 次对帖子表的访问。 每个类别将有 10 个帖子。 @Sefe 感谢您指出我的错误。是的,每个类别将有 10 个帖子。我已经更新了我的帖子 对不起,我不想成为一个痛苦的人,但我还是不明白。为什么优化器更喜欢聚集索引扫描/搜索而不是索引搜索?即使索引相当大,它仍然产生比聚集索引查找更少的 IO。当它可以遍历索引时,它甚至不必触摸表,那为什么会呢? @Sefe 聚集索引确保表中的行存储顺序与索引描述的顺序相同。所以一张表只能有一个聚集索引。最后,您从 POST 表中检索数据而不是索引本身。这个链接是关于聚集索引和非聚集索引的:msdn.microsoft.com/en-us/library/ms190457.aspx 我知道聚集索引和非聚集索引之间的区别。而且无论如何它都会访问聚集索引是不正确的。这就是覆盖索引的全部意义:避免表访问。这里的问题是在这种特定情况下访问覆盖索引而不是聚集索引是否相当便宜。【参考方案3】:与聚集索引相比,您的非聚集覆盖索引可能会给您带来名义上的额外性能优势,但这将取决于您正在查询的数据的大小。如果行数相对较少,则可能没有任何有用的优势。
退后一步,鉴于您的连接谓词只是 [Posts].[Id],将 [Key] 和 [SummaryText] 列添加为索引中的键列是不必要的。它们应该被添加为非键列:
CREATE NONCLUSTERED INDEX [IX_Posts_Covering]
ON [dbo].[Posts] ([Id])
INCLUDE ([Key], [SummaryText])
GO
每个微软:MSDN - Create Indexes with Included Columns
重新设计具有较大索引键大小的非聚集索引,以便只有用于搜索和查找的列是键列。将涵盖查询的所有其他列设为非键列。这样,您将拥有覆盖查询所需的所有列,但索引键本身很小且高效。
在非聚集索引中包含非键列以避免超出当前索引大小限制,即最多 16 个键列和最大索引键大小 900 字节。数据库引擎在计算索引键列数或索引键大小时不考虑非键列。
基本上,覆盖索引复制了 [dbo].[Posts] 表,不包括 [CategoryId] 和 [Text] 列。因为覆盖索引中的列将更少,SQL 应该能够在每个索引页中填充更多行。基于这个假设(诚然,这可能需要仔细检查),当 SQL 遍历 b-tree 时,在页面之间寻找匹配的行,它在覆盖索引上的性能可能名义上更好,因为它需要加载和查看的页面更少.
无论选择何种索引,您也可以考虑将您对 [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
【讨论】:
感谢您的回复。我考虑过使用 INCLUDE。我花了最后半个小时试图找到一篇文章,其中有人进行了性能测试,并且覆盖索引比包含 INCLUDES 的索引产生了更好的性能。我找不到它。我的观点是,对于表中的大 NVARCHAR 列,聚集索引查找可能需要通过比覆盖索引更多的页面。这就是为什么我正在考虑选择覆盖索引。 @sefe:关于键与非键 (INCLUDE) 的第一点,根据 MSDN 设计建议,msdn.microsoft.com/en-us/library/ms190806.aspx,“重新设计具有大索引键大小的非聚集索引,以便仅用于搜索的列和查找是键列。将覆盖查询的所有其他列设为非键列。这样,您将拥有覆盖查询所需的所有列,但索引键本身很小且高效。" @sefe:关于您关于大型 NVARCHAR 的观点,我原则上同意聚集索引必须通过比覆盖索引更多的页面。但是,如果您正在对查询进行非常精细的调整,这似乎只是您会考虑实施的事情。 IMO、缓存、RAM、I/O、碎片、统计信息等都将是重要的因素,一旦您处于该级别的调整。以上是关于当数据按聚簇索引的顺序排列时,覆盖索引是不是有用?的主要内容,如果未能解决你的问题,请参考以下文章