第七章——设计和调整索引

  要定义一个在任何地方都有效的索引策略是不可能的。因为每个系统都是独一无二的,都需要基于工作量的索引方法,业务需求和其他的一些因素。然而,有一些设计方面的考虑和准则,可以适用于每一个系统。  

当我们要优化现有系统时,也是这样。而优化是一个迭代过程,每个案例中都是独一无二的,有一系列技术可以用来检测数据库系统每个案例中的低效率的例子。

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

分类索引设计考虑因素

每次更改聚集索引键的值,都会发生两件事。第一件事就是sql服务器在聚集索引页链和数据文件中把行移动到不同位置。第二,它更新聚集索引键的行号。所有非聚集索引都要存储、更新行标识。但就输入/输出端口而言,在批量更新的情况下会特别很贵。而且,在行id大小增加的情况下,它会增加群集索引和非群集索引的碎片化。因此,最好是有一个静态的聚集索引,因为它关键值不会改变。

所有非聚集索引都使用聚集索引键作为行id。太宽泛的聚集索引键会增加非聚集索引行的大小,而且需要更多空间来存储它们。结果就是sql服务器在索引或范围扫描操作中需要处理更多的数据页,会索引降低效率。在非唯一非聚集索引的情况下,行id也存储在非叶索引级别,反过来,减少每页索引记录的数量,会导致索引中额外的中间级别。即使非叶索引级别通常缓存在内存中,这也导致了每次sql服务器遍历非聚集索引b树会有额外的逻辑读取。

最后一点,较大的非聚集索引消耗缓冲池中更多的空间,还会在索引维护期间使用更多的开销。显然,不可能用通用阈值,是指用于任何表的的键可接受的最大值。但是,作为通用规则,最好有一个索引键尽可能小的聚集索引键。

将群集索引定义为唯一值也是有益的。考虑一下这样的情况,表没有唯一的聚集索引,而您希望在执行计划中运行一个使用非聚集索引搜索的查询。在这种情况下,如果非聚集索引中的行id不是唯一的,sql服务器就不知道选择哪个聚集索引行来进行密钥查找操作。

sql服务器通过为非唯一聚集索引新增一个叫做统一词组的可以为空的整数列来解决这些问题。sql服务器为首次出现的键值填充了空单元,并且为插入到表中的每个后续副本自动递增。

笔记:每组集索引键值可能重复的次数受到整数域值的限制。您不能有多于2,147,483,648行的相同聚集索引键。这是一个理论上的限制,显然创造如此低的选择性的索引是一个错误的想法。

让我们来看看在非唯一的聚集索引中由集合符引入的开销,表7-1中显示的代码创建了三个相同结构的不同表,每个表填充了65,536行。SQL查询语句:Table dbo.UniqueCI 是唯一一张有定义唯一聚集索引的表。 Table dbo.NonUniqueCINoDups没有重复的键值。最后,table dbo.NonUniqueCDups在索引中有大量的重复。

现在,让我们看看每个表的聚集索引的物理统计。这方面的代码如

7-2,结果在图7-1中。

即使dbo.NonUniqueCINoDups 表中没有重复的键值,仍然有两个添加到行中的额外字节。sql服务器在数据的可变长度部分存储一个全局唯一标识符,而这两个字节是由可变长度数据偏移数组中的另一个记录添加的。

在这种情况下,如果集群索引具有重复的值,则全局唯一标识符将添加另外四个字节,从而使总开销达到6个字节。

值得一提的是,在一些极端情况下,全局唯一标识符使用的额外存储空间可以减少可以适合数据页的行数。我们的例子说明了这种情况。正如你所看到的,dbo.laxieci使用的数据页比其他两个表少15%。现在,让我们来看看全局唯一标识符如何影响非聚集索引。表7-3的代码在三个表中都创建了非聚集索引。图7-2是这些索引的物理统计。

dbo.非独立的函数表中的非聚集索引中没有开销。你应该记得,sql服务器不为冗余列存储空数据在可变长偏移数组中存储偏移量信息。

尽管如此,全局唯一标识符还是在dbo.非独立配置表中引入了8个字节的开销。这八个字节由四字节全局变量符的值和两字节可变长度数据偏移数组记录,以及两字节存储行中可变长列数的记录组成。

我们可以用下面的方法来总结这个唯一标识符的存储开销。对于包含一个空唯一标识符的行,如果索引至少有一个存储非空值的可变长列,则有两个字节的开销。开销来自单位列的可变长度偏移数组项。除此以外没有其他开销了。在唯一标识符被填充的情况下,如果可变长列存储非空值,则开销为6个字节,否则为8字节。

如果您希望在群集索引值中有大量重复,则可以添加整数标识列作为索引的最右边列,从而使其唯一。这将给每行增加一个四字节的可预测存储开销,而唯一标识符引入了一个不可预测的高达八字节的存储开销。这也可以改善单个查找操作的性能,当你通过所有的聚集索引列引用该行。

在设计聚集索引时,最好尽量减少插入新行造成的索引碎片。实现这一目标的其中一个方法是使聚集的索引值不断增加。标识上的索引就是这样一个例子。另一个例子是记录插入时填充了当前系统时间的日期时间的列。然而,随着指针不断地增加,有两个潜在的问题。第一个问题与统计有关。你在第三章学过,当直方图中没有参数值时,sql服务器中的传统基数估计器对基数估计不足。你应该在系统的统计维护策略中考虑这种行为,除非您使用的是2014-2016年新的sql服务器的基数估计,它能假设直方图之外的数据具有与表中其他数据相似的分布。

下一个问题更复杂。在索引不断增加的情况下,数据总是插入到索引的末尾。一方面,它可以防止页面拆分、减少碎片,另一方面,它会置于危险,就是序列化延迟,当多个会话试图修改同一数据页和/或分配新页面或区域时会发生序列化延迟。sql服务器不允许多个会话更新相同的数据结构,只能进行序列化操作。

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

最后,如果一个系统有一组需要经常执行的重要查询语句,最好是考虑群集索引,因为它可以优化这些索引。群集索引能消除高消耗的查找键操作,提高了系统的性能。

虽然可以通过覆盖非聚集索引优化此类查询,但它并不是在每个情况下都很理想的解决方案。有时候它需要创建非常宽泛的非聚集索引,这样会消耗磁盘和缓冲池中的大量存储空间。

另一个重要因素是修改列的频率。把经常修改的列添加到非聚集索引中,需要sql服务器在多个地方更改数据,这样会对系统的更新性能产生负面影响而且增加阻塞。

尽管如此,不是每次都能设计出能够满足所有要求的集群索引的准则。此外,不要把这些准则当做绝对的要求。你要分析系统,业务需求,工作量和查询,然后选择对您有利的集群索引,即使你的选择会违反准则。

标识、序列和全局唯一标识符

我们经常用标识、序列和单元素标识符作为聚集索引键。和其他情况一样,这种做法有它自己的利弊。

定义在此类列上的聚集索引是唯一的、静态的、狭窄的。此外,标识和序列不断增加可以减少指指针碎片。

理想用例之一是目录实体表。可以把存储了客户、文章或设备的表当成示例。这些表存储了几千行,甚至几百万行,虽然数据相对是静态的,但热点不是问题。另外,这类表格通常用外键连接.非常简洁有效的整数或大整数列的索引能提高查询的性能。

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

在事务表下,标识列或序列列上的聚集索引的效率比较低,它们高速收集大量数据,所以引入了潜在的热点。Uniqueidentifiers,在另一方面,一般不用唯一标识符作为聚集或非聚集索引的对象。用newid()函数生成的随机值大大增加了索引碎片。另外,唯一标识符降低了批量操作的性能。我们看一个示例来创建两个表:一个在标识列上有聚集索引,另一个在统一标识列上有聚集索引。在下一步,我们将在两个表中插入65,536行记录。您可以在表7-4中看到步骤的代码。

在我电脑上的执行时间和读取次数列在表7-1上。图7-3显示了这两个查询的执行计划。

如图所示,在唯一标识列上的索引中还有另一个排序操作符。sql服务器在插入记录前对随机生成的单一标识值进行排序会降低查询的性能。把另一批行插入表中,并检查索引碎片。步骤代码在清单7-5中。图7-4显示查询的结果。

如图所示,单一标识列上的索引非常分散,用超过40%的数据页和标识页的指针对比。

批量处理插入单一标识符列的索引,会在文件的多个位置插入数据,如果有大量表格,就会导致过重、随机物理输入输出流。这样会大大降低操作的性能。

个人经验

以前,我参与了一个系统的优化,该系统有一个250gb表,其中有一个集群索引和三个非集群索引。其中一个非聚集的索引是单一标识列上的索引。删除这个索引以后,我们可以把50,000行的批插入从45秒加速到7秒。

有两个常见的情况,用与你在单一标识列上创建索引的时候。第一个是支持多个数据库中值的唯一性。想象一个可以将行插入到每个数据库中的分布式系统。开发人员经常使用单一标识符来确保每个关键值都是唯一的系统范围。

实现的关键要素是如何生成关键值。如你所见,用newid()函数或客户端代码生成的随机值会对系统性能产生负面影响。但是,您可以使用新序列函数id: NEWSEQUENTIALID() ,它生成唯一的通用的递增值(sql服务器时不时地重新设置它的基本值)。使用电子序列EWSEQUENTIALID() 函数生成的单列标识列的索引类似于标识列和序列列的索引;但是,你要记住,单位标识器数据类型使用了16字节的存储空间,而INT类型只用4字节,bigint类型用8字节。

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

如果需要在数据库中的所有实体中生成唯一的键值,可以考虑在所有实体中使用单个序列对象。这个方法满足要求,但用的数据类型要比单一标识符小。另一个常见的例子是安全性,其中一个单位标识符值用作安全性令牌或随机对象id。遗憾的是,在这个例子中不能用新序列函数NEWSEQUENTIALID(),因为可以猜出该函数返回的下一个值。一个可行的改进方案是使用检查函数CHECKSUM()创建一个计算过的列,不用再单位标识列上创建索引,而是在此之后将其索引。代码如7-6

技巧:您可以索引计算过的列,就不用把它永久化。

即使 IDX_Articles_ExternalIdCheckSum 的指针会严重碎片化,它与单字节标识栏上的索引(4字节的键比16字节)相比,它会更加紧凑。因为排序速度更快也提高了批处理操作的性能,可以用更少的内存来进行。必须记住的一点,校验和函数 CHECKSUM()的结果不能保证是唯一的。要把预测都包含到查询中。如表7-7.

当你需要索引大于900/1,700字节(非聚集索引键的最大大小)的字符串列时,可以使用同样的技术,虽然这样的索引不支持范围扫描操作,它也可以用于点查找。

猜你喜欢

转载自www.cnblogs.com/xxnzmy/p/10106033.html