索引键列VS包含索引列


Answers:


138

索引键列是索引b树的一部分。包含的列不是。

取两个索引:

CREATE INDEX index1 ON table1 (col1, col2, col3)
CREATE INDEX index2 ON table1 (col1) INCLUDE (col2, col3)

index1 更适合这种查询:

SELECT * FROM table1 WHERE col1 = x AND col2 = y AND col3 = z

index2更适合这种查询:

SELECT col2, col3 FROM table1 WHERE col1 = x

在第一个查询中,index1提供了一种快速识别感兴趣行的机制。该查询将(可能)作为索引查找执行,然后进行书签查找以检索完整行。

在第二个查询中,index2充当覆盖索引。SQL Server根本不需要命中基表,因为索引提供了满足查询所需的所有数据。index1在这种情况下也可以作为覆盖指数。

如果您想要一个覆盖索引,但又不想将所有列都添加到b树中,因为您没有在它们上寻找,或者因为它们不是允许的数据类型(例如XML)而无法添加,请使用INCLUDE子句。


添加col2col3作为索引键的缺点是什么?
Hp93

2
@ Hp93,缺点是您强制SQL Server在列上维护索引结构。这种结构的成本取决于值更改的频率。如果价值很少改变,那么成本将会很低。如果值经常变化,则成本会更高。无论哪种情况,成本都会大于INCLUDE,因为包含的值不会影响项目在索引中的相对位置。使用,读取速度也要快一些INCLUDE,因为遍历的结构较少,因此吞吐量更高。
Peter Radocchia '18

CREATE INDEX index2 ON table1 (col1) INCLUDE (col2, col3)和之间有什么区别CREATE INDEX index2 ON table1 (col1)
安德斯·林登

INCLUDE将col1和col2添加到索引b树中,因此,如果您的查询不需要表中的任何其他列,则SQL Server可以跳过书签查找以检索整行并直接从索引中直接满足查询。
Peter Radocchia

我有相同的愿景,但是请这个问题stackoverflow.com/questions/64542167/...
罗勒科索沃

14

让我们考虑一下这本书。书中的每一页都有页码。本书中的所有信息均根据该页码顺序显示。用数据库术语来说,页码是聚簇索引。现在考虑一下本书结尾处的词汇表。这是按字母顺序排列的,可让您快速找到特定词汇表术语所属的页码。这表示以词汇表术语作为关键列的非聚簇索引。

现在假设每个页面的顶部也显示“章节”标题。如果要在哪个章节中找到词汇表术语,则必须查找描述该词汇表术语的页面#,下一步-打开相应的页面,然后在页面上查看章节标题。这清楚地表示了关键查找-当您需要从非索引列中查找数据时,必须查找实际数据记录(聚集索引)并查看此列值。包含的列有助于提高性能-考虑词汇表,其中除了词汇表术语外,每个章节的标题都包括在内。如果您需要找出词汇表术语所属的章节-无需打开实际页面-查找词汇表术语时可以获取该页面。

因此,包含的列类似于这些章节标题。非聚簇索引(词汇表)具有一个附加属性,作为非聚簇索引的一部分。索引不是按包含的列排序的,它只是有助于加快查找速度的其他属性(例如,由于信息已经在词汇表索引中,因此您无需打开实际页面)

例:

创建表脚本

CREATE TABLE [dbo].[Profile](
    [EnrollMentId] [int] IDENTITY(1,1) NOT NULL,
    [FName] [varchar](50) NULL,
    [MName] [varchar](50) NULL,
    [LName] [varchar](50) NULL,
    [NickName] [varchar](50) NULL,
    [DOB] [date] NULL,
    [Qualification] [varchar](50) NULL,
    [Profession] [varchar](50) NULL,
    [MaritalStatus] [int] NULL,
    [CurrentCity] [varchar](50) NULL,
    [NativePlace] [varchar](50) NULL,
    [District] [varchar](50) NULL,
    [State] [varchar](50) NULL,
    [Country] [varchar](50) NULL,
    [UIDNO] [int] NOT NULL,
    [Detail1] [varchar](max) NULL,
    [Detail2] [varchar](max) NULL,
    [Detail3] [varchar](max) NULL,
    [Detail4] [varchar](max) NULL,
PRIMARY KEY CLUSTERED 
(
    [EnrollMentId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

SET ANSI_PADDING OFF
GO

存储过程脚本

CREATE Proc [dbo].[InsertIntoProfileTable]
As
BEGIN
SET NOCOUNT ON
Declare @currentRow int
Declare @Details varchar(Max)
Declare @dob Date
set @currentRow =1;
set @Details ='Let''s think about the book. Every page in the book has the page number. All information in this book is presented sequentially based on this page number. Speaking in the database terms, a page number is the clustered index. Now think about the glossary at the end of the book. This is in alphabetical order and allows you to quickly find the page number specific glossary term belongs to. This represents non-clustered index with glossary term as the key column.        Now assuming that every page also shows "chapter" title at the top. If you want to find in what chapter is the glossary term, you have to look up what page # describes glossary term, next - open the corresponding page, and see the chapter title on the page. This clearly represents key lookup - when you need to find the data from non-indexed column, you have to find actual data record (clustered index) and look at this column value. Included column helps in terms of performance - think about glossary where each chapter title includes in addition to glossary term. If you need to find out what chapter the glossary term belongs - you don''t need to open the actual page - you can get it when you look up the glossary term.        So included columns are like those chapter titles. Non clustered Index (glossary) has an additional attribute as part of the non-clustered index. Index is not sorted by included columns - it just additional attributes that help to speed up the lookup (e.g. you don''t need to open the actual page because the information is already in the glossary index).'
while(@currentRow <=200000)
BEGIN
insert into dbo.Profile values( 'FName'+ Cast(@currentRow as varchar), 'MName' + Cast(@currentRow as varchar), 'MName' + Cast(@currentRow as varchar), 'NickName' + Cast(@currentRow as varchar), DATEADD(DAY, ROUND(10000*RAND(),0),'01-01-1980'),NULL, NULL, @currentRow%3, NULL,NULL,NULL,NULL,NULL, 1000+@currentRow,@Details,@Details,@Details,@Details)
set @currentRow +=1;
END

SET NOCOUNT OFF
END

GO

使用上述SP,您可以一次插入200000条记录

您可以看到“ EnrollMentId”列上有一个聚集索引。

现在,在“ UIDNO”列上创建一个非聚集索引。

脚本

CREATE NONCLUSTERED INDEX [NonClusteredIndex-20140216-223309] ON [dbo].[Profile]
(
    [UIDNO] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

现在运行以下查询

select UIDNO,FName,DOB, MaritalStatus, Detail1 from dbo.Profile 
--Takes about 30-50 seconds and return 200,000 results.

查询2

select UIDNO,FName,DOB, MaritalStatus, Detail1 from dbo.Profile
where DOB between '01-01-1980' and '01-01-1985'
 --Takes about 10-15 seconds and return 36,479 records.

现在删除上面的非聚集索引,并使用以下脚本重新创建

CREATE NONCLUSTERED INDEX [NonClusteredIndex-20140216-231011] ON [dbo].[Profile]
(
    [UIDNO] ASC,
    [FName] ASC,
    [DOB] ASC,
    [MaritalStatus] ASC,
    [Detail1] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

它将引发以下错误

消息1919,级别16,状态1,行1表'dbo.Profile'中的列'Detail1'具有无效的类型,不能用作索引中的键列。

因为我们不能使用varchar(Max)数据类型作为键列。

现在,使用以下脚本使用包含的列创建非聚集索引

CREATE NONCLUSTERED INDEX [NonClusteredIndex-20140216-231811] ON [dbo].[Profile]
(
    [UIDNO] ASC
)
INCLUDE (   [FName],
    [DOB],
    [MaritalStatus],
    [Detail1]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

现在运行以下查询

select UIDNO,FName,DOB, MaritalStatus, Detail1 from dbo.Profile --Takes about 20-30 seconds and return 200,000 results.

查询2

select UIDNO,FName,DOB, MaritalStatus, Detail1 from dbo.Profile
where DOB between '01-01-1980' and '01-01-1985'
 --Takes about 3-5 seconds and return 36,479 records.


我有相同的愿景,但是请这个问题stackoverflow.com/questions/64542167/...
罗勒科索沃

6

包含的列不构成索引键的一部分,但它们确实存在于索引中。从本质上讲,这些值将重复,因此存在存储开销,但是您的索引更有可能覆盖(即由查询优化器选择)更多查询。由于数据库引擎可以返回值而不必查看表本身,因此这种重复还可以提高查询时的性能。

只有非聚集索引可以包含列,因为在聚集索引中,每个列都有效地包含在内。


+1 IOW覆盖索引的行密度更高,这意味着SQL需要提取的页面更少。
StuartLC 2010年

@nonnb:具有覆盖索引,您不需要书签查找。是的,读取的页面更少,SQL Server要做的工作更少,但这是因为它所需的所有信息都包含在索引中。对于其某些定义,“信息密度”可以更高。但是,不确定行密度是什么意思。
彼得·拉多基亚

1

我想在其他答案中添加有关索引键列和包含列的更多详细信息,以及包含列使用的好处。对于此答案,我从Markus Winand发表于2019-04-30的文章``仔细阅读索引包含子句''中获取了以下信息:https ://use-the-index-luke.com/blog/2019- 04 / include-columns-btree-indexs

索引键列与包含的列有何不同的简要概述

要了解include子句,您必须首先了解使用索引最多会影响三层数据结构:

  • B树
  • B树的叶子节点级别的双链表
  • 桌子

前两个结构共同形成一个索引,因此可以将它们组合为一个项目,即“ B树索引”。在一般情况下,数据库软件开始遍历B树以在叶节点级别(1)处找到第一个匹配条目。然后,它遵循双向链表,直到找到所有匹配的条目(2),最后从表中获取每个匹配的条目(3)。

遍历B树的数据库软件

当加载几行时,B树对整体工作做出最大的贡献。只要您需要从表中获取少量行,此步骤就会带头。在任何情况下(行数很少或很多),双链表通常都是次要因素,因为它存储的值彼此相似的行彼此相邻,因此单个读取操作可以获取100条甚至更多行。关于优化>的最一般的想法是减少工作量以实现相同的目标。当涉及索引访问时,这意味着如果数据库软件不需要数据结构,则它会省略对数据结构的访问。仅索引扫描可以做到这一点:如果索引的双向链接列表中提供了所需的数据,它将忽略对表的访问。

常见的误解是索引仅对where子句有所帮助。B树索引还可以帮助排序依据,分组依据,选择和其他子句。它只是索引的B树部分(而不是双向链表),其他子句无法使用。

include子句使我们能够区分整个索引中想要的列(键列)和仅叶子节点中需要的列(include列)。这意味着如果我们不需要非叶节点,则可以从非叶节点中删除它们。

包含的列如何影响查询执行的多个方面及其使用的好处

叶节点条目的顺序未考虑包括的列。索引仅按其键列排序。这有两个结果:包含的列不能用于防止排序,也不能被认为具有唯一性。

有时在仅索引扫描或包含子句的上下文中使用术语“覆盖索引”。重要的是给定索引是否可以通过仅索引扫描来支持给定查询。该索引是否具有include子句或是否包含所有表列都无关紧要。

带有include子句的新定义具有一些优点:

该树的级别可能更少(<〜40%)

由于双向链表上方的树节点不包含include列,因此数据库可以在每个块中存储更多分支,因此树的级别可能更少。

指数略小(<〜3%)

由于树的非叶节点不包含include列,因此该索引的整体大小略小。但是,索引的叶节点级别无论如何都需要最多的空间,因此剩余节点中的潜在节省很少。

它记录了它的目的

这绝对是include子句最被低估的好处:列在索引中的原因是索引定义本身中的文档。扩展现有索引时,准确了解为什么要以定义索引的方式正确定义索引是非常重要的。您在不破坏任何其他查询的情况下更改索引的自由就是这种知识的直接结果。

   CREATE INDEX idx
    ON sales ( subsidiary_id )
     INCLUDE ( eur_value )

由于该eur_value列位于include子句中,因此它不在非叶子节点中,因此对于导航树和排序均无用。在关键部分的末尾添加新列相对安全。

   CREATE INDEX idx
    ON sales ( subsidiary_id, ts )
     INCLUDE ( eur_value )

即使对于其他查询的负面影响风险仍然很小,但通常值得承担该风险。

过滤包含的列

到目前为止,我们一直专注于include子句如何启用仅索引扫描。我们还要看看另一种情况,在索引中增加一列是有益的。

   SELECT * FROM sales
    WHERE subsidiary_id = ?
     AND notes LIKE '%search term%'

我已将搜索词设为文字值以显示前导和尾随通配符-当然,您将在代码中使用bind参数。现在,让我们考虑一下此查询的正确索引。显然,subsidiary_id必须位于第一位置。如果我们从上面获取上一个索引,则它已经满足了这一要求:

   CREATE INDEX idx
    ON sales ( subsidiary_id, ts )
     INCLUDE ( eur_value )

数据库软件可以按照开头所述的三步过程来使用该索引:(1)将使用B树查找给定子公司的第一个索引条目;(2)它将按照双向链接列表查找该子公司的所有销售额;(3)它将从表中获取所有相关的销售,删除在notes列上的相似图案不匹配的销售,并返回剩余的行。

问题是此过程的最后一步:表访问将加载行,而不知道它们是否会将其放入最终结果。通常,表访问是运行查询的全部最大贡献。加载甚至没有选择的数据都是巨大的性能。

这个特定查询的挑战在于它使用了类似in-fix的模式。普通的B树索引不支持搜索此类模式。但是,B树索引仍然支持对此类模式进行过滤。请注意重点:搜索与过滤。

换句话说,如果在双向链接列表中存在notes列,则数据库软件可以在从表中获取该行之前应用类似的模式(不是PostgreSQL,请参见下文)。如果like模式不匹配,这将阻止对表的访问。如果表中有更多列,由于select *,仍然存在表访问权限来获取满足where子句的行的那些列。

   CREATE INDEX idx
    ON sales ( subsidiary_id, ts )
     INCLUDE ( eur_value, notes )

如果表中有更多列,则索引不会启用仅索引扫描。但是,如果与相似模式匹配的行部分非常低,它可以使性能接近仅索引扫描。在相反的情况下(如果所有行都与模式匹配),由于索引大小增加,性能会稍差一些。但是,盈亏平衡点很容易达到:为了提高整体性能,类似的过滤器删除一小部分行通常就足够了。您的里程将根据所涉及列的大小而有所不同。

具有include子句的唯一索引

include子句的一个完全不同的方面:带有include子句的唯一索引仅考虑键列的唯一性。这使我们能够创建在叶节点中具有其他列的唯一索引,例如用于仅索引扫描。

   CREATE UNIQUE INDEXON … ( id )  
     INCLUDE ( payload )

该索引可防止id列中出现重复值,但它支持对下一个查询的仅索引扫描。

    SELECT payload   
      FROMWHERE id = ?  

请注意,此行为并非严格要求include子句:在唯一约束和唯一索引之间进行适当区分的数据库只需要索引,其唯一键列为最左边的列即可,其他列也可以。


-1

包含的列不构成索引键的一部分,但它们确实存在于索引上。基本上,这些值将重复。下面以示例列为例,选择两种类型的索引

创建聚集索引INDEX NC_index1 ON tableName(第1列,第1列,第1列,第4列)
创建聚集索引INDEX NC_index2 ON tableName(column1)INCLUDE(column2,column3,column4)

NC_index1更适合此类查询:

SELECT * FROM tableName WHERE column1 = x AND column1 = y AND column1 = z and column4=n

而NC_index2更适合这种查询:

SELECT column1, column2 FROM tableName WHERE column1 = a

因为sql server不允许在数据类型(例如XML,文本等)上创建索引

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.