原文链接:传送门。
第十章节我们分析了索引的内部结构。有了这些关于索引结构的知识,我们便可以分析索引碎片了:其产生的原因,如何防止,以及何时可以不去关注它们。
一些背景知识 / 复习
以下知识对于理解索引碎片来说是至关重要的,有些知识在之前的章节中都已经出现过,比如使用索引来返回数据的场景。在本节,我们的场景是索引的维护,因此,一些旧的知识在此会重复出现,而同时会增加一些新的知识。
每张表要么是个堆要么是个聚集索引。
如果表是个堆,那么其非聚集索引的书签部分便是一个行标识符(RID),如果表是一个聚集索引,那么非聚集索引的书签部分便是其聚集索引的索引键值。
索引条目存储在页中,8个物理上连续的页组成一个区。
索引层级从底部,从0开始计算,因此叶子层级总是第0层,而最低的中间层节点总是第1层,根页总是自己成为一个层级,并且它具有最高的层级数。
扫描整个索引意味着叶子层级的所有页都会被读且仅会被读一次。每一个页包含了一个指向上一个和下一个页的指针。页的逻辑顺序和物理顺序或许是不一致的。指向下一个页的指针后续会指向位于同一个区的页,或者指向几百个页之前的一个页(也可能是之后),甚至于指向不同文件的一个页。
页的物理顺序和逻辑顺序越一致,扫描整个索引所需要的IO数就越少。当两个顺序一致的时候,SQL SERVER每次IO可读取一整个区或者更多。同样的,页的物理顺序和逻辑顺序越相关,那么使用的预读数便越多(预读:在使用之前,先将页存入内存)。不管是扫描整个索引,还是只扫描索引的一部分,这都是确定无疑的。
为了返回一个索引条目,从根节点开始,每个层级的一个页都必须被访问到。这种操作的性能不会被索引物理顺序和逻辑顺序的相关性所影响。
记住了这些信息,我们来检查索引碎片这个主题。
什么是碎片?
索引碎片来自于两种情形:内部碎片和外部碎片。决定索引碎片(不管是内部碎片或者是外部碎片)的最好工具是 sys.dm_db_index_physical_stats这个动态管理函数。因为这个函数显示的是索引ID,而不是索引的名称,查询常常将它与视图sys.indexes 连接起来,从而在输出中包含索引名称,本章节我们将使用这个函数,首先我们梳理下内部碎片,然后再陈述下外部碎片。
内部碎片
每个页都会包含一定数量的索引条目,那并不意味着每个页总是包含最大数量的条目数。为了本章后续会讲到的一些理由,通常一个索引页并不总是完全充满的。当我们说一个索引包含内部碎片时,我们就意味着这个页并不是完全充满的。在一个索引中,每页占据的空间的数量体现了索引的内部碎片,同时也体现了一个索引的平均页覆盖率。注意覆盖率测量得越高,内部碎片便越少。一个100%充满的页是每有内部碎片的。内部碎片通常以一个百分比的形式被报告出来,并且其体现了字节数上的覆盖率而不是条目上的覆盖率。因此一个覆盖率是96%的索引页或许是完全充满的。那就是说,4%的页空间或许不是足够的空间用来添加一个新的条目。当页的空间被头信息及页的偏移指针占据的时候,一个各个条目相对比较大的页或许在90%,80%,甚至更少的时候就会充满。
sys.dm_db_index_physical_stats函数在它的avg_page_space_used_in_percent输出列报告了索引的内部碎片。显示在列表1的查询,它检查了SalesOrderDetail表的聚集索引,显示了其内部碎片信息。
SELECT IX.name AS 'Name', PS.index_level AS 'Level', PS.page_count AS 'Pages', PS.avg_page_space_used_in_percent AS 'Page Fullness (%)'FROM sys.dm_db_index_physical_stats( DB_ID(), OBJECT_ID('Sales.SalesOrderDetail'), DEFAULT, DEFAULT, 'DETAILED') PSJOIN sys.indexes IXON IX.OBJECT_ID = PS.OBJECT_ID AND IX.index_id = PS.index_id WHERE IX.name = 'PK_SalesOrderDetail_SalesOrderID_SalesOrderDetailID'; GO
列表1: 一个返回内部碎片信息的查询
这个查询对于索引的每个层级都会返回一行输出,如图表1中所显示的。
图表1:内部碎片查询的输出
这个特定查询的主题是聚集索引,因此索引的页层级是表的数据行,第0层的输出,也就是以上输出的第一行,告诉我们表的数据行分散在1234个页中,其平均充满率是99%。换句话说,这个表具有很小数量的内部碎片。
外部碎片
与内部碎片相对的,外部碎片指的是一个索引逻辑顺序与物理顺序的不一致。它同样被报告为一个百分比。引用微软技术网的原文,它指的是一个索引的叶子节点中无序页(out-of-order pages)的百分比,所谓的无序页指的是如果当前页物理上分配的紧邻的页不是偏移指针所指向的页。那么当前页便称之为无序页。虽然官方定义将定义本身限制在叶子层级,你将注意到 sys.dm_db_index_physical_stats函数能够返回索引所有层级的碎片信息。
我们注意到,与内部碎片不同的是,一个高的数值意味着更大数量的外部碎片,因此一个具有100%外部碎片的索引几乎完全是外部碎片,也就是说,它的逻辑顺序与物理顺序完全没有任何关联性。
考虑图片2演示的一个简单的例子。一个16个页的索引占据了起始于数据库文件40页和56页的区。除了每个区的最后一个页,任何一个页的下一页指针指向区中的下一页。而第一个区的最后一页指向第二个区的第一页。第二个区的最后一页没有指向任何页,因为其是索引的最后一个页。
在这个例子中,无序页的数量是0,虽然第8,9页在文件中是物理上不连续的,它们在分配给索引的空间内是连续的,因此这个例子的外部碎片是0。
图2: 没有外部碎片的索引
在另一方面,图3展示了一样的表,但是具有一些无序页,前三个页逻辑上是连续的,但是在那之后其逻辑顺序和物理顺序便没有任何相关性。
图3:具有外部碎片的索引
任何下一页指针如果没有指向索引的物理上分配的下一个页的话,便是一个无序指针,不管其向前指,向后指,在区内,跨区,甚至于跨越整个文件。
为了演示如何获取一个索引的外部碎片信息,我们将之前的查询做两处改动。因为外部索引涉及了页之间的关系,而不是页的内容,它能够被扫描索引的第一层定义出来,而不用扫描大得多的索引的叶子层级。我们将函数的最后一个参数从DETAILED 修改为LIMITED (or DEFAULT),从而告知SQL SERVER叶子节点扫描不是必要的。
同时我们修改SELECT子句包含一些稍微不同的列集,它们能够提供索引的外部碎片信息,最终修改后的查询显示如下:
SELECT IX.name AS 'Name' , PS.index_level AS 'Level' , PS.page_count AS 'Pages' , PS.avg_fragmentation_in_percent AS 'External Fragmentation (%)' , PS.fragment_count AS 'Fragments' , PS.avg_fragment_size_in_pages AS 'Avg Fragment Size' FROM sys.dm_db_index_physical_stats( DB_ID(), OBJECT_ID('Sales.SalesOrderDetail'), DEFAULT, DEFAULT, 'LIMITED') PS JOIN sys.indexes IX ON IX.OBJECT_ID = PS.OBJECT_ID AND IX.index_id = PS.index_id WHERE IX.name = 'PK_SalesOrderDetail_SalesOrderID_SalesOrderDetailID'; GO
因为我们给最后一个参数指定了LIMITED,我们将得到一行输出,如图4所示。
图4:外部碎片查询输出
这个结果告诉我们这个索引的大小是1234页,包含了20个碎片,平均每个碎片的大小是61.7页。如同砍一块木头导致两块,砍两下导致3块,一个无序的下一页指针导致两个碎片,两个无序指针导致三个碎片,等等。因此我们的索引含有20-1 =19个页包含无序指针,平均下来,当以逻辑数据扫描这个索引的时候,在各个无序页之间61个连续的页会发生。这是一个包含很少外部碎片的索引。
SSMS的索属性窗口,显示在图5,通过执行一个与我们之前执行的查询类似的查询来获取它的信息,其Page fullness和Total fragmentation分别指的是索引的内部碎片和外部碎片。
图5:SSMS的索引属性窗口
本节我们介绍了索引碎片的基础知,下一节我们将深入讨论索引碎片,包括其如何产生,如何防止,以及一些通用的实践模式。
To be continued...