SQL Server索引进阶第十一篇:索引碎片分析与解决

相关有关索引碎片的问题,大家应该是听过不少,也许也很多的朋友已经做了与之相关的工作。那我们今天就来看看这个问题。

为了更好的说明这个问题,我们首先来普及一些背景知识。

知识普及

我们都知道,数据库中的每一个表要么是堆表,要么就是包含聚集索引的表,或者我们称之为有序表。如果表是一个堆表,那么在使用非聚集索引查询数据的时候,会使用书签查找去底层的数据表中去检索需要的数据,这个书签查找会通过每一个索引中包含的行标识(RID)去定位每一个底层数据表的数据行。如果表上面有聚集索引,那么在使用非聚集索引查找其他需要数据的时候,就会使用聚集索引键去定位底层的数据行。

我们也知道,索引是由索引页组成的,索引中的每一个条目包含在页中。每8个页组成一个块。

索引的层级是从底向上的,就是一个树结构,最下面的就是第0层,也是叶节点。索引中的根节点处于整个索引的最上层。

如果要扫描整个索引,那么就意味着必须要读取页节点中的每一个页(要么是数据页,要么是索引页)。其中,每个页都包含着一个指向它前面的页和一个指向它后面也的指针。之前,我们也提过:如果单看某一层节点,其实就是一个双向链表。还是上个图,大家感受一下。

我们应该知道:页(不管是数据页,还是索引页 ,还是其他的类型的页)处于的逻辑顺序和它的物理顺便不一定就是一样的,也就说,在A页中的指针指向了它的下一个页B,也就说A和B页在逻辑上面是一起的,但是它们在物理上面可能不一样,甚至B页和A页在物理上相隔几百个页。

如果在逻辑上面相连的页在物理存储级别相隔的越近,那么在读取这些页的时候所花的I/O成本也就越小,因为产生磁盘的磁头移动带来的延迟。相反,如果他们的物理存储顺序和逻辑顺序一致,那么SQL Server在读取的时候,就可以一次读取,因为每次会读取一个块(8个页)。

好了,普及知识之后,我们就来看看什么是碎片。

什么是索引碎片

索引碎片可以分为两类:内部索引碎片和外部索引碎片。下面我们就来具体的看看而这之前的区别以及如何检查。

内部索引碎片

每一个索引页中都包含一些索引的条目(就类似数据页包含很多的数据行一行),这一点我们在之前讲过了的。但是,很多的时候,不是每个页都包含了最大的条数。例如,一个页的大小8k,也就是4096字节,除去一些页头,页脚等,还剩下8000多字节,如果每个索引条目的大小事100字节,那么这个索引页最大就可以包含80个条目,但是很多的情况下,却没有包含这么多。

也就说,很多的时候,索引页并没有完全的填满,或者这是问题,或许这么我们特意这样的,我们后续会提到。当我们谈到索引碎片的时候,我们往往就是指这些索引页没有完全填满。或者说的更加明白一点就是:我们原本是希望页都被填满的,但是随着数据的增删改,使得索引中的数据没有填满,结果如下:

 Index_Page_after_delete.png(26.79 K)

9/9/2012 11:19:53 AM

图不是很清晰,大家意会一下就行了。

我们可以使用
sys.dm_db_index_physical_stats来查看相关的内部碎片的情况,执行查询如下:

  1. SELECT IX.name AS ‘Name‘
  2. , PS.index_level AS ‘Level‘
  3. , PS.page_count AS ‘Pages‘
  4. , PS.avg_page_space_used_in_percent AS ‘Page Fullness (%)‘
  5. FROM sys.dm_db_index_physical_stats(
  6. DB_ID(),
  7. OBJECT_ID(‘Sales.SalesOrderDetail‘),
  8. DEFAULT, DEFAULT, ‘DETAILED‘) PS
  9. JOIN sys.indexes IX
  10. ON IX.OBJECT_ID = PS.OBJECT_ID AND IX.index_id = PS.index_id

复制代码

执行结果如图:

 20120904184258.png(56.68 K)

9/9/2012 11:19:53 AM

我们可以看到每个索引的页面的填充情况。

下面,我们再来讲讲外部索引碎片。

外部索引碎片

理解了上面的问题,这个外部索引碎片就好理解了,最简单的说法就是:索引中的索引页的逻辑顺序和物理顺序不一致。我们通过个图对比的来看看。

 9865.jpg(95.72 K)

9/9/2012 11:19:53 AM

在上图中,一个索引包含了16个页。但是这16页不是包含在2个相连的块中的,而是分布在不同的地方,因为它们之前中的一些块被其他的对象占用了。这样就导致了16个页在物理上面不连续,这就是碎片。在读取的时候,就会消耗额外的I/O。

和之前一样,我们可以使用
sys.dm_db_index_physical_stats来查看外部碎片的情况。但是这里的参数值可能要发生变化了:之前在sys.dm_db_index_physical_stats最后一个参数值是‘DETAILED‘,这里我们的值是LIMITED或者Default。因为外部碎片关注的是索引页之前的连续性问题,不关注每一个页中的数据,此时只是部分的扫描,没有必要全部的扫描。大家可以参看MSDN的去进一步的理解这些参数的含义。

查询如下:

  1. SELECT IX.name AS ‘Name‘
  2. , PS.index_level AS ‘Level‘
  3. , PS.page_count AS ‘Pages‘
  4. , PS.avg_fragmentation_in_percent AS ‘External Fragmentation (%)‘
  5. , PS.fragment_count AS ‘Fragments‘
  6. , PS.avg_fragment_size_in_pages AS ‘Avg Fragment Size‘
  7. FROM sys.dm_db_index_physical_stats(
  8. DB_ID(),
  9. OBJECT_ID(‘Sales.SalesOrderDetail‘),
  10. DEFAULT, DEFAULT, ‘LIMITED‘) PS
  11. JOIN sys.indexes IX
  12. ON IX.OBJECT_ID = PS.OBJECT_ID AND IX.index_id = PS.index_id

复制代码

结果如下:

 20120904184512.png(30.11 K)

9/9/2012 11:19:53 AM

除了使用脚本之外,我们还可以在SQL Server管理器中查看,在某个索引上面右键,属性,如下:

在这里要说明一下,因为原英文版本在理解上面可能会有些困难,为了使得大家更好的理解原文,我们这里特意的加入了一些其他的内容,帮助朋友们进行一个过渡。

因为索引碎片分析涉及到了页拆分的一些知识,页拆分发生在某个页上的数据已经填满而没有多余的空间给新增的数据而产生的动作,同时,向已经填满数据的页上面加入新的数据还可能会导致另外一个操作,所以,我们这里也随便的讲一个,使得大家更好的理解。

我们之前已经提到过,SQL Server在数据库中把任何的信息都是保存在基于8KB的页(不管是何种类型的页,我们这里不考虑大对象的数据页)上面的。如果记录(不管是底层的数据行记录还是索引中的条目等)的大小总和加起来小于8KB,那么SQL Server可能就会在一个页上面存放多条记录。如果大于了8KB,那么肯定就需要更多的页来进行记录的保存,此时SQL Server必须改变每一个页上面的记录。SQL Server主要基于两种方法来实现这个改变:记录转发与页拆分。

备注:记录-我们这里一个对数据的统称,例如数据页上面的每一条数据是一个记录,索引页上面的一个条目是一个记录。

记录转发

当记录的大小已经超过了一个页的容量的时候,第一种存放记录的方式就是“记录转发”。

这个方法只有当底层的数据表是堆的时候才采用。如果某一行的数据记录被修改,使得此时所在的数据页已经无法存放其修改的行所有的数据,SQL Server将会把这条记录移动到一个新的数据页上面去,同时会增加两个指针。第一个指针将会表明这个数据行现在新的位置,通常这个指针称之为“记录转发指针”,而第二个指针将会放在新的数据页上面,指向这个记录原先数据页,这个指针称之为“回指指针”。熟悉数据结构的朋友,其实可以把这个过程想成在一个链表中加入一个节点。

为了使得大家更好的明白上面的讲述,我们还是来看一个例子。在例子中,我们将会带着大家一起来看看记录转发这个过程是如何进行的。如下图:

 20120906203332.png (30.64 K)

9/9/2012 11:20:20 AM

假设图中的页,编号为100,这个页处于一个堆表中。在这个页中包含了4条数据,而且每一条数据大小约2K,加起来就是8KB。如果此时第二条数据被更新了,使得它的数据大小变为了2.5KB,此时这个数据页肯定就无法存放所有的数据,此时SQL Server就会再去分配一个新的页,假设编号为101。那么,第二条数据就会被移到新分配的数据页上面去,而且在原先的页(编号100)上面加上一个指针指向第二条数据的新位置。那么原先存放第二条记录的地方此时就放置了指针。

另外,在新的页101中,也有一个指针回指向页100。在图中没有画出来。

记录转发的问题在于,它使得一条数据在一个表中存在两个位置:一个位置存放指针,一个位置存放真实的数据。随着记录的不断变多,会增加更多的额外的磁盘空间,特别是读取数据时额外的I/O操作,因为可能存在这样的情况:某些记录通过不断的修改,使得它们不在适合存放在当前页,从而放在新页上,做第一次的记录转发,然后再修改,然后再次进行第二次的记录转发….如下图:

 20120906203429.png (31.27 K)

9/9/2012 11:20:20 AM

大家应该可以体会到,此时原本的数据A已经通过多次的转发,而在其他的页上面保留的仅仅只是它转发过程中下一个页的位置,这样,要想找到A数据,那么就要经过多次的指针查找,直到最后。

页拆分

对于页拆分,相信是很多朋友听的比较多的一个词了。下面,我们就来看看这个话题。页拆分发生在包含有索引的表中,要么有聚集索引,要么有非聚集索引。同时,页拆分不仅仅发生在数据页上,也发生在索引页上。

页拆分的过程基本是这样的: 如果一个记录的大小更新(或者增加),使得原来的页不在适应数据的大小,此时SQL Server无法将变化的数据写入,那么它就会把原先页上面的一半的记录移到新的页上面去。之后,SQL Server再次尝试去把数据写入,如果不行,那么再次分页,直到最后可以把数据写入。

我们还是通过一个例子来讲解这个问题。我们主要通过一个更新的操作来讲述。还是看到下面的图:

 20120906203529.png (31.46 K)

9/9/2012 11:20:20 AM

在页100上有4条记录,每一个的大小约2KB,此时刚好把一个页占满。如果此时对第二条数据进行修改,使得它的大小变为2.5KB,那么此时就会进行页的拆分。那么原先的4条数据,就会被分为2部分放在不同的页上,同时,SQL Server会在原先的页100上面放置一个指向新页的指针,然后SQL Server再次去更新第二条记录。

好,说完了上面两种情况之后,我们就来看看,它们对索引的碎片有什么影响。

其实谈到碎片问题,只要是发生在页拆分操作上,特别是当索引的B树结构发生页拆分的时候。

下面,我们就要细化这个过程。

如果此时,表上已经有了索引,如果在数据表中增加一行数据,那么,这行数据肯定要反应到索引结构中(除非采用了过滤的索引),从而使得索引结构开始发生调整。

如果增加到索引结构中的这个条目可以加入到某个索引页中,换句话说,索引页中的空闲的空间可以容纳新的索引条目的大小,这个过程算是结束。

如果空间不足,那么此时,肯定要去分配新的页面,此时还不确定这个新的页面和旧的页面是否在物理空间上面连续,那么这就产生外部索引碎片,同时把原先页中的索引记录分布在两个页上,使得这个两个页有了比之前更多的空闲的空间,这就增加了内部索引碎片。

但是内部的碎片,可能会随着索引记录的不断增加而将其空闲的填充而减少。但是外部的碎片只有等到我们维护索引的时候才消失。

其实,大家可以看出来,不仅仅是索引碎片,底层数据页的碎片也可以采用同样的分析方法。

时间: 2024-10-08 00:46:48

SQL Server索引进阶第十一篇:索引碎片分析与解决的相关文章

SQL Server 磁盘请求超时的833错误原因分析以及解决

本文出处:http://www.cnblogs.com/wy123/p/6984885.html 最近遇到一个SQL Server服务器响应极度缓慢,并且出现客户端请求报错的情况,在数据库中的errorlog中出现磁盘请求超过一定时间才完成的error消息.对于此类问题,到底是存储系统或者磁盘的故障,还是SQL Server 自己的问题,亦或是应用程序引发的呢?又要如何解决?本文将对引起此问题的某一方面的因素进行简单的分析,但是无法涵盖所有潜在的可能性,因此遇到类似问题还要做具体的分析. SQL

【译】SQL Server索引进阶第八篇:唯一索引

原文:[译]SQL Server索引进阶第八篇:唯一索引     索引设计是数据库设计中比较重要的一个环节,对数据库的性能其中至关重要的作用,但是索引的设计却又不是那么容易的事情,性能也不是那么轻易就获取到的,很多的技术人员因为不恰当的创建索引,最后使得其效果适得其反,可以说"成也索引,败也索引".     本系列文章来自Stairway to SQL Server Indexes,翻译和整理后发布在agilesharp和博客园,希望对广大的技术朋友在如何使用索引上有所帮助.   唯一

SQL Server调优系列基础篇(索引运算总结)

原文:SQL Server调优系列基础篇(索引运算总结) 前言 上几篇文章我们介绍了如何查看查询计划.常用运算符的介绍.并行运算的方式,有兴趣的可以点击查看. 本篇将分析在SQL Server中,如何利用先有索引项进行查询性能优化,通过了解这些索引项的应用方式可以指导我们如何建立索引.调整我们的查询语句,达到性能优化的目的. 闲言少叙,进入本篇的正题. 技术准备 基于SQL Server2008R2版本,利用微软的一个更简洁的案例库(Northwind)进行解析. 简介 所谓的索引应用就是在我们

SQL Server 性能调优3 之索引(Index)的维护

SQL Server 性能调优3 之索引(Index)的维护 热度1 评论 16 作者:溪溪水草 前言 前一篇的文章介绍了通过建立索引来提高数据库的查询性能,这其实只是个开始.后续如果缺少适当的维护,你先前建立的索引甚至会成为拖累,成为数据库性能的下降的帮凶. 查找碎片 消除碎片可能是索引维护最常规的任务,微软官方给出的建议是当碎片等级为 5% - 30% 之间时采用 REORGANIZE 来“重整”索引,如果达到 30% 以上则使用 REBUILD 来“重建”索引.决定采用何种手段和操作时机可

解读SQL Server 2014可更新列存储索引——存储机制

概述 SQL Server 2014被号称是微软数据库的一个革命性版本,其性能的提升的幅度是有史以来之最. 可更新的列存储索引作为SQL Server 2014的一个关键功能之一,在提升数据库的查询性能方面贡献非常突出.据微软统计,在面向OLAP查询统计类系统中,相比其他SQL传统版本的数据库,报表查询的性能最大可提升上十倍. 下面我们从存储的角度来了解下SQL Server 2014的可更新列存储索引. 什么是列存储 微软为了提升SQL Server的查询性能,更好的支持大数据分析,早在SQL

SQL Server 2016:内存列存储索引

作者 Jonathan Allen,译者 谢丽 SQL Server 2016的一项新特性是可以在"内存优化表(Memory Optimized Table)"上添加"列存储索引(Columnstore Index)".要理解这是什么意思,我们应该首先解释术语列存储索引和内存优化表.列存储索引是一种按照列而不是行组织数据的索引.每个数据块只存储一个列的数据,最多包含100万行.因此,如果数据为5列1000万行,那么就需要存储在50个数据块中.当只查询部分列时,这种数

SQL SERVER中关于OR会导致索引扫描或全表扫描的浅析

原文:SQL SERVER中关于OR会导致索引扫描或全表扫描的浅析 在SQL SERVER的查询语句中使用OR是否会导致不走索引查找(Index Seek)或索引失效(堆表走全表扫描 (Table Scan).聚集索引表走聚集索引扫描(Clustered Index Seek))呢?是否所有情况都是如此?又该如何优化呢? 下面我们通过一些简单的例子来分析理解这些现象.下面的实验环境为SQL SERVER 2008,如果在不同版本有所区别,欢迎指正. 堆表单索引 首先我们构建我们测试需要实验环境,

SQL Server 性能调优2 之索引(Index)的建立

前言 索引是关系数据库中最重要的对象之一,他能显著减少磁盘I/O及逻辑读取的消耗,并以此来提升 SELECT 语句的查找性能.但它是一把双刃剑,使用不当反而会影响性能:他需要额外的控件来存放这些索引信息,并且当数据更新时需要一些额外开销来保持索引的同步. 形象的来说索引就像字典里的目录,你要查找某一个字的时候可以根据它的比划/拼音先在目录中找到对应的页码范围,然后在该范围中找到这个字.如果没有这个目录(索引),你可能需要翻遍整本字典来找到要找的字. SQL Server 中的索引以 B-Tree

SQL Server调优系列基础篇(联合运算符总结)

前言 上两篇文章我们介绍了查看查询计划的方式,以及一些常用的连接运算符的优化技巧,本篇我们总结联合运算符的使用方式和优化技巧. 废话少说,直接进入本篇的主题. 技术准备 基于SQL Server2008R2版本,利用微软的一个更简洁的案例库(Northwind)进行解析. 一.联合运算符 所谓的联合运算符,其实应用最多的就两种:UNION ALL和UNION. 这两个运算符用法很简单,前者是将两个数据集结果合并,后者则是合并后进行去重操作,如果有过写T-SQL语句的码农都不会陌生. 我们来分析下