SQL Server 由两个主要引擎组成∶关系引擎(relational engine)和存储引擎(storage engine)。
关系引擎有时称为查询处理器,因为关系引擎的主要功能是进行查询的优化和执行。关系引擎包含三个主要部分,命令解析器(command parser)、查询优化器(query optimizer)和查询执行器(query executor)。命令解析器用于检查查询命令的语法和生成查询树,查询优化器大概是任何数据库系统中最重要的一部分,查询执行器负责查询命令的执行。
命令解析器(command parser)负责处理 T-SOL 语言事件。首先,命令解析器会检查命令的语法结构,如果有任何语法错误,命令解析器将错误返回给协议层,然后协议层将错误返回给客户端。如果命令的语法结构正确,命令解析器会根据查询命令生成一个查询计划(query plan)或寻找一个已存在的计划。查询计划详细描述了 SOL Server 如何执行一段 T-SOL 代码,因此也常称为执行计划(execution plan)。
为了检查已经存在的查询计划,命令解析器首先对查询命令的 T-SOL代码进行散列(hash)运算得到一个散列值,然后在计划缓存里面查找有没有匹配的查询计划。
计划缓存是缓冲池的一部分,用来缓存查询计划。如果在计划缓存中找到了匹配的查询计划,命令解析器则直接从缓存中读取相应的查询计划,并将其传给查询执行器执行。
生成执行计划是一项非常耗时和耗费资源的过程,因此,对已经生成的执行计划进行重用可以显著提高 SOL Server 查询命令的效率。
如果命令解析器没有在计划缓存中找到相应的匹配,就会生成一个基于 T-SOL 的查询树。查询树是一种数据库内部的数据结构,树中的每个节点表示一个查询所需执行的操作。查询树随后被传递到查询优化器进行处理。
查询优化器(Query Optimizer)是一种”基于开销(cost-based)”的优化器,这种优化器评估多种可能的执行查询的方式,并从中挑选出优化器认为执行开销最低的方案作为优化结果。查询优化器将这个最优化的结果生成查询计划,作为它的输出。
这里要注意,查询优化器是在合理的时间内找出较好的查询计划,而不是最佳计划。通常,人们将查询优化器的目标定位为找到最有效的计划。
如果查询优化器每次都试图找到一个最优计划,那么它查找这个最优计划的时间可能比只执行未优化的较慢的计划还要长(一些内置的启发式算法保证了查询优化器绝不会花费比简单执行未优化的慢计划还要长的时间去查找一个好的计划)。
查询优化器不仅是一个基于开销的优化器,还是一个执行多阶段优化的优化器,其中优化的每一阶段都会增加可用于寻找好的计划的决策。一旦找到了好的计划,它就会在那一阶段停止进行优化。
优化的第一阶段称为预优化(pre-optimization),如果查询语句很简单,最有效的执行计划显而易见,查询优化器将直接在这一步结束优化,得到优化结果,从而避免了更多的资源消耗。不包含联接操作的基本查询就属于简单查询,查询优化器生成这种查询的计划是零开销的(因为什么也没有消耗),这种计划也称为普通计划(trivial plan)。
查询优化器的下一阶段开始了真正的优化,包含三个搜索阶段∶
● 阶段 0∶查询优化器在这一阶段查看查询命令中的嵌套循环联接,并不检查并行操作(并行操作指的是在多个处理器上同时进行的操作)。如果在这一阶段中得到的计划的开销小于 0.2,查询优化器则停止优化。在这一阶段得到的计划称为事务处理(Transaction Processing,TP)计划。
● 阶段 1∶ 查询优化器在这一阶段使用部分可用的优化规则,从一些已经有了优化执行计划的普通模式中进行匹配。如果在这一阶段中找到的计划的开销小于 1.0,查询优化器则停止优化。这一阶段得到的计划称为快速计划(quick plan)。
● 阶段 2∶这一阶段查询优化器可以使用所有的优化规则,使尽全力进行最后的优化。在这一阶段查询优化器还会检查查询命令的并行性和索引视图。这一阶段的优化结果是计划的开销和查询优化消耗时间的折中。这一阶段生成的计划的优化级别为”完全优化”。
查询执行器的功能就是执行查询。查询执行器执行查询计划包含的每一个步骤,根据计划中的步骤和存储引擎进行交互,检索或修改数据。
由于 SELECT 查询需要检索数据,因此该请求通过 OLE DB接口传递到存储引繁,之后传递到存储引擎的访问方法(access method)。
存储引擎负责管理与数据相关的所有I/O 操作,包括访问方法(access method)和缓冲区管理器(buffer manager)。其中,访问方法负责处理行、索引、页、分配和行版本的 I/O 请求;缓冲区管理器负责缓冲池的管理,缓冲池是 SQL Server 内存的主要使用者。存储引擎还包含了一个事务管理器,负责数据的锁定以实现 ACID 属性中的隔离性,并负责管理事务日志。
访问方法是一个代码集合,这些代码定义了数据和索引的存储结构,并提供了检索数据和修改数据的接口。访问方法包含了所有检索数据的代码,但是它自己并不执行实际操作,而是将访问数据的具体请求提交给缓冲区管理器。
缓冲区管理器负责缓冲池的管理。SQL Server 占用的大多数内存都在缓冲池中。当要求从一页中读取数据行时,缓冲区管理器会在缓冲池的数据缓存中检查这一页是否已经被缓存到内存中。如果这个页面已经被缓存,缓冲区管理器就直接将这一页作为结果返回给访问方法。如果这个页面尚未缓存,缓冲区管理器会先从磁盘上的数据库中获取这一页,并将其放入数据缓存中,然后再把结果返回给访问方法。
数据操作永远是在内存中进行的。每一次新的数据读取请求发生的时候,缓冲区管理器都会首先将数据从磁盘复制到内存(即数据缓存)中,然后返回结果集。
数据缓存(data cache)通常是缓冲池中最大的一块,因此,数据缓存也是 SOLServer 中消耗内存最大的一部分。从磁盘中读取的所有数据页都要首先缓存在这里,然后才能被使用。
当前数据缓存中的每个数据页都对应 sys.dm_os_buffer descriptors 动态管理视图(Dynamic Management View,DMV)中的一行。可以通过下面的代码查看每一个数据库在数据缓存中占用的空间大小∶
SELECT COUNT(*) * 8 / 1024 AS 'Cached Size (MB)',
CASE database_id
WHEN 32767
THEN 'ResourceDb'
ELSE DB_NAME(database_id)
END AS 'Database'
FROM sys.dm_os_buffer_descriptors
GROUP BY DB_NAME(database_id),
database_id
ORDER BY 'Cached Size (MB)' DESC;
页面在缓存中保留的时间长短由最近最少使用策略(Least Recently Used,LRU)决定。数据缓存中每一个页面的头信息中存储了最近两次被访问的详细信息,缓冲区管理器定期扫描所有的页面,检测每一个页面的这两个值。缓冲区管理器为每一个页面维护一个计数器,如果这一页面有一定的时间没有被访问,这个计数器的值就减1。每当 SQL Server需要释放一些缓存的时候,首先将计数器值最小的页面刷新。
将数据页”老化(age out)”并维护一定数量的空闲缓冲页以供后续使用的这一过程可以由任意工作线程在完成自己的 I/O 调度之后完成,也可以由惰性写入器进程完成。在内存紧张的时候,缓冲区管理器将页面移出缓存的频率也大大增加。
事务管理器有两个重要的组件∶锁管理器(lock manager)和日志管理器(log manager)。锁管理器负责提供并发数据访问,通过锁实现设置的隔离级别(ACID 属性中的隔离性)。
访问方法代码请求所有的数据更改都必须记入日志,日志管理器将数据更改写入事务日志。这种方式称为预写日志(write-ahead logging)。
存储在事务日志中的内容并不是修改查询语句的清单,而是修改操作发生之后数据页面的具体变化。SQL Server 只需要这些信息就可以撤消任何修改操作。