SQL Server 数据库引擎实例的 I/O 包括逻辑读取和物理读取。 每次数据库引擎从 缓冲区缓存(也称为 缓冲池)请求页面时,都会发生逻辑读取。 如果页面当前不在缓冲区缓存中,物理读取首先会将该页从磁盘复制到缓存中。
数据库引擎实例生成的读取请求由关系引擎控制,并由存储引擎进行优化。 关系引擎确定最有效的访问方法(例如表扫描、索引扫描或键读)。 存储引擎的访问方法和缓冲区管理器组件确定要执行的读取常规模式,并优化实现访问方法所需的读取。 执行批处理的线程将安排读取。
预读
数据库引擎支持称为“预读”的性能优化机制。 预读功能会提前读取并加载查询执行计划所需的数据页和索引页到缓冲区,在查询使用之前提前缓存它们。 此过程允许计算和 I/O 重叠,充分利用 CPU 和磁盘。
预读机制允许数据库引擎从一个文件中读取多达 64 个连续页(512 KB)。 读取过程作为一次连续读取操作执行,分配到缓冲区缓存中的适当数量的(可能不连续的)缓冲区。 如果缓冲区缓存中已存在该区域中的任何页面,则读取完成后,将放弃读取中的相应页面。 如果缓存中已有相应的页面,则从任一端“剪裁”页面范围。
有两种类型的预读:一种用于 数据页 ,另一种用于 索引页。
读取数据页
数据库引擎用于读取数据页的表扫描效率高。 SQL Server 数据库中的索引分配映射 (IAM) 页列出了表或索引使用的区数。 存储引擎可以读取 IAM 以生成必须读取的磁盘地址的排序列表。 这使得存储引擎能够根据要读取的磁盘位置,将其 I/O 操作优化为按顺序执行的大型顺序读取。 有关 IAM 页的详细信息,请参阅 管理对象使用的空间。
读取索引页
存储引擎按键的顺序依次读取索引页。 例如,下图显示了一组叶级页的简化表示法,该组叶级页包含映射叶级页的键集和中间索引节点。 有关索引中页面结构的详细信息,请参阅 聚集索引和非聚集索引。
存储引擎使用高于叶级的中间索引页上的信息为包含键的页安排序列预读。 如果请求从ABC到DEF的所有键,存储引擎会首先读取叶页上方的索引页。 但是,它并不仅仅是从第 504 页到第 556 页(即包含指定范围键的最后一页)依次读取每个数据页。 相反,存储引擎将扫描中间索引页并生成必须要读取的叶级页的列表。 然后,存储引擎会按键的顺序安排所有读取。 存储引擎还会识别出页 504/505 以及页 527/528 是相邻页,并执行一次散播读取,从而在单个操作中检索这些相邻页。 如果在一个序列操作中要检索许多页,则存储引擎将一次安排一个读取块。 完成这些读取任务的部分后,存储引擎将安排相同数量的新读取,直至安排所有必需的读取。
存储引擎使用 预提取 来加快非聚集索引中基表查找的速度。 非聚集索引的叶级行包含指针,指向含有每个特定键值的数据行。 当存储引擎读取非聚集索引的叶页时,它还会开始计划对已检索到指针的数据行进行异步读取。 这样,存储引擎就可以在完成非聚集索引扫描之前从基础表检索数据行。 无论表是否有聚集索引,都会使用预提取。 SQL Server Enterprise 版本使用比其他版本的 SQL Server 更多的预提取功能,允许提前阅读更多页面。 预提取级别在任何版本中都不可配置。 有关非聚集索引的详细信息,请参阅 聚集索引和非聚集索引。
高级扫描
在 SQL Server Enterprise 版本中,高级扫描功能允许多个任务共享完整表扫描。 如果 Transact-SQL 语句的执行计划需要扫描表中的数据页,并且数据库引擎检测到其他执行计划正在扫描该表,则数据库引擎会在第二个扫描的当前位置将第二个扫描加入第一个扫描。 数据库引擎会一次读取一页,并将每一页的行传递给这两个执行计划。 此操作将一直持续到该表的结尾处。
第一个执行计划此时已经完成扫描并获得完整结果。 但是,在加入正在进行的扫描之前,第二个执行计划仍必须检索已读取的数据页。 然后,第二个执行计划中的扫描将绕回到表的第一个数据页,并从这里向前扫描到它加入第一个扫描时所处的位置。 可以按这种方式组合任意数量的扫描。 数据库引擎会一直循环浏览数据页,直到完成所有扫描。 此机制也称为“旋转扫描”,它展示了为何在没有ORDER BY子句的情况下,无法保证SELECT语句返回结果的顺序。
例如,假设某个表有 500,000 页。
UserA 执行需要扫描表的 Transact-SQL 语句。 当扫描处理了 100,000 页时, UserB 执行另一个扫描同一表的 Transact-SQL 语句。 数据库引擎将为页 100,001 之后的页安排一组读取请求,并将每页中的行同时传递回两个扫描。 当扫描到达第 200,000 页时, UserC 执行另一个扫描同一表的 Transact-SQL 语句。 从页 200,001 开始,数据库引擎将其读取的每一页中的行传递回所有三个扫描。 读取第 500,000 行后,扫描 UserA 已完成,UserB 和 UserC 的扫描回绕,并开始读取从第 1 页开始的页面。 当数据库引擎读取到第 100,000 页时,扫描 UserB 完成。 扫描 UserC 然后继续独立运行,直到扫描到第 200,000 页。 此时,所有扫描均告完成。
在没有高级扫描的情况下,每个用户都必须要争用缓冲区空间并因此导致磁盘臂争用。 然后,会分别为每个用户读取一次相同的页,而不是一次读取并由多个用户共享,这样会降低性能并加重资源负担。