SQL SERVE的聚集索引与非聚集索引
聚集索引:
聚集索引,来源于生活尝试。这中索引可以说是按照数据的物理存储进行划分的。对于一堆记录来说,使用聚集索引就是对这堆记录 进行 堆划分。即主要描述的是物理上的存储。
举个例子:
比如图书馆新进了一批书。那么这些书需要放到图书馆内。书如何放呢?一般都有一个规则,杂志类的放到101房间,文学类的放到102房间,理工类的放到103房间等等。这些存储的规则决定了每本书应该放到哪里。而这个例子中聚集索引为书的类别。
正式因为这种存储规则,才导致 聚集索引的唯一性。
误区:
有的人认为,聚集索引的字段是唯一的。这是因为sql server 中添加主键的时候,自动给主键所在的字段生成一个聚集索引。所以人们会认为聚集索引所加的字段是唯一的。
思考一下上面这个问题。杂志类的书放到101房间。那么如果杂志类的书太多,一个101房间存放不下。那么可能101,201两个房间来存放杂志类的书籍。如果这样分析的话,那么一个杂志类对应多个房间。放到表存储的话,那么这个类别字段 就不是唯一的了。
非聚集索引:
非聚集索引,也可以从生活中找到映射。非聚集索引强调的是逻辑分类。可以说是定义了一套存储规则,而需要有一块控件来维护这个规则,这个被称之为索引表。
继续使用上述提到的例子:
同学如果想去图书馆找一本书,而不知道这本书在哪里?那么这个同学首先应该找的就是 检索室吧。对于要查找一本书来说,在检索室查是一个非常快捷的的途径了吧。但是,在检索室中你查到了该书在XX室XX书架的信息。你的查询结束了吗?没有吧。你仅仅找到了目的书的位置信息,你还要去该位置去取书。
对于这种方式来说,你需要两个步骤:
1、查询该记录所在的位置。
2、通过该位置去取要找的记录。
区别:
聚集索引:可以帮助把很大的范围,迅速减小范围。但是查找该记录,就要从这个小范围中Scan了。
非聚集索引:把一个很大的范围,转换成一个小的地图。你需要在这个小地图中找你要寻找的信息的位置。然后通过这个位置,再去找你所需要的记录。
索引与主键的区别
主键:主键是唯一的,用于快速定位一条记录。
聚集索引:聚集索引也是唯一的。(因为聚集索引的划分依据是物理存储)。而聚集索引的主要是为了快速的缩小查找范围,即记录数目未定。
主键和索引没有关系。他们的用途相近。如果聚集索引加上唯一性约束之后,他们的作用就一样了。
使用场景
基于上述的两种规则,那么在什么时候适合聚集索引,什么时候适合非聚集索引?
有很多人写了聚集索引和非聚集索引的文章,但我觉得在很多文章中表达的概念并不清楚,因此自己也写一篇,能够让自己想清楚。我的最初目的是要写到NO SQL,因此这系列的文章主要是关注在 1.数据库索引结构、2.表联接、3.递归查询这几个点上。
一、基本概念
1.数据的读取
页(page)是SQL SERVER可以读写的最小I/O单位。即使只需访问一行,也要把整个页加载到缓存之中,再从缓存中读取数据。物理读取是从磁盘上读取,逻辑读取是从缓存中读取。物理读取一页的开销要比逻辑读取一页的要大得多。
2.表的组织方式
总结:表有两种组织方式,在表建立的时候可以选择两种组织方式1,堆2,聚集索引(B树)。而在表上面可以选择是否加上非聚集索引(B树),
加上:1堆+非聚集索引,2聚集索引(B树)+非聚集索引(B树)
不加:3堆,4聚集索引(B树),其中有索引功能的是124这三种。
表有两种组织方式,B树(Balance Tree)或者堆(Heap)。当在表上创建了一个聚集索引的时候,整个表数据就以B数的结构排列。否则就是按照堆的结构排列。无论表是怎么组织的,都可以在表上面创建多个非聚集索引。非聚集索引都是以B树的结构排列。
2.1 堆(Heap)
之所以这个结构称为堆,是因为它不以任何人为指定的逻辑顺序进行排列。而是按照分区组队数据进行组织。也就是说,是按照磁盘的物理顺序。只要需要读取的数据文件没有文件系统碎片(注意和下面提到的索引的碎片区分),这个读取过程在磁盘中就可以连续的进行,没有多余的磁盘臂移动。而磁盘臂移动是I/O操作中开销最大的操作。
堆使用一个bitmap结构来管理数据的分配。也就是它会告诉你两个结果,这个区是分配了,还是没有分配。每一个区中的物理顺序如下图。
对于新插入的数据,堆只管在最后一条数据的后面的一个空闲位置保存新插入的数据,不保持任何的逻辑顺序。比如拿order表举例,如果先插入orderid 4,5,6, 假设在位置1:176、 1:177、1:178这三个位置。这时再插入1,这时保存的数据就变味4,5,6,1, 1保存在 1:179的位置。
2.2聚集索引(Clustered Index)
聚集索引以B树的方式保存数据。由于在另一篇文章中已经详细的分析了B树,这里就不再详细说明。
继续拿Order表举例,Order表中的全部数据都保存在B树中的叶层(leaf level)中,其他层只是起到一个索引的作用,并不包含任何数据。叶层是一个双向链表结构,并按照聚集索引的主键的逻辑顺序排列。因此逻辑顺序是用指针来维护。
重新说一遍,表数据按照索引的顺序来存储的。对于聚集索引,叶子结点即存储了真实的数据行,不再有另外单独的数据页。
我们在图中页层所见到是逻辑顺序,和上图堆中所展示的物理顺序要区分开来。
为什么我一再强调逻辑顺序和物理顺序?因为理解这很重要。
如图所示,聚集索引中除了B树之外,仍然维护了一个IAM结构,而这个结构就能保证在需要的时候,我们能按照物理顺序而不是逻辑顺序去在叶层中读取数据。
那么什么时候才需要呢?先看什么是索引碎片。
2.2.1 索引碎片
数据库中之所以会出现碎片,是因为B树的页拆分造成的。具体页拆分请参考数据结构,这里要说的是由于拆分所产生的新页不保证一定就会在被拆分的页的后面,而是可能出于文件的任何位置。这就是“无序页”。换句话说,也就是在列表中处于后面位置的元素,在物理文件中却排在前面。如果你明白指针的定义的话,这句话并不难理解。因为叶层的双向列表就是以指针来维护逻辑顺序。
因此在按逻辑顺序读取的时候,由于无序页的存在,可能造成磁臂频繁的摆动。别忘记,磁盘摆动是I/O中开销最大的操作。而I/O往往是一个系统的瓶颈所在。
如果按照物理顺序来读取,也就是unordered读取,就会避免上面所产生的问题。再次强调,unordered是指不按逻辑顺序读取,所以叫unordered。
2.2.2 索引的层数2)聚集索引与插入操作
索引的层数,也就是B树的高度,直接表明了一次查找操作在页面读取方面的开销。一些执行计划如Nested loop联接会多次调用查找操作。因此理解这个概念很重要。
树的高度主要和以下几个因素相关
- 表的总行数。
- 平均一行保存数据的大小。
- 页的平均密度。因为不是每一页都应该填充满数据,这样可以减少页拆分的次数。
- 一页所能容纳的行数。
具体公式也很简单,3级索引大概能容纳4百万行,4级索引大概能容纳4亿行数据。因此通常一张表的索引层数通常为3到4级。
另:聚集索引与插入操作
最简单的情况下,插入操作根据索引找到对应的数据页,然后通过挪动已有的记录为新数据腾出空间,最后插入数据。
如果数据页已满,则需要拆分数据页(页拆分是一种耗费资源的操作,一般数据库系统中会有相应的机制要尽量减少页拆分的次数,通常是通过为每页预留空间来实现):
A)在该使用的数据段(extent)上分配新的数据页,如果数据段已满,则需要分配新段。
B)调整索引指针,这需要将相应的索引页读入内存并加锁。
C)大约有一半的数据行被归入新的数据页中。
D)如果表还有非聚集索引,则需要更新这些索引指向新的数据页。
特殊情况:
A)如果新插入的一条记录包含很大的数据,可能会分配两个新数据页,其中之一用来存储新记录,另一存储从原页中拆分出来的数据。
B)通常数据库系统中会将重复的数据记录存储于相同的页中。
C)类似于自增列为聚集索引的,数据库系统可能并不拆分数据页,页只是简单的新添数据页。
另:聚集索引与删除操作
删除行将导致其下方的数据行向上移动以填充删除记录造成的空白。
如果删除的行是该数据页中的最后一行,那么该数据页将被回收,相应的索引页中的记录将被删除。如果回收的数据页位于跟该表的其它数据页相同的段上,那么它可能在随后的时间内被利用。如果该数据页是该段的唯一一个数据页,则该段也被回收。
对于数据的删除操作,可能导致索引页中仅有一条记录,这时,该记录可能会被移至邻近的索引页中,原索引页将被回收,即所谓的“索引合并”。
2.3非聚集索引(NonClustered Index)
非聚集索引也是以B树组织的。和聚集索引的区别就在于它的叶层并不包含所有的数据。在默认情况下它只包含了键列的数据,并包含了一个行定位符(row locator)。这个行定位符的具体内容取决于它建立在以堆形式的表还是以B树组织的表,换句话说也就是这张表是否建立了聚集索引会影响到非聚集索引的行定位符。如果是建立了聚集索引,那么这个行定位符就是一个聚集键,我们通过这个聚集键再次查找聚集索引上的数据。
重新说一遍,非聚集索引中,表数据存储顺序与索引顺序无关。对于非聚集索引,叶结点包含索引字段值及指向数据页数据行的逻辑指针,该层紧邻数据页,其行数量与数据表行数据量一致。
聚集索引上的非聚集索引
如果表是堆组织结构的,那么它就是一个直接指向数据所在行的物理指针。
下图是建立在堆上的非聚集索引
2.3.1 如果非聚集索引包含了我们需要查找的所有数据
这种情况我们通常叫做索引覆盖。
正因为非聚集索引有着和索引一样的结构,并且由于非聚集索引所包含的列少,因此数据量就小,使得叶层的一页能包含更多的行,因此进行一次I/O页读取的动作的时候,就能读取进更多的行。因此查找效率是最高的。
举个不恰当的例子,美女征婚,应征人员的个人信息表有 “姓名、 德、 智、 体 、美、 劳、 高、 富、 帅”这几列,按姓名排序。美女只关注“高、 富、 帅”这三列的内容,为了更快的筛选,我们帮美女按照个人信息表的内容重新制作了一张表,这张表忽略了其他信息,只保留了高、富、帅和姓名,筛选效率当然就比原来关注更多内容时要高。
2.3.2 如果非聚集索引不包含我们需要查找的所有数据
通俗的说这时我们就需要从非聚集索引中所包含的线索去包含所有数据的表中去找。
按照我们之前的定义换句话来说,就是通过非聚集索引中的行定位符去聚集索引或者堆中去查找所需的数据。
二、通过实例来说明上述概念
我们创建一张Order表,表上建立了几个索引
1.为orderdate列创建了聚集索引
2.为orderid列创建了非聚集索引
1.1.1 只为获取整张表的数据,对数据顺序不关心
SELECT [orderid]
,[custid]
,[empid]
,[shipperid]
,[orderdate]
,[filler]
FROM [Performance].[dbo].[Orders]
分析:由于我们需要获取整张表的数据,因此不需要任何筛选也不需要任何排序。因此我们按照磁盘物理顺序读取出所有数据无疑是最快的选择。 所以已排序为False. 再次说明这里的顺序是聚集键的逻辑顺序,和物理顺序不同。
通过IAM在聚集索引的叶层扫描。在这种情况下无论表是以堆或者B树的形式组织情况都类似。
(1000000 行受影响)
表'Orders'。扫描计数1,逻辑读取25081 次,物理读取5 次,预读23545 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。
1.1.2 按聚集键顺序获取整张表的数据
对于Orders表,以orderdate为聚集键,因此如果我们使用顺序查询,就可以直接获取所需要的数据。
这是我们就不再通过IAM来对叶层进行扫描,而是通过叶节点的指针来进行扫描。
1.1.3 如果不按照聚集键,而是按照其他列的顺序来获取整张表
我们并没有把orderid设置成聚集索引的键,而是把它设成了非聚集索引的键。因此在返回整张表的内容时:
1.非聚集索引键列orderid对我们没有意义,因为我们期望返回的是整张表的内容,而非聚集索引只包含键列的内容。
2.聚集键列orderdate的顺序在这里对我们是没有什么用的。
由上面的推论可以知道,这时我们所创建的索引对我们都没有任何帮助。因此,与其按照逻辑顺序返回,不如按照最快速的无序返回,再把返回的结果集排序。而计划证明了我们的猜想。
1.1.4 如果我们要查询的内容,正好在非聚集索引里面就已经包含了
和上面查询基本类似,区别在于我们在查询结果中把非聚集索引中不包含的列全部删除了,这时非聚集索引就形成了覆盖。我们就可以利用非聚集索引进行查询。
一些索引建议:
1.对于长字符串,比如VARCHAR(80)
这种类型的索引要比更为紧凑数据类型的索引大很多。同样地,你也不太可能对长字符串
列进行全匹配查找。
本来想一次写完,但是因为整体太长了,因此分开几段来写。
下一篇中我们将分析一些带有筛选条件WHERE的查询。
当然,如果觉得有帮助,请点一下推荐。
参考:http://www.cnblogs.com/lwzz/archive/2012/08/05/2620824.html