SQL Server索引进阶 第六篇:书签

2016-3-3 123+ 分享:

本文是SQL Server索引进阶系列(Stairway to SQL Server Indexes)的一部分。

 

书签是什么

    我们已经在前面提到过书签,但仅仅说了书签可以帮助SQL Server快速从非聚集索引条目导向到对应的行,本篇文章开始让我们对书签的探索更进一步.书签的内容实际上是取决于非聚集索引所在表是以堆还是聚集索引存放的。

     不论表是堆结构还是段结构,可以确定的是,表中每一行都是某一页的第N行,这个某一页又是某个数据库文件的第N页,这个某个数据库文件又是构成数据库的文件组的第N个文件,因此,数据库中的每一行,在指定时间都可以由三个数字进行定位:  文件号:页号:行号。这三个数字组合起来就是所谓的RID。很多显示SQL Server内部结构的工具软件都会将这三个数字通过冒号分隔进行显示。比如,文件1的第77页的第12行的RID就是1:77:12。

    通常来说,在堆上的行不会被改变位置。一旦它们被插入某个页中,它们就会一直呆在那。如果要用更严谨的技术术语来说的话:在堆上的行很少移动。如果行被移动的话,它们会在原来的位置留下指向其移动到的位置的指针。而由聚集索引组织的行,是可以被移动的,行在改动数据或是整理索引的时候要被移动位置。更多的细节会在本系列文章后续篇幅进行介绍。

    因为在堆上的行几乎很少移动,所以RID就可以唯一标识某一行。RID的值不仅仅不变,RID所表示行的物理位置也不会变。这使得RID的值更适宜作为书签。这也是为什么SQL Server在堆上建立的非聚集索引的书签都使用RID。

 

堆上的非聚集索引:基于RID的书签

    假如SalesOrderDetail是一个基于堆的表;表中的每行都不是有序的。下面让我们建立以ProductID/ModifiedDate为非聚集索引键并包含OrderQty, UnitPrice, LineTotal三个列的非聚集索引,如代码6.1所示。

CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate 
ON 
Sales.SalesOrderDetail(ProductID, ModifiedDate) 
INCLUDE (OrderQty, 
UnitPrice, LineTotal) 

 

 

 

代码6.1 建立含有包含列的非聚集索引

  

    在上面索引中,部分数据的顺序如下所示。

index6

 

当执行下面的查询语句时

SELECT ProductID, SUM(Count) AS 'TotalCount', AVG(Price) AS 'AvgPrice'
FROM t_OrderItem
WHERE ProductID = 888
GROUP BY ProductID
 
SQL SERVER的执行计划如下所示:

    上面建立的非聚集索引因为使用了RID作为书签,直接指向对应行所在的物理地址,因此十分高效。但虽然RID值用于键查找非常高效,但书签中包含的值和具体的用户数据无关。

     另一种与这种基于RID的书签不同的书签,是当非聚集索引所在的表包含聚集索引时出现的。更准确的说,这种书签是建立聚集索引上的非聚集索引的书签。

 

在聚集索引上的非聚集索引:基于键值的书签

    如果表是基于聚集索引的,则表内数据可以在表移动。因此,对于聚集索引来说,RID并不能一直不变的定位一个相同的行。因此必须用另外的方法定位行,这个方法就是使用聚集索引的索引键。

    使用聚集索引键作为书签可以使得当数据在页中的行改变时,不需要非聚集索引的书签的值进行变动,因此非聚集索引的键就可以用于去找底层表的数据,意思是根据书签取数据不再基于物理位置,而是基于聚集索引查找。

    然而,以聚集索引键作为非聚集索引的书签最好要聚集索引键满足如下标准:

 

   索引应该具有唯一性. 每一个索引条目书签都应该使得书签可以通过聚集索引的键值唯一的确认表中的一行,如果你创建的聚集索引键值不唯一,SQL Server将会为有重复键值的每一行自动加上一个叫uniquifier的东西使得每一行唯一。这个uniquifier对客户端是透明的。对于是否可以允许聚集索引键重复,你需要考虑以下两点:

  •     生成uniquifier增加SQL Server插入操作的额外负担,在插入时SQL Server还需要判断插入的值在表中是否唯一,如果不唯一生成uniquifier值再进行插入。
  •     uniquifier本身对业务数据来说是没有意义的,但是这个uniquifier本身不仅仅需要占用聚集索引键的空间,还同时占用非聚集索引书签的空间。

 

   索引键应该短.索引键所占的字节数应该短.因为这个键还会占用非聚集索引书签的空间。比如Contact表中以Last name / first name / middle name / street组合作为索引键看上去不错,但如果表中存在多个非聚集索引的话情况就有些微妙了。n个非聚集索引使得Last name / first name / middle name / street这些字段被存储在n+1个位置。

    索引键最好不要变动.也就是索引键的值最好不要变动。对于聚集索引键的修改会使得基于这个聚集索引的所有非聚集索引同样进行修改。所以对于聚集索引的一次update会造成n个非聚集索引书签的update+1个聚集索引键值本身的update。

 

    AdventureWorks的设计团队在设计SalesOrderDetail表的聚集索引时就是完全遵循上面的建议。它们选择SalesOrderID / SalesOrderDetailID作为聚集索引键完全满足了窄,短和唯一的要求。将SalesOrderID作为索引键最左边的一列,尽管SalesOrderDetailID是唯一的,这两列组合在一起进行聚集。以SalesOrderID / SalesOrderDetailID作为主键和聚集索引键,就不再需要单独建立SalesOrderDetailID的非聚集索引了。

    现在我们创建和列表6.1所示的非聚集索引一样的索引。唯一的不同是现在这个版本是基于聚集索引而不是堆的。非聚集索引的部分数据如下:

 

index62

 

我们现在有了两个版本的非聚集索引,分别是创建在堆上的非聚集索引和创建在聚集索引上的非聚集索引,唯一的不同就是它们的书签值。

 

哪种更好

     上面两种聚集索引是不是一种要比另一种更好呢?或许吧,但是也要看具体情况。基于RID的书签允许快速的找到底层表中行所在的物理位置,而基于聚集索引的书签找到底层表的行就慢多了,但这个书签还可以作为包含列使用,书签列同时也常常会被当作外键。

     所以对于上面哪种非聚集索引更好的真正答案是”都不是”。当在表上建立索引时,最重要的选择只是使用哪些列作为索引键。一旦你确定了聚集索引列(基于文中所示的三点建议),剩下的非聚集索引所带来的影响就交给SQL Server来处理吧。

 

小结

     非聚集索引包含了索引键列,包含列和书签。书签的值根据所在表是堆还是聚集索引既可以是RID也可以是聚集索引键。对于表上聚集索引的最好选择要基于文中所给的三点指南。