【译注:此文为翻译,由于本人水平所限,疏漏在所难免,欢迎探讨指正】
原文链接:传送门。
在之前的章节中,我们在有索引和没有索引的情况下执行同一个查询,比较了各自需要的工作。我们的最主要的评判标准是“逻辑读”。我们总是比较查询聚集索引表所需要的读与查询无索引表所需要的逻辑读。现在是时候解释为什么逻辑读是比较合适的标准,同时我们也会解释实际上读了什么。
当你向SQL SERVER提交一个请求信息的申请,它知道通过扫描整张表总是能够满足那个查询,SQL SERVER理解仅当如果使用索引比表扫描做更少的工作时,索引才对查询是有益的。如果你问SQL SERVER:是什么工作?它的回答首先便是:磁盘IO。一个查询所需要的磁盘IO是这个查询消耗的良好体现,最主要的原因在于IO消耗了两种关键资源,时间和内存。
扫描整张表所需要的IO是一个总是被误解的标准,其原因在于SQL SERVER并不是按行读取,它读取的是页。而读取页和读取行是完全不同的工作单元。
这个章节会尽可能的短,因为我们只关注于SQL SERVER如何执行IO。理解SQL SERVER IO,对于我们理解为什么一些索引有益于查询而另一些并没有益处是很有必要的,也可以理解为什么一些数据更新会比其他的执行得更快些,以及为什么一些索引维护任务比其他会需要更少的时间。简而言之,对于SQL SERVER IO的基本理解对于理解本系列的后续章节是很有必要的。
页
当你创建一个数据库时,你指定了你的数据库被定位的文件,SQL SERVER把各个文件看做是长长的字节字符串,它在逻辑上而不是物理上将文件分割成8K大小的块。这些8K大小的块成为页。因此文件的第一个8K大小的字节块就是页#0,下一个8K是页#1,如此等等,页是最小的IO单元,每次IO,SQL SERVER只是读写一个页,如果多个连续的页需要读或者写,SQL SERVER选择在一个单独的IO中来处理它们。
页不仅仅是IO的最小单位,它还是所有关系的最小单位,也就是说,如果一个页包含了表A的行,那么它将只包含表A的行,反之如果它包含了非聚集索引B的一个条目,它将只会包含非聚集索引B的条目,除了数据之外,每个页都包含了一些头信息及偏移指针,用以帮助SQL SERVER来定位到页中的各个行数据或者条目。
在前面的章节中,我们展示了几张图片来演示索引条目的顺序,而没有管它是聚集索引或者非聚集索引,接下来我们对其中的一个图片进行扩展,展示SalesOrderDetail 表的聚集索引,来反映页的概念。
SalesOrderID SalesOrderDetailID ProductID OrderQty UnitPrice
Page n-1:
43668 106 722 3 178.58
43668 107 708 1 20.19
Page n:
43668 108 733 3 356.90
43668 109 763 3 419.46
43669 110 747 1 714.70
43670 111 710 1 5.70
43670 112 709 2 5.70
43670 113 773 2 2,039.99
43670 114 776 1 2,024.99
43671 115 753 1 2,146.96
43671 116 714 2 28.84
43671 117 756 1 874.79
Page n+1:
43671 118 768 2 419.46
43671 119 732 2 356.90
43671 120 763 2 419.46
43671 121 755 2 874.79
43671 122 764 2 419.46
43671 123 716 1 28.84
43671 124 711 1 20.19
43671 125 708 1 20.19
43672 126 709 6 5.70
43672 127 776 2 2,024.99
Page n+2:
43672 128 774 1 2,039.99
43673 129 754 1 874.79
43673 130 715 3 28.84
43673 131 729 1 183.94
页的逻辑顺序和物理顺序没有必要是一样的。上图展示的示例数据是第n,n+1,n+2页,但实际上可能是n,n+9,n-5 and n+2。这种逻辑顺序和物理顺序的偏差称为外部碎片。一个页内空闲容量的百分比称为页的内部碎片。在后续章节中,我们会详细查看两种类型碎片的原因,影响及防范。
同样也没有要求每个页包含一样的行数,通常,在聚集索引表上常规的添加及删除操作会导致每个页包含类似的行数,更正确的说,各个页大约包含相同的字节数,如果数据行或者属于条目包含可变长的列,那么每页的行数可能会变化,尽管每页的字节数保持稳定。
一个数据行的大小等于它的列的大小加上行开销。行开销的数据取决于好多个因素,可总结 如下:
- 每行6个字节,存储状态信息和长度信息。
- 每个确定宽度的列一个bit,包括进最近的字节。
- 如果存在任何可变长度的列,第一个四个字节,其他可变长度的列每个占用两个字节。
- 在页的末尾,每行两个额外的字节用来存储偏移指针。
因为SalesOrderDetail 表包含可变长度列,它们的大小不是预先设定的,但是平均每行95字节,因为一个页的大小是8K,SalesOrderDetail表每页包含的行数大约是75,超过了我们例子中的每页10行,在后续章节我们将讨论SQL SERVER管理工具,你可以用它来决定这些数字。
尽管我们常常说SQL SERVER读取数据行,但这么说常常会造成误解,SQL SERVER不会读取行,它最少读取一个页。在说“给定一个索引键值,SQL SERVER能够快速访问到一行”时候,我们已经造成了误解,更正确的说法是,给定一个索引键,一个索引允许SQL SERVER快速访问一个页而不是一个数据行,一旦SQL SERVER将一个或者多个页载入内存,它会检查内存中的页并定位到所请求的数据行。
区
SQL SERVER在页上做了另一个逻辑分组,它将8个连续的页分成一个逻辑单元,称为区。
通常,区和页一样,是所有关系的单位,如果区中的一个页从属于表A或者索引B,那么区中的所有8个页都会从属于一个相同的对象。对于很小的表或者索引来说,情况会有所不同,它们不能充满整个区,在这种情况下, 超过一个表或者索引会被定位到一个相同的区中,但对于大部分对象来说,区仍是所有关系的单位。
因此,SQL SERVER并不认为表扫描是读取表的所有行,它认为表扫描是读取表的所有页或者区,它知道它将很可能并发的处理8K,64K的IO请求来读取整个表,这将使得表扫描变得不至于像每行读取那样吓人。
读取页和区不仅意味着做一个表扫描比我们期望的具有更少的工作,它还意味着,为了从非聚集索引受益,一个查询应该比我们期望的更具有选择性。
考虑如下假设性的查询,查询了SalesOrderDetail 表大约4%的数据行。
既然每25行数据就会有一行被选中,既然在ProductID 非聚集索引中,条目被分组放置在一起,使用ProductID 非聚集索引来定位表中的数据行看起来似乎是个好主意。但是再认真想一想。
由于表的聚集索引是SalesOrderID / SalesOrderDetailID,而不是ProductID顺序,因此,如果平均每页包含75行,我们的歘查询从25行中选择一行,那么平均每个页变会包含3个请求行,那么平均每个页都包含了至少一个请求行,换句话说,几乎每个页都 需要被读取来满足这个查询,那么用表扫描来读取它们是最好的选择,一次读取一个区,平均每次都装在24个请求行进入内存(3*8)。
SQL SERVER新手常常问的一个问题是:对于非聚集索引的使用来说,一个查询应该具有何种可选择性?在本章,你现在知道了这个问题的一个答案:应该比每页一行更具有选择性,后续章节提供的信息会使你变得更精确于决定哪个索引是有益的,而哪些毫无益处。
结论
SQL SERVER不会读取数据行,它读取一页或者多个页的数据,作为最小的IO单元,它的大小是8K,区是数据文件中8个连续的页,正常情况下,一个区以及它们的页包含一个单独对象的行或者条目,不管它是一个堆还是一个聚集索引,由于大的IO单元提供的高效性,一个查询必须更具有选择性才能从非聚集索引中受益。
在第五章,我们将看下你能做的一些事来增加聚集索引有益于查询的可能性:使索引变成一个包含索引(index becomes a covering index )。
换句话说,下一章是关于给索引添加包含列( included columns)。