图 5. 准备/执行模型
调用存储过程
存储过程一般是从 ODBC 和 OLE-DB,通过发送 SQL 语句给使用 ODBC 标准 CALL 语法调用过程的 SQL Server 来调用。其应类似于以下语句:
SQLExecDirect(hstm, "{call addorder(?)}", SQL_NTS)
对于默认结果集,这是一个简单的流,因为这正是 RPC 消息原本要处理的对象。客户机向服务器发送 RPC 消息,并获取来自存储过程的处理结果。如果是游标,则情况稍微复杂一些,客户机需要调用 Sp_cursoropen,就像其他游标一样。 Sp_cursoropen 含有内部逻辑,检测该存储过程是否只包含一条 SELECT 语句。如果是,则对该 SELECT 语句打开一个游标。如果该存储过程中不是一条 SELECT 语句,则客户机会得到一个指示,说明“我们为您打开结果集,但是我们将以流水的方式返回数据流,您可以把这个数据流提供给用户”。
存储过程执行流程如图 6 所示。
图 6. 调用存储过程
SQL Manager
前面已经提到过的 SQL Manager 驱动很多服务器处理过程,它实际上是服务器的心脏。SQL Manager 处理所有调用存储过程的请求,管理过程缓存,拥有虚拟系统存储过程,在稍后要介绍的特定查询的自动参数化过程中也要涉及。如果您有与本文类似的描述 SQL 6.5 或更老版本的文章,则不会读到有关 SQL 管理器的讨论,然而,您会读到一些完成 SQL 管理器工作的一些不同的组件。但是在 SQL Server 7.0 中,这些组件被统一为 SQL 管理器,通过系统驱动查询语句的处理。
在一般情况下,当要求 SQL 管理器为您做某些工作时,通过 RPC 消息调用 SQL 管理器。但是,当通过 SQL 消息发送 SQL 语句并进入引擎编译时,也会用到 SQL 管理器。当存储过程或批处理程序包含 EXEC 语句时,也会调用 SQL 管理器,因为 EXEC 实际上就是调用 SQL 管理器。如果该 SQL 语句传送了下面就要讨论的一个自动参数化模板,则需要调用 SQL 管理器对该查询进行参数化处理。当特定查询语句需要装入缓存时,也要调用 SQL 管理器。
编译与执行
现在讨论在 SQL Server 中编译和执行的一般流程。需要注意的是编译和执行在 SQL Server 内部是两个不同的阶段。 SQL Server 编译查询语句和执行该语句之间的间隔时间可能非常短,只有几个毫秒,也可能是几秒钟、几分钟、几小时甚至几天。在编译过程中(这个过程包括优化),我们必须区分什么样的知识可以用于编译。并不是所有对编译有用的知识对执行也起作用。您必须把编译和执行理解为两个不同的活动,即使您发送并立即执行的是特定 SQL 查询语句。
当 SQL Server 可以开始处理查询语句时,SQL Manager 要在缓存内进行查找,如果没有找到该语句,则必须编译该语句。编译处理要完成以下几件工作。首先,要进行分析和正常化。分析就是剖析该 SQL 语句,将其转换成更适合计算机处理的数据结构。分析还要验证语法的正确性。分析不进行表名和列名合法性等检查,这些工作在正常化阶段完成。正常化主要是解析 SQL 语句中引用的对象,转换成实际的数据库对象,检查请求的语义是否有意义。例如,试图执行一个表,这在语义上就是错误的。
下一步是编译 Transact-SQL 代码。Transact-SQL 和 SQL 本身都让人有点儿困惑,Microsoft 的开发人员也像别人一样经常互换两个词。但是,这两者之间还是有重要差别的。SQL 包括所有 DML 语句:INSERT、UPDATE、DELETE 和 SELECT。SQL Server 还有一种包括这些 DML 语句的语言,称为 Transact-SQL,也就是 TSQL。TSQL 提供过程结构:IF 语句、WHILE 语句、局部变量声明等。服务器内部对 SQL 和 TSQL 的处理方法完全不同。TSQL 的过程逻辑要由知道如何进行过程化处理的引擎来编译。
SQL 语句本身由典型的查询优化器来处理。优化器必须把基于集合的 SQL 语句的非过程化的请求,翻译成可以被高效执行并返回所需结果的过程。除非特别说明,我们在以下讨论编译时,均指 TSQL 的编译和 SQL 语句的优化。
上面已经提到,编译和执行是两个不同的查询处理阶段,因此,优化器完成的工作之一是基于相当稳定的状态进行优化。您可以注意到,SQL Server 可能会根据语句所满足的条件重新编译,所以状态并不是永远稳定的,但也不是处于不停的变化之中。如果优化器使用的信息变化太剧烈、太经常 ? 并发处理器的数量和锁的数量不稳定 ? 则必须不断重新进行编译,而一般来说编译是比较耗时的。例如,SQL 语句的运行时间为百分之一秒,而编译可能需要占用半秒。最理想的情况是,SQL Server 能够只编译语句一次,而执行成千上万次,不必每次执行该语句时都重新编译它。
编译阶段的最终产品是查询计划,放在过程缓存中。便宜的特定 SQL 计划并不放在缓存中,不过这只是个小问题。我们不希望缓存被不太可能重复执行的内容占满,一般来说,特定 SQL 语句的计划是最不可能反复使用的了。如果语句编译已经很便宜(小于百分之一秒),则没有必要再把计划放入缓存,用不太可能重新使用的计划占用缓存。
把计划放入缓存之后,SQL Manager 按照执行要求逻辑进行检查,确定是否有更改的内容,是否需要重新编译。即使编译到执行之间时间间隔只有几毫秒,也可能有人会执行一条数据定义语句 (DDL),为关键的表加了索引。这种可能性不大,但是确实存在,因此 SQL Server 必须考虑这一点。有几种情况 SQL Server 必须重新编译存储规划。元数据的修改,例如增加或删除索引,是重新编译的最主要的原因。服务器必须确信所使用的计划反映了索引的当前状态。
重新编译的另一种原因是统计情况发生变化。SQL Server 还维护不少数据使用频率的统计信息。如果数据使用频率分布情况变化很大,则可能需要另一个查询计划以便更有效地执行。SQL Server 跟踪表数据插入和删除的统计数据,如果数据修改的数量超过根据表的容量变化的某一阈值,则需要根据新的分布数据重新编译计划。
图 7 给出了编译和执行过程的流程。
图 7. 编译与执行
注意,实际参数的改变并不会导致重新编译,环境的改变,例如可用内存的增加或所需数据的增加,也不会导致重新编译。
执行是比较简单的,如果需要执行的查询很简单,如“插入一行”,或从带有唯一索引的表中查询数据,则执行处理会非常简单。但是,很多查询都要求大量的内存以提高运行效率,或至少从所增加的内存得到好处。在 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 文本,该计划就会留在缓存中。
如果查询不是特定的,也就是说是一个存储过程、带参数的查询或自动参数化的查询,则每次该计划被重新使用时,成本都会设置回原来的值。只要计划被重新使用,就会留在缓存中。即使有一段时间没有被使用,取决于最初的编译代价的高低,计划停留在缓存中的时间也有长短。
图 9 显示从缓存中检索计划并调整成本的流程。
图 9. 从缓存中检索计划
迟缓写入器(Lazywriter) 是使计划过时的机制,负责在必要的时候从缓存中删除计划。迟缓写入器实际上是存储引擎的一部分,但是因为迟缓写入器对于查询处理机制是如此重要,我们还是在这里进行讨论。迟缓写入器管理查询计划内存使用的机制与管理页面的机制一样,因为 SQL Server 7.0 计划存储在普通缓冲存储器中。迟缓写入器要检查系统中所有的缓冲器标题。如果系统的内存不紧张,检查的次数就很少;如果开始紧张,则迟缓写入器就会经常运行。当迟缓写入器运行时,它要检查缓冲区标题,并检查缓存区中该页面的当前成本。如果成本为 0,则意味着自从上次迟缓写入器检查以来,该页面没有被使用过,于是迟缓写入器就会释放该页面,以便为系统增加可用内存,用于页面 I/O 或其他计划。此外,如果该缓冲区包含过程计划,则迟缓写入器会调用 SQL 管理器,以完成一些清理工作。最后,该缓冲区会被放到可用内存表中供重新使用。
如果与缓冲区关联的成本大于 0,则迟缓写入器会把成本减 1,并继续检查其他缓冲区。这成本实际上反映的,某计划若是没被使用,它在缓存中还能存在多少个迟缓写入器的检查周期。这种算法,除了如果对象是存储过程则调用 SQL Manager 这一步之外,对缓存中的计划和缓存的数据或索引没有什么区别。迟缓写入器并不知道对象是否存储过程,这种算法很好地平衡了磁盘 I/O 对缓存的使用和存储过程计划对缓存的使用。
您会发现,如果计划的编译成本很高,那么即使很长一段时间都没有被重新使用,也仍然会停留在缓存中,这是因为其初始成本太高了。经常被重新使用的计划也会长期停留在缓存中,这是因为每当它被重新使用时其成本已被重新设置,迟缓写入器不会看到它的成本降为 0。
图 10 显示迟缓写入器处理缓存的流程。
图 10. 迟缓写入器处理缓存的流程
处理客户机的 SQL
下面再看看提交 SQL 语句之后的处理过程。首先,我们将研究客户机向 SQL Server 发送 RPC 事件。因为 SQL Server 收到了 RPC 事件,所以它会知道该事件是某种参数化的 SQL;它是准备/执行模型,或者是 EXECUTESQL。SQL Server 需要构建一个缓存键,以标识这个具体的 SQL Server 文本。如果 SQL Server 处理的是实际的存储过程,则不需要建立它自己的键;直接使用该过程的名称即可。对于通过 RPC 调用发来的简单 SQL 文本,则通过杂凑该 SQL 文本来建立缓存键。此外,该键还要反映一定的状态信息,如某些 ANSI 设置。使所有 ANSI 设置为 ON 的连接和另一个使所有 ANSI 设置为 OFF 的连接,即使它们来自相同的查询,也不能使用相同的计划。处理过程是不同的。例如,如果一个连接把 concat_null_yields_null 设置为 ON,另一个把 concat_null_yields_null 设置为 OFF 的连接,即使它们执行的是完全相同的 SQL 文本,但所产生的结果则完全不同。这样,SQL Server 可能需要在缓存中保存计划的多个版本,每个版本对应于一个不同的 ANSI 设置组合。启用的选项设置是键的一部分,而键字是使用这种缓存处理机制检查对象的核心,因此 SQL Server 建立这种键并用来检查缓存。如果在缓存中没有发现该计划,则 SQL Server 会按照前面介绍的方式编译该计划,并把该计划与键一起存入缓存中。
SQL Server 还需要确定该命令是否是准备操作,这意味着该计划应该只编译但不执行。如果是准备操作,则 SQL Server 会给客户机返回一个句柄,供客户机在以后检索并执行该计划。如果不是一个准备操作,则 SQL Server 提取并执行该计划,就像最初从缓存中找到该计划一样。
准备/执行模型为缓存管理增加了复杂因素。预备给出了今后能够执行该计划的句柄。应用程序可以在几小时或几天之内保持该句柄是激活的,以定期执行计划。即使需要在缓存中为更多的活动计划或数据页面腾出空间,也不能使该句柄无效。SQL Server 实际所做的就是将计划放入缓存,此外还从预备操作中将 SQL 保存到更加紧凑的空间。如果空间紧张,则可按前述的方式释放计划所占用的空间,但仍有 SQL 的副本准备着。如果客户机要执行预备的 SQL,但在缓存中没有找到计划,则 SQL Server 能够检索到该文本并编译它,再将它放回缓存中。这样,缓存中的 16 千字节 (KB) 或更多的页面用来保存可重用的计划,而长期占用的空间或许是存储在其他处的 SQL 代码的 100 或 200 字节。
处理来自客户机的语句时的另一种情况是,查询是作为 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 便将找到的成本最低的计划与内部阈值进行比较。阈值用于决定
[1] [2] 下一页