【译】索引进阶(六):SQL SERVER索引书签
【译注:此文为翻译,由于本人水平所限,疏漏在所难免,欢迎探讨指正】
原文链接:传送门。
在之前的章节,我们把索引看做一组有序条目的集合,每行数据对应一个索引条目。我们解释了很多关于索引逻辑方面的内容,集中在各个条目的数据上以及把它们包含进索引建序列所产生的影响上。因此到目前为止,我们已经涉及了非聚集索引条目的前两个组件:检索键(search key)和包含列(included columns)。本章节我们将检查它的第三个,也是最后一个组件:书签(bookmark)。
书签里是什么?
之前我们提到过书签,但仅仅是说它使得SQL SERVER从非聚集索引条目快速导航到所交互的行数据上。现在是时候深入书签的更多细节了,毕竟,书签的内容根据其底层表是堆或者是聚集索引而有所不同。
不管表是一个堆或者是一个聚集索引,表中的每一行数据都是其所在页的第N行,所在页都是其所在数据文件的第N个页,那个文件总是组成数据库的文件组的第N个文件。因此,数据库的每一行数据,在任何给定的时刻,都能被三个数字来标识:文件号,页号,行号。这种由三个数字组成的标识称为 行ID(row id),通常简写为RID。显示SQL SERVER内部信息的大部分工具都会以冒号分割的形式来显示这三个数字,因此1号文件的77页的12行数据将会有一个这样的RID: 1:77:12。
一般而言,堆的数据行不会移动,一旦它们被插入进一个页中,便会一直在那个页中。从技术角度更精确的说,堆中的数据行很少移动,当它们确实发生移动时,便会在原来的位置留下跳转地址。然而,聚集索引的数据行可以移动,也就是说,在数据更新或者索引重组时,它们能被重新定位到另一个页。关于数据行被更新时会发生什么的更多细节(包含关于跳转行的信息)将在后续章节给予介绍。
既然堆的数据行不会移动,那么RID 便可以永久的标识堆中的数据行,这个值不仅是恒定的,它也实际确定了行的位置,这使得它成为了在堆的非聚集索引中表示书签值的理想值,这便是SQL SERVER在堆上创建的非聚集索引中使用它(RID)的原因。
堆的非聚集索引:基于RID的书签
我们说表SalesOrderDetail 是一个堆,它的数据行没有特定的顺序,然后我们创建了在第五节使用的索引:在ProductID / ModifiedDate列建立的非聚集索引,并包含了包含列,如下所示:
CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate ON Sales.SalesOrderDetail(ProductID, ModifiedDate) INCLUDE (OrderQty, UnitPrice, LineTotal)
在这个非聚集索引中,按序排列的条目看起来像是这样:
:- Key Columns -: :--- Included Columns ---: : Bookmark :
ProductID ModifiedDate OrderQty UnitPrice LineTotal
----------- ------------ -------- --------- --------- -------------
Page n-1:
709 01 Feb 2002 1 5.70 5.70 3:9198:41
709 01 May 2002 1 5.70 5.70 3:969:2
710 01 Jul 2001 1 5.70 5.70 3:9840:29
710 01 Jul 2001 1 5.70 5.70 3:9916:29
710 01 Sep 2001 1 5.70 5.70 4:12331:32
Page n:
710 01 Oct 2001 1 5.70 5.70 3:1911:33
710 01 Nov 2001 1 5.70 5.70 4:2604:34
710 01 Nov 2001 1 5.70 5.70 4:2889:34
710 01 Nov 2001 1 5.70 5.70 3:3522:35
710 01 Nov 2001 1 5.70 5.70 3:3623:35
710 01 Jun 2002 1 5.70 5.70 4:3917:5
712 01 Jul 2001 1 5.19 5.19 3:9886:29
712 01 Jul 2001 1 5.19 5.19 3:10270:29
712 01 Aug 2001 1 5.19 5.19 4:10609:30
712 01 Aug 2001 1 5.19 5.19 4:10617:30
Page n+1:
712 01 Aug 2001 1 5.19 5.19 4:10689:30
712 01 Aug 2001 1 5.19 5.19 4:10885:30
712 01 Aug 2001 1 5.19 5.19 4:11002:30
712 01 Sep 2001 1 5.19 5.19 4:12318:32
712 01 Sep 2001 1 5.19 5.19 4:509:32
我们的索引行的书签值非常高效,直接指向交互的数据行,但是它们取决于底层的表是一个堆,尽管这些值对于行查找来说非常高效,但是它们没有包含对于用户来说有价值的信息。
这种基于RID的书签的替代方案便是在聚集索引上创建非聚集索引时会用到的书签。简洁精确的说,便是聚集索引表的非聚集索引所使用的书签。
聚集索引的非聚集索引:基于键值的书签
如果表是一个聚集索引,那么数据行便能在表中重新定位,因此,对于聚集索引来说,RID不能永久标识一个数据行,因而一个不同的值必须被用作此非聚集索引的书签值,这个被用到的值便是聚集索引的索引键。
这解决了需要不变的书签值的需求。因为当聚集索引的一个数据行被移动至新页时,它仅仅被移动,并没有被更新,聚集索引键值便没有变化,因此,此书签值总是可以返回与之交互的行,这就是说数据行会通过索引键返回,而不是通过其实际的物理位置返回。
然而,聚集索引键用作非聚集索引的书签值这种用法意味着聚集索引应该满足三个条件:
聚集索引必须是唯一的。每一个索引条目书签必须允许SQL SERVER找到与此条目交互的一行数据,如果你创建一个不唯一的聚集索引,SQL SERVER将通过生成额外的值来保证聚集索引的唯一性,这个由SQL SERVER生成的用来保证唯一性的值成为 标识符(uniquifier ),其对任何客户端程序是透明的。基于如下原因,你应该仔细考虑是否允许SQL SERVER在聚集索引中产生重复:
- 生成标识符需要额外的开销,在插入时,SQL SERVER必须决定,是否新插入的数据行的键与已存在的数据行的键产生重复,如果是,那么便产生标识符加到新行上。
- 标识符是一条无意义的信息,并且被加在表的非聚集索引中,通常给非聚集索引加上有意义的信息会是更好的做法。
索引应该简短。也就是说它应该包含少量的字节数,因为它将在所有的非聚集索引中进行传播。Contact 表的 Last name / first name / middle name / street address这几个字段作为聚集索引或许看起来像是一个好主意,但是如果一个表存在多个非聚集索引,它便不是一个好主意了。如果一个表包含N个非聚集索引,那么contact表的 last name / first name / middle name / street address 值便会存储在N+1个位置。
索引应该是静态的。也就是说,它的值应该很少变化,对于一个数据行来说,索引键值的改变强迫各个非聚集索引中此行的条目更新为新键值。因此,对于一个包含N个非聚集索引的表来说,聚集索引键的更改会导致N+1个更改(还不包含 N+1个日志文件的插入)。
当AdventureWorks 数据库的设计团队为SalesOrderDetail 表选择聚集索引时,他们坚持了如上三个指导原则。通过选择SalesOrderID / SalesOrderDetailID 作为聚集索引键达到了窄值,静态以及唯一性的要求。通过添加SalesOrderID 作为索引键的最左列,即使SalesOrderDetailID是唯一的,索引也具有一定的聚集性。一个订单的所有行分组在相同的一两个页上。通过设置SalesOrderID / SalesOrderDetailID 作为表的主键以及聚集索引键,他们消除了在SalesOrderDetailID列上建立单独索引的需求。
没有人会要求去看条目7的数据,他们会要求去看订单#47386的所有条目,或者是订单#47386的#7条目,最重要的是,就如我们说过几次,它使得表处在一个能够最好服务于各个程序的顺序。现在,如果我们在聚集索引表上创建了之前的非聚集索引:
CREATE NONCLUSTERED INDEX FK_ProductID_ModifiedDate ON Sales.SalesOrderDetail(ProductID, ModifiedDate) INCLUDE (OrderQty, UnitPrice, LineTotal)
那么此非聚集索引看起来会是这样:
:- Key Columns -: : --- Included Columns ---: :--- Bookmark ---:
ProductID ModifiedDate OrderQty UnitPrice LineTotal OrderId DetailId
----------- ------------ -------- --------- --------- ----------- ----------
Page n-1:
709 01 Feb 2002 1 5.70 5.70 45329 6392
709 01 May 2002 1 5.70 5.70 46047 8601
710 01 Jul 2001 1 5.70 5.70 43670 111
710 01 Jul 2001 1 5.70 5.70 43676 152
710 01 Sep 2001 1 5.70 5.70 44075 1448
Page n:
710 01 Oct 2001 1 5.70 5.70 44303 2481
710 01 Nov 2001 1 5.70 5.70 44484 2853
710 01 Nov 2001 1 5.70 5.70 44499 3006
710 01 Nov 2001 1 5.70 5.70 44523 3346
710 01 Nov 2001 1 5.70 5.70 44527 3400
710 01 Jun 2002 1 5.70 5.70 46365 10183
712 01 Jul 2001 1 5.19 5.19 43673 136
712 01 Jul 2001 1 5.19 5.19 43694 342
712 01 Aug 2001 1 5.19 5.19 43846 524
712 01 Aug 2001 1 5.19 5.19 43847 528
Page n-1:
712 01 Aug 2001 1 5.19 5.19 43851 567
712 01 Aug 2001 1 5.19 5.19 43863 672
712 01 Aug 2001 1 5.19 5.19 43871 735
712 01 Sep 2001 1 5.19 5.19 44074 1441
712 01 Sep 2001 1 5.19 5.19 44109 1729
我们的两个非聚集索引版本:之前在堆上创建的版本和在聚集索引上创建的版本,除了书签的内容不同外,其他地方是完全相同的。
哪一个更好?
这些设计中的一个比其他的更好吗?或许是吧,但是也好不了太多。基于RID 的书签允许在表中对数据行进行快速查找,索引键书签导致了对应底层表的更慢的数据行查找,但增加了索引覆盖查询的可能性,它通常包含了一个用来做JOIN连接的外键值。
“哪个是更好的?”这个问题的真正答案是:“两者都不”。当为一个表创建索引时,最重要的决策是:哪个索引应该成为表的聚集索引?一旦作出决策(根据本章早些时候提到的指导方案),你不必担心那个决策对于非聚集索引的影响,它们将表现良好。
结论
一个非聚集索引条目包含了检索键,包含列,书签。书签值要么是RID,要么是聚集索引键,这决定于底层表是堆还是聚集索引。对于表来说,选择最合适的聚集索引需要你遵守上述三个指导准则,这样可以确保这个索引键会成为良好的书签值。