SQL Server索引的维护 - 索引碎片、填充因子 <第三篇>

  实际上,索引的维护主要包括如下两个方面:html

  •   页拆分
  •   碎片

  这两个问题都和页密度有关,虽然二者的表现形式在本质上有所区别,可是故障排除工具是同样的,由于处理是相同的。sql

  对于很是小的表(比64KB小得多),一个区中的页面可能属于多余一个的索引或表---这被称为混合区。若是数据库中有太多的小表,混合区帮助SQL Server节约磁盘空间。数据库

  随着表(或索引)增加而且请求超过8个页面,SQL Server建立专用于该表(或索引)的区而且从该区中分配页面。这样一个区被称为统一区,它能够为多达8个相同表或索引的页面请求服务。并发

1、碎片

  当数据库增加,页拆分,而后删除数据时,就会产生碎片。从增加的方面看,平衡树处理得很不错。可是对于删除方面,它并无太大的做用。最终可能会出现这种状况,一个页上有一条记录,而另外一个页上有几个记录。在这种状况下,一个页上保存的数据量只是它可以保存总数据量的一小部分。app

  一、碎片会形成空间的浪费,SQL Server每次会分配一个区段,若是一个页上只有一条记录,则仍然会分配整个区段。函数

  二、散布在各处的数据会形成数据检索时的额外系统开销。为了获取须要的10行记录,SQL Server不是只加载一个页,而是可能必须加载10个页来获取相同的信息。并不仅是读取行致使了这一结果,在读取行前,SQL Server必须先读取页。更多的页意味着更多的工做量。工具

  可是碎片也不仅是有坏处,好比一个插入很是频繁的表就很喜欢碎片,由于在插入数据时几乎不用担忧页拆分的问题。因此大量的碎片意味着较差的读取性能,但也意味着极好的插入性能。性能

  关于碎片的理解,找到了数据库牛人CareySon的这篇文章 T-SQL查询高级—SQL Server索引中的碎片和填充因子 ,在这里消化一下
ui

  碎片分两种,外部碎片和内部碎片this

  外部碎片:

  外部碎片指的是页拆分而产生的碎片。如向表中插入一行,而这一行致使现有的页空间没法容纳新插入的行,则致使页拆分。

  

  新的页不断随数据的增加而产生,而汇集索引要求行之间连续,因此若是汇集索引不是自增列,页拆分后和原来的页在磁盘上并不连续-这就是外部碎片。
  因为页拆分,致使数据在页之间的移动,因此若是插入更新等操做常常须要分页,则会大大消耗IO资源,形成性能降低。
  对于查找连说,在有特定搜索条件,如where子句有很细的限制或者返回无序结果集时,外部碎片并不会对性能产生影响。但若是要返回扫描汇集索引并且查找连续页面时,外部碎片就会产生性能上的影响。因此当要读取相同的数连续的数据时须要扫描更多的页,更多的区。并且连续数据不能预读,形成额外的物理读,增长磁盘IO。一般,外部碎片过多会形成频繁的区切换。

  若是页面连续排序,预读功能能够提早读取页面而不须要太多的磁头移动。

  内部碎片:  

  内部碎片是页拆分后,致使索引页的数据并不满,有空行。一样读取一个索引页,却只能拿到x%的数据。

--新建一张表
CREATE TABLE Person
(
    Id    int,
    Name    char(999),
    Addr    varchar(10)
)
--汇集索引 CREATE CLUSTERED INDEX CIX ON Person(Id)
--插入8条数据 DECLARE @var INT SET @var=100 WHILE(@var < 900) BEGIN INSERT INTO Person(Id,Name,Addr) VALUES(@var,'xx','') SET @var = @var+100 END

  这个表每一个行由int(4字节),char(999字节)和varchar(10字节组成),因此每行为1003个字节,则8行占用空间1003*8=8024字节加上一些内部开销,能够容纳在一个页面中。(原来这个表和数据搞得还挺巧的)。

  执行查看语句:

  SELECT page_count,avg_page_space_used_in_percent,record_count,avg_record_size_in_bytes,avg_fragmentation_in_percent,fragment_count 
  FROM sys.dm_db_index_physical_stats
  (DB_ID('Nx'),object_id('dbo.Person'),NULL,NULL,'sampled')

  示例以下:

  

  其中page_count是查看占用了多少个页,而第二个参数表示该页空间的使用率。所以从以上信息能够得到,这8条数据是放在一个页上,并且该页的空间使用率已是百分之百了。

  如今将其中一行的Addr改长一点:

  UPDATE Person SET Addr = '广东广州' where Id = 100

  则再执行检查索引语句:

  

  能够看到,这个表已经有了两页,页面平均使用为50%左右。可是明显也形成了碎片,在列avg_fragmentation_in_percent上能够看到,碎片大约为50%。

  页拆分后的示意图以下:

  

  这个时候,继续插入数据,碎片会上升。在又插入了至达到48条记录后,碎片程度以下:

  

  这个时候,执行一个查询计划,查看下IO性能:

  

  能够看到I/O降低了很多。

2、元数据函数sys.dm_db_index_physical_stats分析碎片

  SQL Server提供了一种特殊的元数据函数sys.dm_db_index_physical_stats,它有助于肯定数据库中的页和区段有多满。而后用该信息做出一些维护数据库的决策。

  该函数语法以下:

sys.dm_db_index_physical_stats(
    {<database id> | NULL | 0 | DEFAULT},
    { <object id> | NULL | 0 | DEFAULT },
    { <index id> } | NULL | 0 | -1 | DEFAULT },
    { <partition no> | NULL | 0 | DEFAULT },
    { <mode> | NULL | DEFAULT }
)

  下面假设从SmartScan中获取全部的索引信息:

DECLARE @db_id SMALLINT;
DECLARE @object_id INT;
SET @db_id = DB_ID(N'Nx');
SET @object_id = OBJECT_ID(N'Account')
SELECT database_id,object_id,index_id,index_depth,avg_fragmentation_in_percent,page_count
FROM sys.dm_db_index_physical_stats(@db_id,@object_id,NULL,NULL,NULL);

   下面看看统计信息的说明:

列名

数据类型

说明

database_id

smallint

表或视图的数据库 ID。

object_id

int

索引所在的表或视图的对象 ID。

index_id

int

索引的索引 ID。

0 = 堆。

partition_number

int

所属对象内从 1 开始的分区号;表、视图或索引。

1 = 未分区的索引或堆。

index_type_desc

nvarchar(60)

索引类型的说明:

HEAP

CLUSTERED INDEX

NONCLUSTERED INDEX

PRIMARY XML INDEX

SPATIAL INDEX

XML INDEX

alloc_unit_type_desc

nvarchar(60)

对分配单元类型的说明:

IN_ROW_DATA

LOB_DATA

ROW_OVERFLOW_DATA

LOB_DATA 分配单元包含类型为textntextimagevarchar(max)nvarchar(max)varbinary(max) 和 xml 的列中所存储的数据。 

ROW_OVERFLOW_DATA 分配单元包含类型为 varchar(n)nvarchar(n)varbinary(n) 和sql_variant 的列(已推送到行外)中所存储的数据。

index_depth

tinyint

索引总级别数。

1 = 堆,或 LOB_DATA 或 ROW_OVERFLOW_DATA 分配单元。

index_level

tinyint

索引的当前位于B树结构中的级别。

0 表示索引叶级别、堆以及 LOB_DATA 或 ROW_OVERFLOW_DATA 分配单元。

大于 0 的值表示非叶索引级别。 index_level 在索引的根级别中属于最高级别。

仅当 mode = DETAILED 时才处理非叶级别的索引。

avg_fragmentation_in_percent

float

索引的逻辑碎片,或 IN_ROW_DATA 分配单元中堆的区碎片。

此值按百分比计算,并将考虑多个文件。

0 表示 LOB_DATA 和 ROW_OVERFLOW_DATA 分配单元。

若是是堆表且mode模式 为 Sampled 时,为 NULL。若是碎片小于10%~20%,碎片不太可能会成为问题,若是索引碎片在20%~40%,碎片可能成为问题,可是能够经过索引重组来消除索引解决,大规模的碎片(当碎片大于40%),可能要求索引重建。

fragment_count

bigint

IN_ROW_DATA 分配单元的叶级别中的碎片数。 

对于索引的非叶级别,以及 LOB_DATA 或 ROW_OVERFLOW_DATA 分配单元,为 NULL。

对于堆,当 mode 为 SAMPLED 时,为 NULL。

avg_fragment_size_in_pages

float

IN_ROW_DATA 分配单元的叶级别中的一个碎片的平均页数。

对于索引的非叶级别,以及 LOB_DATA 或 ROW_OVERFLOW_DATA 分配单元,为 NULL。

对于堆,当 mode 为 SAMPLED 时,为 NULL。

page_count

bigint

索引或数据页的总数。

对于索引,表示 IN_ROW_DATA 分配单元中 b 树的当前级别中的索引页总数。

对于堆,表示 IN_ROW_DATA 分配单元中的数据页总数。

对于 LOB_DATA 或 ROW_OVERFLOW_DATA 分配单元,表示该分配单元中的总页数。

avg_page_space_used_in_percent

float

全部页中使用的可用数据存储空间的平均百分比。

对于索引,平均百分比应用于 IN_ROW_DATA 分配单元中 b 树的当前级别。

对于堆,表示 IN_ROW_DATA 分配单元中全部数据页的平均百分比。

对于 LOB_DATA 或 ROW_OVERFLOW DATA 分配单元,表示该分配单元中全部页的平均百分比。

当 mode 为 LIMITED 时,为 NULL。

record_count

bigint

总记录数。

对于索引,记录的总数应用于 IN_ROW_DATA 分配单元中 b 树(包括非叶子数据页的数量)的当前级别。

对于堆,表示 IN_ROW_DATA 分配单元中的总记录数。

注意 注意

对于堆,此函数返回的记录数可能与经过对堆运行 SELECT COUNT(*) 返回的行数不匹配。 这是由于一行可能包含多个记录。 例如,在某些更新状况下,单个堆行可能因为更新操做而包含一条前推记录和一条被前推记录。 此外,多数大型 LOB 行在 LOB_DATA 存储中拆分为多个记录。

对于 LOB_DATA 或 ROW_OVERFLOW_DATA 分配单元,表示整个分配单元中总记录数。

当 mode 为 LIMITED 时,为 NULL。

ghost_record_count

bigint

分配单元中将被虚影清除任务删除的虚影记录数。

对于 IN_ROW_DATA 分配单元中索引的非叶级别,为 0。

当 mode 为 LIMITED 时,为 NULL。

version_ghost_record_count

bigint

由分配单元中未完成的快照隔离事务保留的虚影记录数。

对于 IN_ROW_DATA 分配单元中索引的非叶级别,为 0。

当 mode 为 LIMITED 时,为 NULL。

min_record_size_in_bytes

int

最小记录大小(字节)。

对于索引,最小记录大小应用于 IN_ROW_DATA 分配单元中 b 树的当前级别。

对于堆,表示 IN_ROW_DATA 分配单元中的最小记录大小。

对于 LOB_DATA 或 ROW_OVERFLOW_DATA 分配单元,表示整个分配单元中的最小记录大小。

当 mode 为 LIMITED 时,为 NULL。

max_record_size_in_bytes

int

最大记录大小(字节)。

对于索引,最大记录的大小应用于 IN_ROW_DATA 分配单元中 b 树的当前级别。

对于堆,表示 IN_ROW_DATA 分配单元中的最大记录大小。

对于 LOB_DATA 或 ROW_OVERFLOW_DATA 分配单元,表示整个分配单元中的最大记录大小。

当 mode 为 LIMITED 时,为 NULL。

avg_record_size_in_bytes

float

平均记录大小(字节)。

对于索引,平均记录大小应用于 IN_ROW_DATA 分配单元中 b 树的当前级别。

对于堆,表示 IN_ROW_DATA 分配单元中的平均记录大小。

对于 LOB_DATA 或 ROW_OVERFLOW_DATA 分配单元,表示整个分配单元中的平均记录大小。

当 mode 为 LIMITED 时,为 NULL。

forwarded_record_count

bigint

堆中具备指向另外一个数据位置的转向指针的记录数。 (在更新过程当中,若是在原始位置存储新行的空间不足,将会出现此状态。)

除 IN_ROW_DATA 分配单元外,对于堆的其余全部分配单元都为 NULL。

当 mode = LIMITED 时,对于堆为 NULL。

compressed_page_count

bigint

压缩页的数目。

  • 对于堆,新分配的页未进行 PAGE 压缩。 堆在如下两种特殊状况下进行 PAGE 压缩:大量导入数据时和从新生成堆时。 致使页分配的典型 DML 操做不会进行 PAGE 压缩。 当 compressed_page_count 值增加到超过您所需的阈值时,将从新生成堆。

  • 对于具备汇集索引的表,compressed_page_count 值表示 PAGE 压缩的效率。

   一般返回多行的时候,有个index_level列,这个列表示改行属于B树结构的第几层。

  分析小表的碎片

  不要过度关注小表的sys.dm_db_index_physical_stats输出。对于少于8个页面的小表或者索引,SQL Server使用混合区。例如,若是一个表仅包含两个页面,SQL Server从一个混合区中分配两个页面,二不是分配一个区给该表。混合区也能够包含其余小表或索引的页面。

  跨越多个混合区的页面分布可能致使你相信在表或索引中有大量的外部碎片,而实际上这是SQL Server的设计,于是是可接受的。

  先来建一张表以下,3个int字段,1个char(2000)字段。平均尺寸为4+4+4+2000=2012字节,8KB的页面最多包含4行。在添加了28行以后,建立一个汇集索引来从屋里上排列行并将碎片减小到最低限度。

  

  咋一看,好像碎片很是厉害。实际上并非这么回事。

  分析以下:

  •   avg_fragmentation_in_percent:尽管这个索引可能跨越多个区,这里看到碎片的状况并非外部碎片的迹象,由于该索引保存在混合区上。
  •   avg_page_space_used_in_percent:这说明全部或大部分县市在page_count中的7个页面中的数据存储情况良好。几乎满了,99点几。这消除了逻辑碎片的可能性。
  •   fragment_count:这说明数据有碎片而且保存在多于一个区上,可是由于它的长度小于8个页面,SQL Server对存储该数据的地点没有不少选择。

  尽管有上述引发误导的数值,一个少于8个页面的小表(或索引)不可能从去除碎片的工做中获益,由于它保存在混合区上。

  索引说明:

  

3、关于碎片的解决方法

  1.删除索引并重建

  这种方式有以下缺点:

  索引不可用:在删除索引期间,索引不可用。

  阻塞:卸载并重建索引会阻塞表上全部的其余请求,也可能被其余请求所阻塞。

  对于删除汇集索引,则会致使对应的非汇集索引重建两次(删除时重建,创建时再重建,由于非汇集索引中有指向汇集索引的指针)。

  惟一性约束:用于定义主键或者惟一性约束的索引不能使用DROP INDEX语句删除。并且,惟一性约束和主键均可能被外键约束引用。在主键卸载以前,全部引用该主键的外键必须首先被删除。尽管能够这么作,但这是一种冒险并且费时的碎片整理方法。

  基于以上缘由,不建议在生产数据库,尤为是非空闲时间不建议采用这种技术。

  2.使用DROP_EXISTING语句重建索引

  为了不重建两次索引,使用DROP_EXISTING语句重建索引,由于这个语句是原子性的,不会致使非汇集索引重建两次,但一样的,这种方式也会形成阻塞。

CREATE UNIQUE CLUSTERED INDEX IX_C1 ON t1(c1)
WITH (DROP_EXISTING = ON)

  缺点:

  阻塞:与卸载重建方法相似,这种技术也致使并面临来自其余访问该表(或该表的索引)的查询的阻塞问题。

  使用约束的索引:与卸载重建不一样,具备DROP_EXISTING子句的CREATE INDEX语句能够用于从新建立使用约束的索引。若是该约束是一个主键或与外键相关的惟一性约束,在CREATE语句中不能包含UNIQUE。

  具备多个碎片化的索引的表:随着表数据产生碎片,索引经常也碎片化。若是使用这种碎片整理技术,表上全部索引都必须单独确认和重建。

  3.使用ALTER INDEX REBUILD语句重建索引

  使用这个语句一样也是重建索引,可是经过动态重建索引而不须要卸载并重建索引.是优于前两种方法的,但依旧会形成阻塞。能够经过ONLINE关键字减小锁,但会形成重建时间加长。

  阻塞:这个依然有阻塞问题。

  事务回滚:ALTER INDEX REBUILD彻底是一个原子操做,若是它在结束前中止,全部到那时为止进行的碎片整理操做都将丢失,能够经过ONLINE关键字减小锁,但会形成重建时间加长。

  4.使用ALTER INDEX REORGANIZE

  这种方式不会重建索引,也不会生成新的页,仅仅是整理叶级数据,不涉及非叶级,当遇到加锁的页时跳过,因此不会形成阻塞。但同时,整理效果会差于前三种。

  4种索引整理技术比较:

特性/问题 卸载并重建索引 DROP_EXISTING ALTER INDEX REBUILD ALTER INDEX REORGANIZE
在汇集索引碎片整理时,重建非汇集索引 两次
丢失索引
整理具备约束的索引的碎片 高度复杂 复杂性适中 简单 简单
同时进行多个索引的碎片整理
并发性 中等,取决于冰法用户活动
中途撤销 由于不使用事务,存在危险 进程丢失 进程丢失 进程被保留
碎片整理程度 中到低
应用新的填充因子
更新统计

 

4、填充因子FILLFACTOR

   重建索引可以解决碎片的问题,可是重建索引的代码一来须要常常操做,二来会形成数据阻塞,影响使用。在数据比较少的状况下,重建索引代价很快,可是当索引比较大的时候,例如超过100M,那么重建索引的时间会很是长。

   填充因子的做用是控制索引叶子页面中的空闲空间数量。说白了就是预留一些空间给INSERT和UPDATE。若是知道表上有不少的INSERT查询或者索引键列上有足够的UPDATE查询,能够预先使用填充因子来增长索引叶子页面的空闲空间已最小化页面分割。若是表示只读的,能够建立一个高填充因子来减小索引页面的数量。

  默认的填充因子为0,这意味着页面将被100%充满。

   

   填充因子的概念能够理解为预留必定的空间存放插入和更新新增长的数据,以免页拆分:

  

   能够看出,使用填充因子会减小更新或者插入时的分页次数,但因为须要更多的页,则会对应的损失查找性能.

   填充因子值的选择:

  如何设置填充因子的值并无一个公式或者理念能够准确的设置。使用填充因子虽然能够减小更新或者插入时的分页,但同时由于须要更多的页,因此下降了查询的性能和占用更多的磁盘空间.如何设置这个值进行trade-off须要根据具体的状况来看.

    具体状况要根据对于表的读写比例来看,我这里给出我认为比较合适的值:

  1. 当读写比例大于100:1时,不要设置填充因子,100%填充
  2. 当写的次数大于读的次数时,设置50%-70%填充
  3. 当读写比例位于二者之间时80%-90%填充
相关文章
相关标签/搜索