本文属于SQL Server T-SQL执行内幕系列


    关系型数据库的数据访问操作总是从内存的缓存中读取数据而不是从磁盘中读取。这个缓存称为Buffer Pool。如果数据访问操作符未能在缓存中找到所需的数据,那么就需要从磁盘中加载,这就会产生一个磁盘I/O读(set statistics io on中的物理读),并且需要等待这个物理读完成(及从磁盘找到数据并加载到缓存为止)之后,才能进行操作。在Buffer Pool中的数据是共享给所有查询以便更高效地使用资源和降低不必要的I/O请求。

    SQL Server会尽可能多地读取数据到缓存中,直到系统中所有内存分配给SQL Server(通过修改最大服务器内存限制来控制),才会增加分配给私有内存的进程。数据缓存是SQL Server占用内存的最大部分,热数据的缓存能够极大地降低数据访问和检索速度从而提高性能,所以千万别再说SQL Server吃内存,服务器那么多的内存放在那里干嘛?注意I/O读写操作并不作用在单独的行而是以8KB页为操作单位。

堆操作

    首先来看看堆表上的读取操作。在堆上进行扫描操作(注意堆只有扫描操作)的过程:

  1. 第一次调用next()时,操作符需要找到第一行然后返回。SQL Server存储关于表的元数据用于描述哪个页属于这个表。管理对象使用的空间Inside the Storage Engine: IAM pages, IAM chains, and allocation units。数据访问操作符会对缓冲池中的页请求一个引用,返回指向所请求页的内存中的副本指针。如果页不在内存中,那么请求会被阻塞直到页从磁盘加载到内存为止。页中包含了每个数据记录的一个数组(偏移量)。这个数据记录不一定是完整的一行,因为有LOB值和变长值存在,可能需要存储在另外的页,参考Inside the Storage Engine: Anatomy of a record。数据访问操作符会定位页中第一行,复制出请求所需的列值然后返回。同时会保存一个内部状态用于高效地回到这个位置。
  2. 父操作符处理操作符返回的第一行数据
  3. 当操作符再次调用next()方法时,会使用前面建立的上下文状态去快速找到当前页和行,然后按前面的方式处理下一行。
  4. 父操作符同样处理子操作符返回的下一行数据。
  5. 这种操作直到属于这个表的最后一个页的最后一行被处理完为止。同时它的内部状态会标识“超出表的尾部”,不能再返回任何行。
  6. 当数据访问操作符不能在返回任何数据给父操作符时,父操作符就开始做自己需要操作的事情,比如一个SORT操作符现在就可以开始返回数据。
  7. 数据操作符可以后退(rewind),比如,如果堆扫描操作是嵌套循环操作中的内表,当完成时,父嵌套循环将请求外表的下一行,然后后退到数据访问操作符(内表)并再次循环。后退将导致重置内部位置状态,并使其从第一页的第一行重新开始。

 B-Tree操作

    对于B-Tree结构的数据访问操作:

  1. 第一次调用next()方法时,操作符必须找到第一行基于键请求的数据并返回。SQL Server 存储关于B-Tree的元数据用于描述那个页属于这个索引,但是和堆表不同,堆表必须从第一页开始,而B-Tree可以直接定位到特定的键。从元数据中查找跟节点页的ID然后请求Buffer Pool中这个页的指针。使用查找键值,数据访问操作符定位B-Tree中包含第一行等于键值或者大于搜索键值的叶子页。在这个过程中,每一步都需要从BufferPool中找到对应的页,一旦没有找到,也会发生磁盘加载到内存的情况。在叶子页中,数据访问操作符通过预期的键值搜索页中的行,并且复制所需的数据然后返回。不过也存在找不到符合键值的行。由于B-Tree不仅可以定位行,还能找到大于键值的数据,所以B-Tree可以双向查找,这个“大于”依赖于键的定义。这个跟索引定义中的升序或降序不一样,索引定义中的顺序是B-Tree的行的实际顺序。
  2. 父操作符处理子操作符返回的结果。
  3. 如果操作符使用范围扫描,那么next()方法会被再次调用用于查找在前面数据的后面的值并返回。B-Tree访问操作符会存储前面返回的值的键值并用于定位,同时使用相同的过程来返回下一页的第一行符合要求的数据。
  4. 父操作符继续处理返回结果。
  5. 范围扫描包含了范围键值的尾部,再次调用next()方法时,如果行被移到高于范围值尾部的地方。则会返回false。B-Tree中的高于包含了升序或排序。
  6. 对于rewind,B-Tree同样可以重绑。倒带复位操作员状态, 以使用相同的键/范围参数重新启动查找/扫描。重新绑定将更改实际的键值。
  7. 扩展内容可见:Showplan 逻辑运算符和物理运算符参考

总结

    从两类操作可见,对于数据读取,都是从缓存中通过元数据的记录去查找数据,并且以递归循环的方式遍历。



本文转载:CSDN博客