《Pro SQL Server Internals》翻译之集群索引

本文选自《Pro SQL Server Internals

作者: Dmitri Korotkevitch

出版社: Apress

出版年: 2016-12-29

页数: 804

作者简介:Dmitri Korotkevitchis是微软SQL Server MVP和微软认证大师。作为应用程序和数据库开发人员、数据库管理员和数据库架构师,他具有多年使用SQL Server的经验。他专门从事OLTP系统在高负载下的设计、开发和性能调优。Dmitri经常在各种MicrosoftSQL PASS活动上发言,他为世界各地的客户提供SQL Server培训。

原文链接:http://www.doc88.com/p-4042504089228.html

集群索引设计注意事项

每次更改集群索引键的值时,都会发生两件事。首先,SQL Server移动行到群集索引页链和数据文件中的另一个位置。其次,它更新行id,这是群集索引键。行id存储在所有非集群索引中就需要更新。就I/O而言,这可能很昂贵,尤其是在批量更新的情况下。此外,它可以增加集群索引的碎片化,以及在行id大小增加的情况下,非集群索引的碎片化。因此,在键值不变的情况下,最好使用静态聚集索引。

所有非聚集索引都使用聚集索引键作为行id。一个太宽的聚集索引键增加非聚集索引行的大小,并需要更多空间来存储它们。因此,SQL Server在索引或范围扫描操作期间需要处理更多的数据页,这使得索引更少非常高效。

对于非惟一非聚集索引,行id也存储在非叶索引级别,反过来,也减少了每页索引记录的数量,并可能导致索引中额外的中间级别。即使非叶索引级别通常缓存在内存中,这也会引入额外的逻辑读取每次SQL Server遍历非集群索引B-树。

最后,较大的非集群索引会在缓冲池中使用更多空间,并在索引维护中带来更多开销。显然,不可能提供一个通用阈值来定义可应用于任何表的键的最大可接受大小。但是,一般来说,最好是有一个窄的聚集索引键,索引键越小越好。

将聚集索引定义为惟一的也是有益的。这很重要的原因不是显而易见的。考虑一个场景,其中一个表没有惟一的聚集索引,希望运行一个在执行计划中使用非集群索引的查询。在这种情况下,如果行id是非集的索引不是唯一的,SQL Server不知道在键期间选择哪个集群索引行查找操作。

SQL Server通过向nonunique添加另一个名为uniquifier的可空整数列来解决此类问题聚集索引。SQL Server使用NULL填充第一次出现键的uniquifiers

值,为插入到表中的每个后续副本自动递增该值。

■注意可能重复的数量每聚集索引键值是有限整数域值。使用相同的聚集索引键,不能有超过2,147,483,648行。这是一个理论极限,创建选择性如此差的索引显然不是一个好主意。

让我们看看在非惟一聚集索引中uniquifiers引入的开销。所示的代码在清单7-1中,创建了三个相同结构的不同表,并使用65,536行填充它们。表dbo.UniqueCI定义了惟一集群索引。表dbo.NonUniqueCINoDups没有任何重复的键值。最后,表dbo.NonUniqueCDups具有大量在索引中复制。

清单7 - 1。非惟一聚集索引:表创建

create table dbo.UniqueCI

(

    KeyValue int not null,

    ID int not null,

    Data char(986) null,

    VarData varchar(32) not null

    constraint DEF_UniqueCI_VarData

    default 'Data'

);

create unique clustered index IDX_UniqueCI_KeyValue

on dbo.UniqueCI(KeyValue);

create table dbo.NonUniqueCINoDups

(

    KeyValue int not null,

    ID int not null,

Data char(986) null,

    VarData varchar(32) not null

      constraint DEF_NonUniqueCINoDups_VarData

      default 'Data'

);

create /*unique*/ clustered index IDX_NonUniqueCINoDups_KeyValue

on dbo.NonUniqueCINoDups(KeyValue);

create table dbo.NonUniqueCIDups

(

 KeyValue int not null,

     ID int not null,

 Data char(986) null,

     VarData varchar(32) not null

        constraint DEF_NonUniqueCIDups_VarData

        default 'Data'

);

create /*unique*/ clustered index IDX_NonUniqueCIDups_KeyValue

on dbo.NonUniqueCIDups(KeyValue);

-- Populating data

;with N1(C) as (select 0 union all select 0) -- 2 rows

,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows

,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows

,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows

,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows

,IDs(ID) as (select row_number() over (order by (select null)) from N5)

insert into dbo.UniqueCI(KeyValue, ID)

    select ID, ID from IDs;

insert into dbo.NonUniqueCINoDups(KeyValue, ID)

    select KeyValue, ID from dbo.UniqueCI;

insert into dbo.NonUniqueCIDups(KeyValue, ID)

    select KeyValue % 10, ID from dbo.UniqueCI;

现在,让我们看看每个表的聚集索引的物理统计信息。其代码显示在清单7-2,结果如图7-1所示。

清单7 - 2。非唯一聚集索引:检查聚集索引的行大小

select index_level, page_count, min_record_size_in_bytes as [min row size]

,max_record_size_in_bytes as [max row size]

,avg_record_size_in_bytes as [avg row size]

from

    sys.dm_db_index_physical_stats(db_id(),object_id(N'dbo.UniqueCI'), 1, null ,'DETAILED');

select index_level, page_count, min_record_size_in_bytes as [min row size]

    ,max_record_size_in_bytes as [max row size]

, avg_record_size_in_bytes as [avg row size]

from

sys.dm_db_index_physical_stats(db_id(),object_id(N'dbo.NonUniqueCINoDups'), 1, null,'DETAILED');

select index_level, page_count, min_record_size_in_bytes as [min row size]

    ,max_record_size_in_bytes as [max row size]

    ,avg_record_size_in_bytes as [avg row size]

from   sys.dm_db_index_physical_stats(db_id(),object_id(N'dbo.NonUniqueCIDups'), 1, null

,'DETAILED');

7 - 1。非唯一聚集索引:聚集索引的行大小

     即使表dbo.NonUniqueCINoDups中没有重复的键值,表中还有两个添加到行的额外字节。SQL Server在数据的可变长度部分存储一个uniquifier,和这两个字节由可变长度数据偏移数组中的另一个条目添加。

在这种情况下,当集群索引具有重复值时,uniquifiers将再添加4个字节,即导致总共6字节的开销。

值得一提的是,在某些边缘情况下,uniquifier使用的额外存储空间可以减少数据页上可以容纳的行数。我们的示例演示了这种情况。你可以看到,表dbo.UniqueCI与其他两个表相比使用的数据页少了大约15%

现在,让我们看看uniquifier如何影响非集群索引。清单7-3所示的代码创建三个表中的非聚集索引。图7-2显示了这些索引的物理统计信息。

清单7。非唯一聚集索引:检查非聚集索引的行大小

create nonclustered index IDX_UniqueCI_ID

on dbo.UniqueCI(ID);

create nonclustered index IDX_NonUniqueCINoDups_ID

on dbo.NonUniqueCINoDups(ID);

create nonclustered index IDX_NonUniqueCIDups_ID

on dbo.NonUniqueCIDups(ID);

select index_level, page_count, min_record_size_in_bytes as [min row size]

   ,max_record_size_in_bytes as [max row size]

   ,avg_record_size_in_bytes as [avg row size]

from

   sys. dm_db_index_physical_stats(db_id(), object_id(N'dbo.UniqueCI'), 2, null

,'DETAILED');

select index_level, page_count, min_record_size_in_bytes as [min row size]

   ,max_record_size_in_bytes as [max row size]

   ,avg_record_size_in_bytes as [avg row size]

from

 sys. dm_db_index_physical_stats(db_id(), object_id(N'dbo.NonUniqueCINoDups'), 2, null

,'DETAILED');

select index_level, page_count, min_record_size_in_bytes as [min row size]

 ,max_record_size_in_bytes as [max row size]

 ,avg_record_size_in_bytes as [avg row size]

from

 sys. dm_db_index_physical_stats(db_id(), object_id(N'dbo.NonUniqueCIDups'), 2, null

,'DETAILED');

 

图7-2 不唯一聚集索引:非聚集索引大小

dbo.NonUniqueCINoDups表中的非聚集索引没有开销。 您可能还记得,SQL Server不会将偏移量信息存储在可变长度偏移数组中,以便跟踪列存储空数据。 尽管如此,标识符介绍八个字节在dbo.NonUniqueCIDups表中的开销。 这八个字节包括一个四字节的标识符值,两个字节的可变长度数据的偏移阵列条目,和两个字节的条目中存储的行中的可变长度的列数的。

我们可以总结的标识符的存储开销以下列方式。 对于具有标识符为空的行,如果索引至少有一个存储不为空值的可变长度列,则会产生两字节开销。该开销来自标识符列的可变长度偏移数组条目。 否则没有开销。在填充标识符的情况下,如果存在存储不为空的值的可变长度列,则开销为六个字节。 否则,开销是8个字节

■提示如果预计聚簇索引值中存在大量重复项,则可以将整数标识列作为索引的最右列添加,从而使其唯一。 与由标识符引入的不可预测的高达8字节的存储开销相比,这为每一行增加了四字节可预测的存储开销。 当您通过其所有聚簇索引列引用该行时,这还可以提高单个查找操作的性能。

以最小化插入新行导致的索引碎片的方式设计聚簇索引是有益的。 实现此目标的方法之一是使聚簇索引值不断增加。 标识列上的索引就是一个这样的例子。 另一个示例是使用插入时的当前系统时间填充的日期时间列。

然而,不断增加的指数存在两个潜在的问题。 第一个涉及统计。 正如您在第3章中学到的,当直方图中不存在参数值时,SQL Server中的遗留基数估计器会低估基数。 您应该将此类行为纳入系统的统计信息维护策略,除非您使用新的SQL Server 2014-2016基数估算器,该估算器假定直方图之外的数据具有与表中其他数据类似的分布。

下一个问题更复杂。 随着索引的不断增加,数据总是插入到索引的末尾。 一方面,它可以防止页面拆分并减少碎片。 另一方面,它可能导致热点,这是多个会话尝试修改同一数据页和/或分配新页面或扩展区时发生的序列化延迟SQL Server不允许多个会话更新相同的数据结构,而是序列化这些操作。

除非系统以非常高的速率收集数据并且索引每秒处理数百个插入,否则热点通常不是问题。 我们将在第27章“系统故障排除”中讨论如何检测此类问题。

最后,如果系统具有一组频繁执行且重要的查询,则考虑聚合索引可能是有益的,这会优化它们。 这消除了昂贵的密钥查找操作并提高了系统的性能。

即使可以使用覆盖非聚集索引来优化此类查询,但它并不总是理想的解决方案。 在某些情况下,它需要您创建非常宽的非聚集索引,这将占用磁盘和缓冲池中的大量存储空间。

另一个重要因素是修改列的频率。 将经常修改的列添加到非聚集索引需要SQL Server在多个位置更改数据,这会对系统的更新性能产生负面影响并增加阻塞。

尽管如此,并不总是能够设计满足所有这些准则的聚集索引。 此外,您不应将这些指南视为绝对要求。 您应该分析系统,业务需求,工作负载和查询,并选择有益于您的聚集索引,即使它们违反了某些准则。

标识、序列和标识符

人们通常选择身份,序列和唯一标识符作聚集索引键。 与往常一样,这种方法有其自身的优缺点。

在此类列上定义的聚集索引是唯一的,静态的和窄的。 此外,身份和序列不断增加,这减少了索引碎片。 其中一个理想的用例是目录实体表。 作为示例,您可以考虑存储客户,文章或设备列表的表。 这些表存储数千甚至数百万行,尽管数据相对静态,因此热点不是问题。 此外,这些表通常由外键引用并用于连接。 整型或bigint型列上的索引非常紧凑和高效,这将提高查询的性能。

■注意我们将在第8章“约束”中更详细地讨论外键约束。

在事务表的情况下,身份或序列列上的聚集索引效率较低,事务表由于它们引入的潜在热点而以非常高的速率收集大量数据。

另一方面,标识符号很少是集群和非集群索引的理想选择。 使用NEWID()函数生成的随机值极大地增加了索引碎片。 此外,标识符号上的索引会降低批处理操作的性能。 让我们看一个示例并创建两个表:一个表在标识列上有聚集索引,另一个在标识符号列上有聚集索引。 在下一步中,我们将在两个表中插入65,536行。 您可以在清单7-4中看到执行此操作的代码。

清单7-4  标识符:表创建

(

 ID int not null identity(1,1),

 Val int not null,

 Placeholder char(100) null

);

create unique clustered index IDX_IdentityCI_ID

on dbo.IdentityCI(ID);

create table dbo.UniqueidentifierCI

(

 ID uniqueidentifier not null

 constraint DEF_UniqueidentifierCI_ID

 default newid(),

 Val int not null,

 Placeholder char(100) null,

);

create unique clustered index IDX_UniqueidentifierCI_ID

on dbo.UniqueidentifierCI(ID)

go

;with N1(C) as (select 0 union all select 0) -- 2 rows

,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows

,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows

,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows

,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows

,IDs(ID) as (select row_number() over (order by (select null)) from N5)

insert into dbo.IdentityCI(Val)

 select ID from IDs;

;with N1(C) as (select 0 union all select 0) -- 2 rows

,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows

,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows

,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows

,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows

,IDs(ID) as (select row_number() over (order by (select null)) from N5)

insert into dbo.UniqueidentifierCI(Val)

 select ID from IDs;

我的计算机上的执行时间和读取次数如表7-1所示。 图7-3显示了两个查询的执行计划。

7-1 将数据插入表:执行统计

 

7-3 将数据插入表中:执行计划

如您所见,标识符列上的索引有另一个排序运算符。 SQL Server在插入之前对随机生成的标识符值进行排序,这会降低查询的性能。

让我们在表中插入另一批行并检查索引碎片。 执行此操作的代码如清单7-5所示。 图7-4显示了查询的结果。

清单7-5 标识符:插入行并检查碎片

;with N1(C) as (select 0 union all select 0) -- 2 rows

,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows

,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows

,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows

,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows

,IDs(ID) as (select row_number() over (order by (select null)) from N5)

insert into dbo.IdentityCI(Val)

 select ID from IDs;

;with N1(C) as (select 0 union all select 0) -- 2 rows

,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows

,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows

,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows

,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows

,IDs(ID) as (select row_number() over (order by (select null)) from N5)

insert into dbo.UniqueidentifierCI(Val)

 select ID from IDs;

select page_count, avg_page_space_used_in_percent, avg_fragmentation_in_percent

from sys.dm_db_index_physical_stats(db_id(),object_id(N'dbo.IdentityCI'),1,null,'DETAILED');

select page_count, avg_page_space_used_in_percent, avg_fragmentation_in_percent

from sys.dm_db_index_physical_stats(db_id(),object_id(N'dbo.UniqueidentifierCI'),1,null

,'DETAILED');

insert into dbo.UniqueidentifierCI(Val)

 select ID from IDs;

select page_count, avg_page_space_used_in_percent, avg_fragmentation_in_percent

from sys.dm_db_index_physical_stats(db_id(),object_id(N'dbo.IdentityCI'),1,null,'DETAILED');

select page_count, avg_page_space_used_in_percent, avg_fragmentation_in_percent

from sys.dm_db_index_physical_stats(db_id(),object_id(N'dbo.UniqueidentifierCI'),1,null

,'DETAILED');

7-4。 索引碎片

如您所见,uniqueidentifier列上的索引严重碎片化,使用大约40

与标识列上的索引相比,数据页面的百分比更多。

uniqueidentifier列的索引中的批量插入会在数据文件的不同位置插入数据,这会导致在大型表的情况下出现繁重的随机物理I / O.这会显着降低操作的性能。

个人经验

前段时间,我参与了一个系统的优化,该系统具有250 GB的表,其中包含一个聚簇索引和三个非聚簇索引。 其中一个非聚簇索引是uniqueidentifier列上的索引。 通过删除此索引,我们能够将50,000行的批量插入从45秒加速到7秒。

当您想要在uniqueidentifier列上创建索引时,有两种常见用例。 第一个是支持跨多个数据库的值的唯一性。考虑一个可以将行插入每个数据库的分布式系统。开发人员通常使用uniqueidentifier来确保每个键值在系统范围内都是唯一的。

此类实现中的关键元素是如何生成键值。 正如您已经看到的,使用NEWID()函数或客户端代码生成的随机值会对系统性能产生负面影响。但是,您可以使用NEWSEQUENTIALID()函数,该函数生成唯一且通常增加的值(SQL Server能够随时重置它们的值)。 使用NEWSEQUENTIALID()函数生成的uniqueidentifier列的索引类似于identitysequence列的索引; 但是,您应该记住,uniqueidentifier数据类型使用16字节的存储空间,而4字节的int8字节的bigint数据类型。

作为替代解决方案,您可以考虑创建一个包含两列的复合索引(Installation IdUnique_Id_Within Installation)。这两列的组合保证了多个安装和数据库的唯一性,并且使用的存储空间比uniqueidentifier更少。您可以使用整数 用于生成Unique_Id_Within_Installation值的标识或序列,这将减少索引的碎片。

如果需要在数据库中的所有实体上生成唯一键值,则可以考虑在所有实体中使用单个序列对象。 此方法满足要求,但使用比uniqueidentifier更小的数据类型。

另一个常见用例是安全性,其中uniqueidentifier值用作安全性令牌或随机对象ID。 不幸的是,您无法在此方案中使用NEWSEQUENTIALID()函数,因为可以猜测该函数返回的下一个值。

在这种情况下,一种可能的改进是使用CHECKSUM()函数创建计算列,然后对其进行索引,而不在uniqueidentifier列上创建索引。 代码如清单7-6所示。

清单7-6。 使用CHECKSUM():表结构

create table dbo.Articles

(

 ArticleId int not null identity(1,1),

 ExternalId uniqueidentifier not null

 constraint DEF_Articles_ExternalId

 default newid(),

ExternalIdCheckSum as checksum(ExternalId),

 /* Other Columns */

);

create unique clustered index IDX_Articles_ArticleId

on dbo.Articles(ArticleId);

create nonclustered index IDX_Articles_ExternalIdCheckSum

on dbo.Articles(ExternalIdCheckSum);

提示:可以索引计算列而不保留它。

尽管IDX Articles ExternalId CheckSum索引将会严重碎片化,但与uniqueidentifier列上的索引(4字节密钥与16字节)相比,它将更加紧凑。 它还提高了批处理操作的性能,因为更快的排序,这也需要更少的内存来进行。

您必须记住的一件事是CHECKSUM()函数的结果不保证是唯一的。 您应该在查询中包含两个谓词,如清单7-7所示。

清单7-7。 使用CHECKSUM():选择数据

select ArticleId /* Other Columns */

from dbo.Articles

where checksum(@ExternalId) = ExternalIdCheckSum and ExternalId = @ExternalId

注意:在需要索引大于900 / 1,700字节的字符串列的情况下,可以使用相同的技术,这是非聚簇索引键的最大大小。 即使这样的索引不支持范围扫描操作,它也可以用于点查找。

非聚集索引设计注意事项

当单非聚簇索引查找和键查找操作时,很难找到连接多个非聚簇索引比使用更有效的转折点。当索引选择性很高并且SQL Server估计索引查找操作将返回少量行时,键查找成本将相对较低。 在这种情况下,没有理由使用另一个非聚集索引。或者,当索引选择性较低时,索引查找会返回大量行,而SQL Server通常不会使用它,因为效率不高。

让我们看一个示例,我们将创建一个表并用1,048,576行填充它。 Col1在列中存储50个不同的值,Col2存储150个值,Col3存储200个值。 最后,我们将在表上创建三个不同的非聚簇索引。 执行此操作的代码如清单7-8所示。

清单7-8。 多个非聚簇索引:表创建

create table dbo.IndexIntersection

(

 Id int not null,

 Placeholder char(100),

 Col1 int not null,

 Col2 int not null,

 Col3 int not null

);

on dbo.IndexIntersection(ID);

;with N1(C) as (select 0 union all select 0) -- 2 rows

,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows

,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows

,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows

,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows

,N6(C) as (select 0 from N3 as T1 cross join N5 as T2) -- 1,048,576 rows

,IDs(ID) as (select row_number() over (order by (select null)) from N6)

insert into dbo.IndexIntersection(ID, Col1, Col2, Col3)

 select ID, ID % 50, ID % 150, ID % 200 from IDs;

create nonclustered index IDX_IndexIntersection_Col1

on dbo.IndexIntersection(Col1);

create nonclustered index IDX_IndexIntersection_Col2

on dbo.IndexIntersection(Col2);

create nonclustered index IDX_IndexIntersection_Col3

on dbo.IndexIntersection(Col3); 

猜你喜欢

转载自www.cnblogs.com/linyanfang1/p/10107640.html