SQL Server索引进阶第十一篇:索引碎片分析与解决(中)-碎片发生原理深度剖析
索引设计是数据库设计中比较重要的一个环节,对数据库的性能其中至关重要的作用,但是索引的设计却又不是那么容易的事情,性能也不是那么轻易就获取到的,很多的技术人员因为不恰当的创建索引,最后使得其效果适得其反,可以说“成也索引,败也索引”。
本系列文章来自Stairway to SQL Server Indexes,然后经过我们团队的理解和整理发布在agilesharp,希望对广大的技术朋友在如何使用索引上有所帮助。
系列文章索目录:
SQL Server索引进阶第一篇:索引介绍
SQL Server索引进阶第二篇:深入非聚集索引
SQL Server索引进阶第三篇:聚集索引
SQL Server索引进阶第四篇:页和区
SQL Server索引进阶第五篇:索引包含列
SQL Server索引进阶第六篇:书签
SQL Server索引进阶第七篇:过滤的索引
SQL Server索引进阶第八篇:唯一索引
SQL Server索引进阶第九篇:解读执行计划
SQL Server索引进阶第十篇:索引的内部结构
SQL Server索引进阶第十一篇:索引碎片分析与解决(上)
SQL Server索引进阶第十一篇:索引碎片分析与解决(中)-碎片发生原理深度剖析
SQL Server索引进阶第十二篇:索引的创建,修改和删除
SQL Server索引进阶第十三篇:Insert,Update,Delete语句
SQL Server索引进阶第十四篇:索引统计
SQL Server索引进阶第十五篇:索引的最佳实践
在上一篇文章中,我们讲述了如何查看内部和外部的索引碎片,那么在本篇的文章中,我们将会从内部的底层来分析这些索引碎片是如何产生的,同时也给出处理碎片相对应的解决方案和一些最佳实践。
在这里要说明一下,因为原英文版本在理解上面可能会有些困难,为了使得大家更好的理解原文,我们这里特意的加入了一些其他的内容,帮助朋友们进行一个过渡。
因为索引碎片分析涉及到了页拆分的一些知识,页拆分发生在某个页上的数据已经填满而没有多余的空间给新增的数据而产生的动作,同时,向已经填满数据的页上面加入新的数据还可能会导致另外一个操作,所以,我们这里也随便的讲一个,使得大家更好的理解。
我们之前已经提到过,SQL Server在数据库中把任何的信息都是保存在基于8KB的页(不管是何种类型的页,我们这里不考虑大对象的数据页)上面的。如果记录(不管是底层的数据行记录还是索引中的条目等)的大小总和加起来小于8KB,那么SQL Server可能就会在一个页上面存放多条记录。如果大于了8KB,那么肯定就需要更多的页来进行记录的保存,此时SQL Server必须改变每一个页上面的记录。SQL Server主要基于两种方法来实现这个改变:记录转发与页拆分。
备注:记录-我们这里一个对数据的统称,例如数据页上面的每一条数据是一个记录,索引页上面的一个条目是一个记录。
记录转发
当记录的大小已经超过了一个页的容量的时候,第一种存放记录的方式就是“记录转发”。
这个方法只有当底层的数据表是堆的时候才采用。如果某一行的数据记录被修改,使得此时所在的数据页已经无法存放其修改的行所有的数据,SQL Server将会把这条记录移动到一个新的数据页上面去,同时会增加两个指针。第一个指针将会表明这个数据行现在新的位置,通常这个指针称之为“记录转发指针”,而第二个指针将会放在新的数据页上面,指向这个记录原先数据页,这个指针称之为“回指指针”。熟悉数据结构的朋友,其实可以把这个过程想成在一个链表中加入一个节点。
为了使得大家更好的明白上面的讲述,我们还是来看一个例子。在例子中,我们将会带着大家一起来看看记录转发这个过程是如何进行的。如下图:
假设图中的页,编号为100,这个页处于一个堆表中。在这个页中包含了4条数据,而且每一条数据大小约2K,加起来就是8KB。如果此时第二条数据被更新了,使得它的数据大小变为了2.5KB,此时这个数据页肯定就无法存放所有的数据,此时SQL Server就会再去分配一个新的页,假设编号为101。那么,第二条数据就会被移到新分配的数据页上面去,而且在原先的页(编号100)上面加上一个指针指向第二条数据的新位置。那么原先存放第二条记录的地方此时就放置了指针。
另外,在新的页101中,也有一个指针回指向页100。在图中没有画出来。
记录转发的问题在于,它使得一条数据在一个表中存在两个位置:一个位置存放指针,一个位置存放真实的数据。随着记录的不断变多,会增加更多的额外的磁盘空间,特别是读取数据时额外的I/O操作,因为可能存在这样的情况:某些记录通过不断的修改,使得它们不在适合存放在当前页,从而放在新页上,做第一次的记录转发,然后再修改,然后再次进行第二次的记录转发….如下图:
大家应该可以体会到,此时原本的数据A已经通过多次的转发,而在其他的页上面保留的仅仅只是它转发过程中下一个页的位置,这样,要想找到A数据,那么就要经过多次的指针查找,直到最后。
页拆分
对于页拆分,相信是很多朋友听的比较多的一个词了。下面,我们就来看看这个话题。页拆分发生在包含有索引的表中,要么有聚集索引,要么有非聚集索引。同时,页拆分不仅仅发生在数据页上,也发生在索引页上。
页拆分的过程基本是这样的: 如果一个记录的大小更新(或者增加),使得原来的页不在适应数据的大小,此时SQL Server无法将变化的数据写入,那么它就会把原先页上面的一半的记录移到新的页上面去。之后,SQL Server再次尝试去把数据写入,如果不行,那么再次分页,直到最后可以把数据写入。
我们还是通过一个例子来讲解这个问题。我们主要通过一个更新的操作来讲述。还是看到下面的图:
在页100上有4条记录,每一个的大小约2KB,此时刚好把一个页占满。如果此时对第二条数据进行修改,使得它的大小变为2.5KB,那么此时就会进行页的拆分。那么原先的4条数据,就会被分为2部分放在不同的页上,同时,SQL Server会在原先的页100上面放置一个指向新页的指针,然后SQL Server再次去更新第二条记录。
好,说完了上面两种情况之后,我们就来看看,它们对索引的碎片有什么影响。
其实谈到碎片问题,只要是发生在页拆分操作上,特别是当索引的B树结构发生页拆分的时候。
下面,我们就要细化这个过程。
如果此时,表上已经有了索引,如果在数据表中增加一行数据,那么,这行数据肯定要反应到索引结构中(除非采用了过滤的索引),从而使得索引结构开始发生调整。
如果增加到索引结构中的这个条目可以加入到某个索引页中,换句话说,索引页中的空闲的空间可以容纳新的索引条目的大小,这个过程算是结束。
如果空间不足,那么此时,肯定要去分配新的页面,此时还不确定这个新的页面和旧的页面是否在物理空间上面连续,那么这就产生外部索引碎片,同时把原先页中的索引记录分布在两个页上,使得这个两个页有了比之前更多的空闲的空间,这就增加了内部索引碎片。
但是内部的碎片,可能会随着索引记录的不断增加而将其空闲的填充而减少。但是外部的碎片只有等到我们维护索引的时候才消失。
其实,大家可以看出来,不仅仅是索引碎片,底层数据页的碎片也可以采用同样的分析方法。
本篇就到这里,我们下一篇继续这个话题。