从客户机的角度看服务器
前面已经提到过,客户机与 SQL Server 通信的主要方法就是通过使用 TDS 消息。TDS 是一种简单协议。当 SQL Server 接收到一条消息时,可以认为是发生了一个事件。首先,客户机在一个连接上发送登录消息(或事件),并得到返回的成功或失败的响应。当您希望发送 SQL 语句时,客户机可以把 SQL 语言消息打包发送给 SQL Server。另外,当您希望调用存储过程、系统过程或虚拟系统存储过程(我们后面还要详细讨论)时,客户机可以发送 RPC 消息,这种消息相当于 SQL Server 上的一个 RPC 事件。对于上面的后两种情况,服务器会以数据令牌流的形式送回结果。Microsoft 没有把实际的 TDS 消息写入文档中,因为这被认为是 SQL Server 组件之间的私用契约。
目录存储过程是另一类关键的客户机/服务器的交互部分。这些存储过程首先在 ODBC 的 SQL Server 6.0 中出现, 包括诸如 sp_tables 和 sp_columns 等存储过程。ODBC 和 OLE-DB API 定义了描述有关数据库对象的元数据的标准方法,这些标准需要适用于所有类型的 RDBMS 服务器,而不必调整为 SQL Server 自己的系统表。不是客户机向服务器发送对系统表的多个查询,并在客户机端建立标准的元数据视图,而是创建一组存储在服务器上的系统存储过程,并对 API 返回适当格式的信息。这种方法使得通过一次通信就可以完成很多重要的元数据请求。
为 ODBC 编写的过程已经写入文档,通常适合需要从系统表中获取信息但其他机制没有提供这种方法的情况。这使得 Transact-SQL 过程和 DB-Library 应用程序可以访问元数据,而不需要编写对 SQL Server 系统表的复杂查询,并且使应用程序不受今后 Microsoft 修改系统表的影响。
OLE DB 定义了一组架构行集,它们类似于 ODBC 的元数据,但又和它不同。它创建了一组新的目录存储过程,以更有效地为这些架构行集植入数据。但是,这组新的存储过程没有写入文档,因为这些存储过程重复了早先提供的功能。通过现有的若干种方法都可以得到元数据,因此 SQL Server 开发组决定不显露这些并没有为编程模型增加新内容的对象。
客户机与服务器的交互还有第三个方面。它最初出现在 SQL Server 6.0 中,但是没有得到普遍使用。这就是虚拟系统存储过程的概念;在 SQL Server 7.0 中起很重要的作用。当第一次为 SQL Server 6.0 开发服务器端游标时,开发人员就需要选择采取什么方法管理客户机/服务器的交互。游标并不特别适合现有的 TDS 消息,因为这些消息允许逐行返回数据,不需要客户机指定额外的 SQL 语句。开发人员本来可以向 TDS 协议添加更多的消息,但是需要修改太多的其他组件。SQL Server 6.0 中的 TDS 版本还需要向 Sybase 版本靠拢,以便确保两者的可互操作性,于是开发人员选择了另外的处理机制。他们开发了外表看起来像是系统存储过程的新功能(服务器端游标),实际上是指向 SQL Server 代码的入口存储过程。它们被客户机应用程序使用标准的 RPC TDS 消息来调用。它们被称为虚拟系统存储过程,因为在客户机上,它们像其他存储过程那样被调用,和其他存储过程不同的是,它们并不是由简单的 SQL 语句组成。大多数虚拟系统存储过程都是私用的,并且没有写入文档。对于游标过程,所有 API 都显露其自有的一组游标 API 模型和它们自己的游标操作函数,因此没有必要为存储过程本身编写文档。即使是在 Transact-SQL 语言中,也有显露游标的语法,可以使用 DECLARE、OPEN、FETCH 等,所以完全没有必要为虚拟系统存储过程编写文档,例如 sp_cursor,因为这些过程只在内部使用。
ODBC 和 OLE DB 中出现了带参数的查询和准备/执行模型的概念。在 SQL Server 7.0 以前的版本中,这些概念是由客户机 API 中的代码实现的。在 SQL Server 7.0 中,Microsoft 为这些概念添加了对“关系服务器”的支持,并且通过新的虚拟系统存储过程显露了这种支持。本文后面还要介绍这些功能,以及服务器如何支持这些功能。通过 sp_executesql 过程对带参数的查询的支持,被认为对直接 Transact-SQL 和 DB-Library 的使用特别有用,所以将其写入了文档。准备/执行的过程,被 ODBC 驱动程序和 OLE DB 提供程序专用。
这样,可以与 SQL Server 通信的所有客户机程序,都建立在这三组功能之上:TDS 协议、目录存储过程和虚拟系统存储过程。
服务器结构
SQL Server,或更确切一点地说,是“SQL Server 关系服务器”,经常被说成是由两个主要部分组成,即关系引擎和存储引擎。正如前面提到过的那样,已经有很多文献介绍存储引擎的细节了,所以本文主要介绍关系引擎的功能。图 3 给出了 SQL Server 关系引擎部分的主要组件。所给出的组件可以分为三组子系统。左边的组件编译查询,包括查询优化器。查询优化器是所有关系数据库引擎中的最神秘的部分之一,从性能的角度看也是最重要的部分。查询优化器负责提取 SQL 语句中的非过程请求部分,并将其翻译成一组磁盘 I/O、过滤以及其他能够高效地满足该请求的过程逻辑。图中右侧是执行基础结构。这里实际上只有很少的功能。当编译组件的工作完成之后,所产生的结果只需用很少几个服务即可直接执行。
图 3. 服务器结构
图的中间是称为 SQL Manager 的部分。SQL Manager 控制着 SQL Server 内部的所有数据的流动。SQL Manager 控制着 RPC 消息,在 SQL Server 7.0 中,绝大多数来自客户机的功能调用都是通过 RPC 消息进行的。上一节中介绍的虚拟系统存储过程逻辑上也是 SQL Manager 的一部分。通常,作为 TDS SQL 语言消息的 SQL 语句直接在编译一端执行,与早期版本相比,SQL Server 7.0 较少使用这种方法,但还算是比较常见的。执行结果由称为 ODS 的执行引擎中的组件格式化为 TDS 执行结果消息。
绝大多数输出都来自图中的执行端,而且输出结果也真正出自表达式服务。“表达式服务”库是进行数据转换、谓词评估(过滤)以及算法计算的组件。它还利用了 ODS 层,把输出结果格式化为 TDS 消息。
还有几个组件,我们只是在这里简单地提一下,这些组件在关系引擎内部提供附加服务。这些组件中的一个是目录服务组件,用于数据定义语句,例如 CREATE TABLE、CREATE VIEW 等。目录服务组件主要放在关系引擎中,但是实际上大约有三分之一的目录服务组件是在存储引擎中运行的,所以可以看作是共享组件。
关系引擎中的另一种组件是“用户模式调度程序 (UMS)”,这是 SQL Server 自己内部的纤程和线程规划器。把任务分配给纤程或线程是一种非常复杂的内部机制,取决于对服务器如何配置,以及在 SMP 系统中允许 SQL Server 进行处理器之间的适当的负载平衡。UMS 还可以避免 SQL Server 由于同时运行太多的线程而导致性能过低。最后,还有大家熟悉的系统过程,逻辑上它们也属于关系引擎的一部分。这些组件肯定不是服务器代码,因为可以很容易地使用 sp_helptext 检查定义这些过程的 Transact-SQL 代码。但是,系统过程被作为服务器的一部分来对待,因为系统过程的用途是显露重要的服务器能力,像系统表一样,以供应用程序在更高的层次上和更适当的层次上使用。如果应用程序开发人员将较高层次的系统过程 ― 更容易使用 ― 作为一种接口,即使随着版本的更新,原始层次上的系统表发生变化时,应用程序仍然可以继续使用。
处理 SQL 语句时的客户机/服务器交互
下面我们将讨论当客户机应用程序与 SQL Server 交互时客户机的动作。以下是一个 ODBC 调用的例子:
SQLExecDirect(hstmt, "SELECT * FROM parts where partid =
7", SQL_NTS)
(OLE-DB 也有一个与这个调用几乎直接等价的调用,此处不再讨论这个调用,因为这个调用实际上与 ODBC 调用相同。)该 ODBC 调用取一个 SQL 语句,然后将其发送给 SQL Server 来执行。
在这个具体的查询语句中,我们从零件表中提取具有特定零件标识号的所有行。这是特定 SQL 的一个典型例子。在 SQL Server 7.0 以前的版本中,特定的 SQL 与存储过程的一个显著差别是,查询优化器所生成的计划从不缓存。查询语句要被读入、编译、执行,然后再抛弃计划。在 SQL Server 7.0 中,正如稍后还要讨论的,实际上提供了可以缓存特定查询语句的计划的机制。
在这条语句被送往 SQL Server 之前,还必须要问几个问题。所有客户机程序都要提供某种游标说明,所以客户机程序在内部必须询问的一个问题是,程序员请求的是什么样的结果集或什么样的游标。最快的类型是在文档中被称为默认结果集的游标。这种游标由于历史上的原因被称为消防站游标,有时甚至根本不把它作为游标看待。当 SQL 请求被送到服务器之后,服务器开始把结果返回给客户机,这个返回结果的过程持续进行,直到把全部数据集发送完毕为止。这就像一个将数据抽给客户机的大型消防站。
一旦客户机程序确定了这是默认结果集,则下一步就是确定是否有参数标记。使用这个 ODBC SQLExecDirect(以及 OLE-DB 中等价的调用)调用的选项之一是,不是在 WHERE 从句中给出像 7 这样的具体值,而是可以用一个问号来传递参数标记,如下所示:
SQLExecDirect(hstmt, "SELECT * FROM parts where partid =
?", SQL_NTS)
请注意,您必须分别提供实际的参数值。
客户机需要知道 SQL 语句中是否有参数标记,或者它是否为真正特定的非参数化 SQL。这将影响到客户机将用这个语句在