编译与执行 现在讨论在 SQL Server 中编译和执行的一般流程。需要注意的是编译和执行在 SQL Server 内部是两个不同的阶段。 SQL Server 编译查询语句和执行该语句之间的间隔时间可能非常短,只有几个毫秒,也可能是几秒钟、几分钟、几小时甚至几天。在编译过程中(这个过程包括优化),我们必须区分什么样的知识可以用于编译。并不是所有对编译有用的知识对执行也起作用。您必须把编译和执行理解为两个不同的活动,即使您发送并立即执行的是特定 SQL 查询语句。
当 SQL Server 可以开始处理查询语句时,SQL Manager 要在缓存内进行查找,如果没有找到该语句,则必须编译该语句。编译处理要完成以下几件工作。首先,要进行分析和正常化。分析就是剖析该 SQL 语句,将其转换成更适合计算机处理的数据结构。分析还要验证语法的正确性。分析不进行表名和列名合法性等检查,这些工作在正常化阶段完成。正常化主要是解析 SQL 语句中引用的对象,转换成实际的数据库对象,检查请求的语义是否有意义。例如,试图执行一个表,这在语义上就是错误的。
上面已经提到,编译和执行是两个不同的查询处理阶段,因此,优化器完成的工作之一是基于相当稳定的状态进行优化。您可以注意到,SQL Server 可能会根据语句所满足的条件重新编译,所以状态并不是永远稳定的,但也不是处于不停的变化之中。如果优化器使用的信息变化太剧烈、太经常 — 并发处理器的数量和锁的数量不稳定 — 则必须不断重新进行编译,而一般来说编译是比较耗时的。例如,SQL 语句的运行时间为百分之一秒,而编译可能需要占用半秒。最理想的情况是,SQL Server 能够只编译语句一次,而执行成千上万次,不必每次执行该语句时都重新编译它。
把计划放入缓存之后,SQL Manager 按照执行要求逻辑进行检查,确定是否有更改的内容,是否需要重新编译。即使编译到执行之间时间间隔只有几毫秒,也可能有人会执行一条数据定义语句 (DDL),为关键的表加了索引。这种可能性不大,但是确实存在,因此 SQL Server 必须考虑这一点。有几种情况 SQL Server 必须重新编译存储规划。元数据的修改,例如增加或删除索引,是重新编译的最主要的原因。服务器必须确信所使用的计划反映了索引的当前状态。
重新编译的另一种原因是统计情况发生变化。SQL Server 还维护不少数据使用频率的统计信息。如果数据使用频率分布情况变化很大,则可能需要另一个查询计划以便更有效地执行。SQL Server 跟踪表数据插入和删除的统计数据,如果数据修改的数量超过根据表的容量变化的某一阈值,则需要根据新的分布数据重新编译计划。
执行是比较简单的,如果需要执行的查询很简单,如“插入一行”,或从带有唯一索引的表中查询数据,则执行处理会非常简单。但是,很多查询都要求大量的内存以提高运行效率,或至少从所增加的内存得到好处。在 SQL Server 6.5 中,每个查询能够使用的内存限制在 0.5MB 或 1MB 以下。有一个控制查询内存使用的参数,称为排序页。顾名思义,它主要是限制可能占用大量内存的排序操作。不管要处理的排序有多大,在 SQL Server 6.5 中,内存的使用不能超过 1MB。即使您使用的机器上配置了 2GB 内存,需要对数百万行数据排序,也不能突破限制。显然,复杂的查询不能高效执行,因此 SQL Server 开发人员增加了 SQL Server 7.0 的能力,使得单个查询可以使用大量的内存。
另一个问题随之而来。一旦您开始允许查询使用大量内存,就必须确定如何把内存分配给可能需要内存的很多查询。 SQL Server 按照以下方法解决这个问题。当查询计划优化之后,优化器要确定有关给该查询使用的内存的两部分信息。第一,该查询有效执行所需要的最小内存,该参数与查询计划一起存放。优化器还要确定该查询可以获益的最大的内存量。例如,如果要排序的整个表只有 100MB,分配 2GB 内存就没什么帮助了。您需要的只是 100MB,这个最大有用内存参数随查询计划一起存放。
当 SQL Server 开始执行计划时,该计划被传递给一个所谓内存授权调度程序的例程中。这个授权调度程序要完成几项有趣的工作。首先,如果授权调度程序要处理的查询在计划中没有排序或杂凑操作,则 SQL Server 知道该查询不会需要很多内存。在这种情况下,不需要内存授权调度程序进行判断。该计划会立即执行,因此典型的事务处理请求会完全旁路这种判断机制。内存授权调度程序还设有多个队列处理不同容量的请求。内存调度程序优先处理较小的请求。例如,如果有一个查询要求“提取前 10 个”,并且只需要对 20 行排序,则虽然需要经过内存授权调度程序,但是要释放该查询并且很快调度。服务器需要并行或并发执行许多这种查询。
如果有很大的查询,您希望一次只运行几个查询,让它们占有所需的更多内存。SQL Server 确定一个由 4 X(系统中的 CPU 个数)得到的数。如果可能,SQL Server 会同时运行那个数量的查询,为它们分配高效运行所需要的最小内存。如果还剩有内存,则一部分查询会允许占用最大高效内存。SQL Server 试图既为查询分配尽可能多的内存,又让尽可能多的查询同时运行在系统中。
一旦调度程序说现在可以为请求分配内存,则计划即被“打开”,开始实际运行。计划会一直运行直到完成。如果查询使用了默认结果集模型,则计划会一直运行到检索到所有结果为止,然后把结果返回给客户机。如果使用的是游标模型,则处理过程略有不同。每个客户机请求只提取一块数据,并不是所有数据。当每个结果块返回给客户机之后,SQL Server 必须等待客户机的下一个请求。在等待时,整个计划就会睡眠。这意味着要释放一些锁,要释放一些资源,并保留一些断点信息。这些断点信息使得 SQL Server 能够返回到睡眠之前的状态,使得执行可以继续。
过程缓存 我们在前面已经多次提到 SQL Server 的过程缓存。需要注意的是,SQL Server 7.0 的过程缓存与以前的版本有很大不同。在早期的版本中,有两个有效配置值用于控制过程缓存的容量:一个是定义 SQL Server 总可用内存的固定容量,另一个是供存储查询计划使用的内存百分比(扣除满足固定需要的内存)。在老版本中,特定 SQL 语句从不存入缓存,只有存储过程计划才存入其中。在 SQL Server 7.0 中,内存的总容量是动态的,用于查询计划的空间也是经常变化的。
在处理查询时,SQL Server 7.0 首先会问的是:这个查询既是特定的又是易于编译的吗?如果是,SQL Server 就根本不会将其写入缓存中。将来重新编译这些计划比把复杂的计划或数据页推出内存更合算。如果查询不是特定的或不易于编译,则 SQL Server 会从缓存区中分配一些缓存内存存储该计划,因为该缓存区是 SQL Server 7.0 用来满足 99% 内存需求的唯一来源。在少数特殊情况下,SQL Server 会直接从操作系统中分配大块内存,但是这种情况极为罕见。SQL Server 的管理是集中式的。
写入缓存的除计划外,还有反映通过编译该查询实际创建该计划的成本的成本因子。如果这是一个特定计划,则 SQL Server 将它的成本设置为 0,表示可以立即将它撤出过程缓存。对于特定 SQL,虽然有可能被重复使用,但可能性很小,如果系统内存紧张,总是愿意首先撤出特定语句的计划。这样,特定查询的计划是最适合清出缓存的对象。如果查询不是特定的,则 SQL Server 会把该成本设置为实际编译查询的成本。这些成本是以磁盘 I/O 为单位的。如果从磁盘中读出一个数据页,则有一个 I/O 成本。在编译计划时,信息从磁盘中读出,包括统计数据和查询本身的文本。SQL 要进行附加的处理,而且这处理工作被正常化为 I/O 成本。现在,建立过程的成本可用执行 I/O 的成本表示。该成本非常恰当反映了,与打算用磁盘缓存的数据量相比,管理实际打算分配给存储过程和任何种类查询计划的缓存量的能力。该成本被计算出来之后,该计划就会被写入缓存。
图 8 显示计算计划成本并将其写入缓存的流程。
图 8. 将计划写入缓存
如果另一个查询可以重新使用该计划,则 SQL Server 要再次判定计划的类型。如果是一个特定计划,SQL Server 会把成本加 1。这样,如果特定计划确实要被重新使用,则它会在缓存中稍作停留,停留时间越长,成本就增加越多。如果该计划经常被重新使用,则成本会一次增加一个单位地不断增长,直到增长到其实际编译成本。该成本和设置的成本一样高。不过该计划经常被重复使用;如果同一用户或其他用户不断重新提交完全一样的 SQL 文本,该计划就会留在缓存中。
处理来自客户机的语句时的另一种情况是,查询是作为 SQL 语言事件出现的。除了一点以外,此流程并无太大的差异。在这种情况下,SQL Server 试图使用称为自动参数化的技术。SQL 文本与自动参数化模板相匹配。自动参数化是个棘手的问题,因此,过去一直能够利用共享的 SQL 的其他数据库管理产品, 一般并没有提供这一选项。随之而来的问题是,如果 SQL Server 自动地参数化每个查询,那么对于随后提交的某些特定值而言,这些查询中的某些(或绝大多数)将获得非常糟糕的计划。在程序员将参数标记放在代码之中的场合下,其假定是程序员知道所期望的值的范围,并愿意接受 SQL Server 提供的计划。但当程序员实际补充一个特定的值,并且 SQL Server 决定将该值当做一个可变的参数来对待时,所产生的任何适合于某个值的计划可能不适合于后续的值。利用存储过程,通过在过程中放入 WITH RECOMPILE 选项,程序员可以强制产生新的计划。利用自动参数化,程序员无法指出必须为每一个新值开发新的计划。
当 SQL Server 处理自动参数化时,它是非常保守的。被安全地自动参数化的查询有一个模板,并且只有匹配模板的查询才能应用自动参数化。例如,假设有这样一个查询,其中包含带有等于操作符、但没有连接的 WHERE 子句,WHERE 子句中的列带有唯一的索引。SQL Server 知道绝对不会返回一行以上,而且计划将总是使用那个唯一的索引。SQL Server 绝对不会考虑扫描,实际值绝对不会以任何方式改变计划。对于自动参数化而言,这种查询是安全的。
如果查询匹配自动参数化模板,则 SQL Server 自动用参数标记(例如 @p1、@p2)代替文字,并且这就是我们发送到服务器的内容,正如它是 sp_executesql 调用一样。如果 SQL Server 认为该查询对自动参数化并不安全,则客户机将向 SQL Server 发送文字的 SQL 文本,以此作为特定的 SQL。
图 11 显示客户机向 SQL Server 发送请求时的处理流程。
图 11. 处理客户机的 SQL
编译 现在让我们更详细地讨论一下编译和优化。在编译过程中,SQL Server 分析语句,并创建所谓的次序树,即语句的内部表述。这是 SQL Server 6.5 实际保留在 SQL Server 7.0 中的几个数据结构之一。该次序树是正常化的。正常化程序的主要功能是执行绑定。绑定包括检验表和列的存在,以及装载有关表和列的元数据。有关必需的(隐含的)转换信息也附加在次序树上,例如,如果查询试图向数字值添加整数 10,则 SQL Server 将向该树插入隐含的转换。正常化还用视图的定义代替对该视图的引用。最后,正常化执行一些基于语法的优化。如果该语句是传统的 SQL 语句,则 SQL Server 从关于该查询的次序树中提取信息,并创建称为查询图表的特殊结构,设置查询图表是为了使优化器工作非常有效。然后优化该查询图表,一个计划就产生了。
图 12 显示编译过程流程。
图 12. 编译
优化 SQL Server 优化器其实是由独立的段组成的。第一段是一个非基于成本的优化器,称为琐细计划优化。琐细计划优化的完整概念是,当 SQL 语句确实只有一个可变计划时,基于成本的优化太昂贵了。最好的例子是,带 VALUES 子句的 INSERT 语句组成的查询。它只可能有一个计划。另一个例子是,所有的列都在唯一的封面索引(且没有其他列的索引)中的 SELECT 语句。这两例中,SQL Server 只要简单地生成一个计划,用不着在多个计划选一个更好的方案。琐细计划优化器可找到真正显而易见的计划,而且通常非常便宜。所以,最简单的查询在处理的前期就趋于被清除,优化器不花很多时间来搜索一个好计划。这是好事,因为随着 SQL Server 将杂凑连接、合并连接和索引相交增加到其处理技术列表上, SQL Server 7.0 版上的潜在计划数呈天文数字增长。
如果琐细计划优化器不能找到一个计划,SQL Server 便进入优化的下一部分,称为简化。简化是查询本身的语法变换,寻找可交换的特性和可重新排列的运算。SQL Server 可进行常数合并,以及无需考虑成本或分析索引是什么但能得出更有效查询的其他运算。SQL Server 然后上载关于索引和列的统计信息,并输入优化的最后的主要部分,即基于成本的优化器。
基于成本的优化有三个阶段。第一个基于成本的阶段,称为交易处理阶段,查找简单请求的计划,即典型的交易处理系统。这些请求一般比由琐细计划优化器处理的那些请求要复杂些,并要求比较众多计划查找出成本最低的计划。当交易处理阶段完成时,SQL Server 便将找到的成本最低的计划与内部阈值进行比较。阈值用于决定是否要求进一步的优化。如果计划成本比阈值低,那么,进行附加优化比只执行已找到的计划成本要高。所以,SQL Server 不做进一步优化,并使用交易处理阶段找到的计划。
如果交易处理阶段找到的计划,仍比该阶段的阈值贵,SQL Server 便进入第二个阶段。这个阶段有时称为 QuickPlan 阶段。QuickPlan 阶段扩大搜索范围来寻找一个好计划,包括选择好的、适度复杂的查询。QuickPlan 检查可能的计划范围,完成之后,将最佳计划的成本与第二个阈值进行比较。因为在交易处理阶段,如果发现了一个成本比阈值低的计划,优化便终止,并使用那个计划。一般来说,SQL Server 6.5 版中已有的查询的计划,在 SQL Server 7.0 版中也应当是最佳的,这个计划将要么被琐细计划优化器找到,要么被基于成本的优化的头两个阶段中的一个发现。这些规则被有意地组织起来以达到这个目的。这个计划将很可能由使用单一的索引和使用嵌套循环联合组成。
摘要 如前所述,SQL Server 的内部机制与结构是一个非常大的主题,远远超过了我们能在本文中提供的内容。我们重在直接介绍 SQL Server 与客户机的交互方式,以及 SQL Server 关系引擎如何处理来自客户机的请求。我们希望,在了解 SQL Server 如何处理查询,以及如何和何时编译或重新编译它们之后,您就能利用 SQL Server 7.0 的功能和技巧编写出更好的应用程序。