十五周翻译《Pro SQL Server Internals, 2nd edition》 CHAPTER 7

文章来自《Pro SQL Server Internals, 2nd edition》的CHAPTER 7 Designing and Tuning the Indexes中的Clustered Index Design Considerations

原作者:Dmitri Korotkevitch

第七章  设计和调整索引

无法定义可在任何地方使用的索引策略。 每个系统都是独特的,需要它自己的索引方法基于工作量,业务需求和其他一些因素。但是,有几个设计考虑因素和指南可以应用于每个系统。

当我们优化现有系统时也是如此。 虽然优化是一个迭代过程在每种情况下都是独一无二的,有一套技术可用于检测每种情况下的低效率数据库系统。

     在本章中,我们将介绍在设计时需要牢记的几个重要因素新索引和优化现有系统。

聚集索引设计注意事项

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

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

     在非唯一非聚簇索引的情况下,row-id也存储在非叶索引级别,反过来,减少了每页索引记录的数量,并可能导致索引中的额外中间级别。尽管非叶索引级别通常缓存在内存中,但这会引入额外的逻辑读取每次SQL Server遍历非聚集索引B-Tree时。

最后,较大的非聚簇索引在缓冲池中使用更多空间并引入更多开销在索引维护期间。 显然,不可能提供定义的通用阈值可应用于任何表的密钥的最大可接受大小。 但是,作为一般规则,最好是具有窄聚簇索引键,索引键尽可能小。

定义唯一的聚簇索引也是有好处的。 这个原因重要但不明显。 考虑到一种情况,表没有唯一的聚簇索引,你希望在执行计划中运行使用非集群索引查找的查询。 在这种情况下,如果非聚簇索引中的行编号得到的就不是唯一的,SQL Server就不知道在键查找操作期间要选择哪个聚簇索引行。

SQL Server通过向非唯一聚簇索引中添加另一个名为uniquifier的可空整数列的操作来解决此类问题。对于第一次出现的键值,SQL Server用NULL填充uniquifiers,并对插入到表中的后续重复值进行自动递增操作。 

注意:每个聚簇索引键值的可能重复项数量受整数域值的限制。使用相同的聚集索引键的行不能超过2,147,483,648。 这是理论上的限制,创建一个选择性这么差的索引显然是个不好的方式。

让我们看看在非唯一聚簇索引中引入uniquifiers的开销。列表7-1所示的代码创建了三个相同结构的不同表,每个表都填充了65,536行。dbo表。表dbo.UniqueCI是唯一定义了唯一聚簇索引的表。表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-3 非唯一聚集索引:检查非聚集索引的行大小

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不会将偏移量信息存储在NULL数据的尾随列的可变长度偏移数组中。尽管如此,uniquifier在dbo.NonUniqueCIDups表中也引入了8个字节的开销。 这八个字节由一个4字节的unquifier值,一个双字节的可变长度数据偏移数组条目和一个存储行中可变长度列数的双字节条目组成。

我们可以通过以下方式总结uniquifier的存储开销。 对于具有uniquifier但为NULL的行,如果索引中至少有一个存储NOT NULL值的可变长度列,那就会产生两个字节的开销。 该开销来自uniquifier列的可变长度偏移数组条目。否则没有开销。

在填充uniquifier的情况下,如果存在存储非空值的可变长度列,则开销为6个字节。否则,开销是8个字节。

提示:如果预计聚簇索引值中存在大量重复项,可以将整数标识列作为最右边的列添加到索引中,从而实现唯一性。 与uniquifiers引入的最多8字节的不可预测存储开销相比,这将为每行增加一个4字节的可预测存储开销。当你通过其该行的聚簇索引列使用该行时,这还可以提高单个查找操作的性能。

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

然而,索引不断增加会存在两个潜在问题。第一个与统计有关。就像你在第3章中了解到的,当直方图中没有参数值时,SQL Server中的遗留基数估计器会低估基数。你应该将这种行为考虑到系统的统计维护策略中,除非你使用新的SQL Server 2014-2016基数估计值,该估计值假定在直方图之外的数据具有类似于表中其他数据的分布。

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

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

最后,如果系统有一组频繁执行且重要的查询,就要考虑到聚簇索引这时候是有好处的,它会优化它们。这解决了昂贵的密钥查找操作花销并提高了系统的性能。

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

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

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

身份,序列和唯一标识符

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

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

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

在事务性表中,身份或序列列上的聚集索引效率较低,事务性表以非常高的速率收集大量数据,这是由于它们引入的潜在热点造成的。

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

列表7-4 Uniqueidentifiers:表创建

create table dbo.IdentityCI

(

 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-3。 将数据插入表中:执行计划

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

 如您所见,uniqueidentifier列上的索引有另一个排序运算符。SQL Server在插入之前对随机生成的uniqueidentifier值进行排序,从而减少了查询的性能。让我们在表中插入另一批行并检查索引碎片。 这样做的代码

如清单7-5所示。 图7-4显示了查询的结果。

;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'); 

  

图7-4。 索引碎片

正如你所看到的,uniqueidentifier列上的索引严重碎片化,与标识列上的索引相比,它使用的数据页大约多40%。

在uniqueidentifier列的索引中的批量插入会变成在数据文件的不同位置插入数据,当表太大这会出现繁重的随机物理I / O. 这可能会降低操作性能。

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

当你想要在uniqueidentifier列上创建索引时,有两种常见方法。 第一个是支持跨多个数据库的值保证唯一性。其中行可以插入到分布式系统的每个数据库中。开发人员经常使用uniqueidentifiers来确保每个键值在系统范围内都是唯一的。

像这种实现的关键元素是如何生成键值。就像你已经看到的,NEWID()函数或客户机代码中生成的随机值会对系统性能产生负面影响。但是,你可以使用NEWSEQUENTIALID()函数,该函数会生成递增的唯一的值(SQL Server不时重置它们的基值)。使用NEWSEQUENTIALID()函数生成的uniqueidentifier列上的索引类似于标识列和序列列上的索引;但是,你要记住,uniqueidentifier数据类型使用16字节的存储空间,而4字节的int或8字节的bigint数据类型。 

作为替代解决方案,你也可以创建两列的复合索引(InstallationId,Unique_Id_Within_Installation)。 两列的组合保证了多个安装和数据库的唯一性,并且比独特标识符使用更少的存储空间。 你可以使用整数标识或序列来生成Unique_Id_Within_Installation值,这将减少索引的碎片。 

在需要跨数据库后所有实体生成唯一键值的情况下,可以考虑跨所有实体使用单个sequence对象。这种方法可以满足需求,但是使用到比唯一标识符更小的数据类型。

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

在这种情况下,一种可能的改进是不在uniqueidentifier列上创建索引,而是使用CHECKSUM()函数创建计算列,然后对其进行索引。 代码如列表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_ExternalIdCheckSum索引非常分散,但与uniqueidentifier列上的索引(4字节密钥与16字节)相比,它更紧凑。 它还提高了批处理操作的性能,因为更快的排序,所以也需要更小的内存来进行。

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

列表7-7 使用CHECKSUM():选择数据

select ArticleId /* Other Columns */
from dbo.Articles
where checksum(@ExternalId) = ExternalIdCheckSum and ExternalId = @ExternalId   

提示     如果需要索引大于的字符串列,则可以使用相同的技术900/1700字节,这是非聚簇索引键的最大大小。 即使这样的指数不会
支持范围扫描操作,它可以用于点查找。

猜你喜欢

转载自www.cnblogs.com/jiangfan123/p/10111061.html
今日推荐