marvelyu
作者marvelyu·2013-07-23 11:15
软件开发工程师·IBM

转载一篇不错的文章:洞察 DB2 Universal Database: SQL 语句的生命周期

字数 9968阅读 6781评论 0赞 0

基本概念

要理解 DB2 UDB 如何处理 SQL 语句,需清楚一个基本概念,那就是每一条单独的 SQL 请求都将被映射到 DB2 UDB 内一个特定的包。换句话说,为了处理一条 SQL 语句,请求者必须将该语句与一个特定的 DB2 UDB 包关联起来。对于与 SQL 语句有关的所有应用程序请求,例如 PREPARE、FETCH 和 CLOSE 来说,都是如此。命令行处理器(CLP)和 Call Level Interface(CLI)之类的接口对用户隐藏了这一事实,这些接口将在幕后执行必要的对包的管理。

什么是包?

包是一个信息群集,它控制着任何静态 SQL 语句的编译,同时还部分地控制着任何动态 SQL 语句的编译,而且,包还会影响在其作用域内发出的任何 SQL 请求的执行。包信息包括一些项,例如编译期间所使用的优化级别、在执行期间是否为合格的游标使用块(blocking)。所有这些信息都存储在 SYSCAT.PACKAGES 编目表(catalog table)中,作为表示这个包的一个条目。对于静态 SQL 语句,包还针对每一条语句有一个与其自身相关联的段(section)。段是 SQL 语句的 DB2 UDB 可执行版本。DB2 UDB 使用包作为一个基本授权控制点。为允许某人执行或维护包,可以针对包为之授予或撤消一定的特权(privilege)。这种信息反映在 SYSCAT.PACKAGEAUTH 中。

什么是段条目?

既然一个应用程序可能有很多不同的 SQL 语句(这些语句可能是静态的,也可能是动态的),那么应用程序就可以有一个包。为了使各条 SQL 语句的上下文清晰明了,同时也为了避免对于每一条请求都必须提供这种信息,DB2 UDB 将一个包细分成一些更小的单元,这些单元就叫做段条目(section entry)。段条目包含了有关 SQL 语句自身的信息(如果有的话)以及有关该 SQL 语句在应用程序中所处的上下文的信息。例如,对于一个游标,段条目包含该游标的名称、它是否是 WITH HOLD 游标以及该游标是否被定义为 FOR UPDATE。在执行期间,段条目包含关于与其相关的任何段的当前状态的信息。对于动态 SQL 语句,与一个包一起存储的段条目是空的,这里只是用来作为一个“书签”。

对于应用程序中的每一条惟一的 PREPARE、DECLARE CURSORS 请求或静态 SQL,都有一个惟一的段条目。当在应用程序中发现一条 EXECUTE IMMEDIATE 请求时,也会添加一个惟一的段条目。这个条目由同一应用程序中的所有其他 EXECUTE IMMEDIATE 请求共享。

该图是一个应用程序的内容与一个包中的段条目之间的对应关系的一个例子。我们可以看到,第一条静态 INSERT 语句与段条目 #1 相关联。接下来的两条 PREPARE 请求各自针对一条 SQL 语句(即 S1 和 S2),它们分别与段条目 #2 和段条目 #3 相关联。DECLARE CURSOR 语句所定义的静态游标与段条目 #4 相关联。接下来是两条连续的 EXECUTE IMMEDIATE 请求,这两条请求都与段条目 #5 相关联。最后,还有另一条对于 S2 的 PREPARE 请求。由于它不是一条惟一的语句,因此该请求跟先前的 PREPARE S2 请求一样与同一个段条目(即段条目 #3)相关联。

更精确的基本概念

现在我们已经有了关于一个包的内部格式的更多信息,因而可以重新表述起初的基本概念。关于 DB2 UDB 如何处理 SQL 请求的一个更准确的描述是:在 DB2 UDB 中,每一条请求都被映射为一个特定包中的一个特定段条目。

为什么需要包?

我们需要包的实际原因是,这些包允许 DB2 UDB 支持在同一个包内具有静态和动态 SQL 的任何混合形式。我们可以使用纯静态 SQL,也可以使用静态和动态 SQL 的混合形式,或者使用纯动态 SQL,来编写应用程序。DB2 UDB 通过使用包为所有这些方法提供了支持。通过使用包带来的另一大好处就是,开发人员在开发嵌入式 SQL 应用程序时,同样可以使用应用程序开发中用到的模块化编程技术。他们可以安排来自应用程序不同部分的 SQL 请求,以拥有不同的包,进而拥有不同的 SQL 上下文。有了包,就可以对应用程序采取更细粒度的授权控制。可以使用包授权来控制哪个用户可以执行整个的应用程序,或执行应用程序的一部分。而且,通过使 用静态 SQL 语句,用户就可以执行一些特殊的请求(通过临时地采用包绑定者(binder)的特权不能执行这些请求),但这只适用于包的上下文中。

什么是段?

段是一条 SQL 语句的可执行的真实化身。它包含 DB2 UDB 在产生指定结果时所需的逻辑和数据访问方法。一个段由一系列的操作符和任何相关的操作数组成,这些操作符和操作数勾画出了数据访问的执行顺序以及最合适的 操作。操作符对应于低级的访问和操纵数据的 DB2 UDB 函数。操作数代表数据元素(例如行、表、索引等)以及控制结构。段是 SQL 语句编译后的最终结果。SQL 编译器决定最有效的执行步骤以满足 SQL 语句,并产生一个段来实现这一计划。

段是表达 SQL 语句所需逻辑的一种有效方式,因为它是直接按照特定的 DB2 UDB 内部函数来工作的。段中的内容与数据存储机制的内部细节没有什么关系。由于去掉了 SQL 提供的抽象层,段可以确保在语句的执行期间拥有最佳性能。SQL 语句处理特定的结果集或目标集。一个集可以由任意数量的行组成,并且可以作为一个整体由 SQL 来处理。也就是说,SQL 只认得数据集,而不认得一个一个的行。由于段是从物理上访问一行一行的数据,因此段具有行一级的控制粒度。段中的每一步都是基于对当前行做些什么来定的。 段一行一行地将数据返回给应用程序。最后,通过去掉 SQL 语句的抽象,段允许使用很多不同的获取结果集的 SQL 方法来表示 SQL 语句及其结果集。每一个段都是由它被编译时所在的环境产生出来的产品。当需要支持在不同时候、不同上下文中创建静态和动态 SQL 时,这种灵活性就很有价值。

包是如何创建的?

在 DB2 UDB 包的创建当中有两个基本步骤:预编译(precompilation)和绑定(binding)。尽管有时候在用户看来这两个步骤结合成了一个步骤,但是从 DB2 UDB 的角度来看它们实际上是完全不同的两个步骤。

当包含 SQL 语句和主机变量(host variable)的应用程序源文件被提交给一个预编译器时,预编译步骤就开始了。预编译器是特定于编程语言的(例如 COBOL、FORTRAN 和 C++)。它的工作是解析应用程序源代码以发现任何 SQL 语句和主机变量,移除这些 SQL 语句和主机变量,然后在源文件中用适合于所使用的编程语言的函数调用和变量来替代它们。

这些函数调用是对 DB2 UDB 客户机函数的调用,这些客户机函数在应用程序的执行期间会与 DB2 UDB 进行交互。预编译的最终结果是一个经过修改的源文件以及抽取出来的 SQL 语句和变量。后者可以存储在一个叫做绑定文件(.bnd)的文件中,或者在绑定步骤中直接提交给 DB2 UDB。

在绑定步骤中,要对从应用程序源文件中抽取出来的 SQL 信息进行分析,并将其存储在 DB2 UDB 编目表中。这种信息可能直接来自预编译步骤,也可能来自一个绑定文件(bindfile)。在这一步中,有关包、段条目和主机变量的信息直接存储在 SYSCAT.PACKAGES 编目表中。这一步将通过 SQL 编译器传递静态 SQL 语句,以得到为这些语句生成的段。然后,将这些语句存储在 SYSCAT.STATEMENTS 编目表中。为静态 SQL 语句生成的段存储在 SYSIBM.SYSSECTION 表中。成功绑定后的最终结果是一个DB2 UDB 包。

经过修改的应用程序源文件可以编译并链接成可执行文件。如果调用了与一条 SQL 语句对应的函数,那么 DB2 UDB 就被请求对一个特定段条目执行一个特定的动作,这个条目位于应用程序中包含该函数调用的那一部分所对应的包中。由于只使用了段条目来标识应用程序中的上下 文,修改过的源文件和包被紧紧地链接在一起,并且必须保证它们总是指向相同的上下文。这是通过在修改过的源文件中嵌入一个一致性标志(token),也叫 “时间戳(timestamp)”,并且在 DB2 UDB 内将相同的值存储在包信息中来实现的。来自应用程序的每一条请求都带有这种一致性标志,传入的值要与编目表中的值相比较。如果这两个值不相同,并且装入模 块的时间戳与一致性标志不相同,那么将发生一个“timestamp”错误(SQLCODE -818)。

如果应用程序源文件中有需要更改的地方,那么必须再次预编译该文件,并使用一个绑定重新创建包信息,以确保一致性标志在装入模块中与 DB2 段中是一致的。虽然在被修改的源文件中作一些简单的、非 SQL 编程的更改也是可以的,但是不鼓励这么做,因为在进行下一次预编译时被修改的源文件将被覆盖,所作的更改也将丢失。

段是如何创建的?

从一条 SQL 语句创建一个段的过程是指编译过程。该过程也可以看作是优化或准备。在 DB2 UDB 中应用程序的编译是由 SQL 编译器执行的。这里提到的这一过程既适用于静态 SQL 语句,也适用于动态 SQL 语句。当开始编译时,这两种过程之间的差异便出现了,并且这种值被用于编译环境。静态 SQL 将它的编译环境完全建立在包信息的基础上。而动态 SQL 不仅将其编译环境建立在某些包信息的基础上,而且还建立在许多专用寄存器(例如 CURRENT DEGREE 和 CURRENT SCHEMA)的当前值的基础上。

SQL 编译器是 DB2 UDB 提供的丰富而复杂的 SQL 功能的心脏和灵魂。下面这个图提供了对编译器内处理过程的各阶段的一些洞察,但这决不是完整的。

SQL 编译器有三个输入源:SQL 语句,编目表中的信息,以及作出请求的应用程序的编译环境。编译环境基本上就是请求的上下文,它包含了能够影响由编译器生成的段的最终形式的任何信息和所有信息。这样的信息包括:

  • 要求的隔离级别(isolation level)。
  • 所需的查询优化级别。
  • 发出请求的应用程序的代码页。
  • 所需的内部分区并行性(intra-partition parallelism)的程度。
  • 缺省的限定符(qualifier)。
  • 函数路径(function path)。
  • 用于函数解析的编译时间戳。
  • WITH HOLD 游标标志(flag)。
  • 块级别(blocking level)。
  • 日期/时间格式。

SQL 语句编译过程的第一步是解析阶段。在这一阶段中,首先要检验 SQL 语句的语法,并将该语句拆散成一些组成片段。这一阶段使用来自所有被引用的数据对象或函数的编目表中的信息,以图表(graph)的形式构造出语句的内部 表示。这种表示称作查询图表模型(Query Graph Model)或 QGM。Almaden IBM Research Center 已经发表了很多关于使用 QGM 的优点和有效性的研究论文(可供读者参考)。QGM 为语句提供了一种简明而又灵活的表示,使得语句可以适应为支持复杂 SQL 和高级优化技术所需的任何添加和更改。

QGM 表示是编译器在研究和处理语句时所使用的基本信息结构。一旦构造好了表示基本语句结构的图表,就将该图表传递给处理过程的下一个阶段。这一阶段称作语义 (Semantics)阶段。这一阶段负责使用条目所需的附加信息(例如被引用的视图、该语句所激活的触发器以及对该语句有影响的约束)来补充基本 QGM 图表。考虑到这些实体带进来的附加信息,QGM 图表要进行扩展和修改。

一旦语句的整个作用域被编译器理解和用 QGM 图表表示了,接着便进入处理过程的下一阶段。这一阶段称作查询改写(Query Rewrite)或改写(Rewrite)。这一阶段计算输入的图表,并使用有明确定义的变换(transformation)和等价 (equivalence)技术将输入的 QGM 图表改写成一个新的版本,这种版本为下一阶段 —— Optimizer 提供了最大的灵活性。Rewrite 组件背后的原理很简单,即 SQL 对于相同的结果有多种不同的表达方式。这些 SQL 语句中有些语句的形式要求对编译器作出某些选择,而有些语句则没有这样的要求。因而,为表示相同的结果集,可以使用多种不同的图表,而且,通过将一个图表 从一种版本变换成另一种版本,就可以有更多的选择来得到可能更好的决策和更高效的访问路径。关于查询改写的一个简单的例子是 DISTINCT 子句的移除。由于有可以影响整个结果集的惟一索引的存在,这种子句迫使对结果执行一次排序,以移除重复的行。(在 DB2 UDB Administration Guide 关于 SQL 编译器的章节中有关于这一话题的更多信息)。

Rewrite 阶段结束之后,便进入了编译过程的最重要的阶段 —— 优化(Optimization)。在优化阶段中,要对 QGM 图表进行分析,评估所有可能的访问数据的方法并估计这些方法的代价(cost)。我们经常提到的 Optimizer 组件是一个基于代价的决策制订者,它使用数据访问和数据操纵的可变代价的复杂数学模型来改进和选择最有效的访问计划,以满足原始的 SQL 语句。Optimizer 使用 QGM 图表中引用到的所有不同对象和函数的编目表中提供的统计信息来计算查询中涉及到的代价。取决于所选择的查询优化级别,Optimizer 将使用许多不同的技术来迭代可能的候选方法,根据所考虑的候选方法的种数,其中某些技术比起其他技术来更具有限制性。最后,选定在考虑之列的所有候选方法 中代价最低的那种方法。这个最优的访问计划被传递到编译过程的最后一个阶段。

最后一个阶段是代码生成,或 Codegen,在这一阶段中将产生实际的段。Codegen 阶段将 Optimizer 选定的“理论上的(theoretical)”访问计划转换成由段体现的“实际的(practical)”访问计划。然后,这个段被返回给请求者,以便执 行。

这里还应提到编译过程的两个副产品。除了段之外,SQL 编译器还将返回与该段相关联的依赖关系(dependency)和权限的一个列表。依赖关系列表包含为保证段的执行完整性而必须连续、不变地存在的那些对 象。权限列表包含该段任何预期的执行者必须具有的对该段所引用的特定对象的特定权限。

所有这些过程如何工作?

首先,为了开始这一系列的过程,必须有一个应用程序直接或间接地生成一条对 DB2 UDB 的 SQL 请求。从 DB2 UDB 看来,该应用程序要么是一个嵌入式静态应用程序,要么是一个嵌入式动态应用程序。然而,从用户的角度来看,存在很多其他的选择。例如,他们可以使用 CLI (ODBC) 接口 —— 这只是一个编程接口,并不需要预编译。CLI 本身是一个预定义的嵌入式动态应用程序,它对用户隐藏了任何作为先决条件的包操作。命令行处理器也是一个类似的接口。其他的选择包括 JDBC 和 SQLJ。当然还有其他一些选择,但从 DB2 UDB 的角度来看它们无非就是同一样东西:一个嵌入式 SQL 应用程序。

在 DB2 UDB 中,对 SQL 请求的运行时处理引入了两个新的内部组件:Package Cache 和 Runtime Interpreter。Package Cache 是一个全局的、数据库级的缓存,被用来作为一个储存库,或“公共库(public library)”,以便储存包和段信息、静态 SQL段以及缓存的动态 SQL 语句和它们相应的段。Runtime Interpreter 是段的执行者。该组件将段中的操作符映射为指定的低级函数,调用该函数,最终将结果返回给应用程序。

每一条提供给 DB2 UDB 的 SQL 请求都附带了以下信息:

  • 请求类型。
  • 包模式和名称。
  • 段条目编号。
  • 附加的请求信息。

DB2 UDB 所处理的每一条 SQL 请求是基于两个条件来计算的,即指定包中指定段条目中的上下文信息以及与那个段条目相关的 SQL 语句的当前状态(如果有的话)。当 DB2 UDB 收到一条请求时,要查询相关的包和段信息,以确认该请求的合法性并获得它的上下文。如果 DB2 代理(一个 DB2 请求处理实体)在其工作内存中没有这样的信息,那么就从 Package Cache 复制一份这样的信息到工作内存中去。

接下来访问被请求的段。如果没有找到这个段,就从 Package Cache 读取它。对于动态 SQL,如果在具有相同编译环境的包缓存中有一条匹配的语句,那么就可以使用现有的为那条语句缓存的段(假设用户具有必需的权限)。如果不存在匹配的语 句,或者没有匹配的编译环境,那么包缓存将调用 SQL 编译器来为新环境生成一个新的段,将段和环境缓存在相同的语句文本之下,然后返回被请求的段。

一旦得到了段,就调用 Runtime Interpreter 来执行这个段。Runtime Interpreter 是 DB2 UDB 中负责控制一个段的执行以及将结果返回给应用程序的那一部分。当第一次将一个段提交给 Runtime Interpreter 时,需要进行一些特殊的处理。当 SQL 编译器生成一个段的时候,这个段的内部指针是以该段内的绝对偏移量的形式存储的。这样做是为了可以根据每种不同的执行环境对段内指针作出适当的调整。对段 进行重定位时会将这些偏移量变成普通的指针。除了重定位以外,一个段在执行期间所需的许多缓冲器和控制结构都要分配内存并与该段关联起来。一旦 Runtime Interpreter 创建了这个初始配置,代理就可以执行这个段了。

段的执行的作用域的定义要看是否有游标使用这个段。对于没有游标的情况,段的执行跨度就是特定的 SQL 请求。对于有游标的情况,这个执行跨度就是从 OPEN CURSOR 请求,经过所有的 FETCH 请求,一直到收到 CLOSE CURSOR 请求。如果段引用了专用寄存器,那么在开始每一次执行之前,都要将被引用的专用寄存器的当前值复制到这个段中。这样做是为了确保该段所使用的专用寄存器的 值在整个执行跨度中保持不变。

当一个段被执行的时候,首先要对初始的系列操作符(也称 Open 线程)执行一次评估 —— 假设存在这样的线程。该线程创建段主体所需的各项数据或中间集。在段的执行的其余时间内,这些结果集不会改变。对于特定的段执行,Open 线程只执行一次。接下来,执行段的主线程,并将结果返回给请求者。对于游标操作,OPEN 请求执行 Open 线程,而随后的一些 FETCH 请求则一一执行主线程。

注意,对于使用了块的游标,当收到第一条从应用程序发来的 FETCH 请求时,DB2 UDB 就会在内部生成一系列的 FETCH 请求。这些内部的 FETCH 请求会一直延伸下去,直到当前的数据块已满。然后,将这个块发送给应用程序。随后的应用程序 FETCH 请求都是在那个数据块的客户机上处理的。一旦用了这个数据块,下一个应用程序 FETCH 请求又将被发送到 DB2 UDB,于是重复这一过程。

一旦执行结束,Runtime Interpreter 就关闭段。关闭一个段的过程包括重新设置标志和控制结构、关闭打开的内部扫描以及释放可能存在的锁。对于游标操作,段的关闭是由 CLOSE 请求发起的。对于所有其他情况,段的关闭是在执行结束时自动进行的。Runtime Interpreter 执行的段处理的最后一个方面是段的终止(termination)。在应用程序终止时要释放段的时候,或者由于其他原因而需要内存的时候,便会出现段的终 止。段的终止会回收与段相关的控制结构以及内存。

一旦执行了段,并且确定了结果,就将这些结果返回给应用程序。DB2 UDB 返回的数据由 SQLCA、SQLDA 和数据组成。SQLCA 通过 SQLCODE 和 SQLSTATE 字段来表明 SQL 的成功和失败。SQLCA 还包含其他的重要信息,例如消息标志(message token)、警告标志(warning flag)以及其他诊断数据。SQLDA 包含对数据本身的一个描述:数据元素的个数、每个元素的数据类型以及每个元素的长度。

一个段的生命跨度(life span)

一旦一个段被创建,它的生命跨度就取决于它的本性以及它所在的数据库中当前活动的类型。如果存储在编目表中,一条静态 SQL 语句的自然生命跨度就是永久的。只有当包被撤消,或者段所依附的对象发生了什么事情,才可以将段移除。静态 SQL 的依赖关系是在包这一级记录的。也就是说,对一个包中所有静态 SQL 段的所有依赖关系都是记录在编目表中这个包的名下。如果有会影响一个包的一条依赖关系记录的事情发生,那么在包被重新绑定之前,整个包将变得无效,并且包 中所有的段都不再可用。会影响一个包的动作有DROP TABLE、DROP INDEX 或 altER TABLE。

大多数会影响一个包的 DDL 都会使包被标记为无效(invalid)。当这样的事情发生时,接下来用于引用这个包的 SQL 请求会导致 DB2 UDB 隐式地重新绑定这个包。但也有例外,那就是将一个包标记为不起作用的(inoperative)。当撤消一个特定的 UDF 依赖关系时,就会这么做,这是为了在用户知情的情况下作出重新绑定的决定。之所以要这样,是因为包中 SQL 的语义可能会随着所选择的不同函数而改变。在未经用户同意的情况下,不应该将包标记为不起作用的。对于不起作用的包,必须由用户显式地重新绑定这个包。

动态 SQL 的生命跨度要受到缓存它的数据库节点的活动时间的限制。这是因为动态 SQL 的生命跨度最终是由 Package Cache 的生命跨度控制的。对动态 SQL 段生命跨度的另一个限制因素是 Package Cache 中的可用空间。当 Package Cache 中的空间用完时,就要释放段所占的空间。就 DDL 和其他数据库活动的效果而论,缓存的动态 SQL 语句比起缓存的静态 SQL 语句来既有更不敏感的一面,也有更敏感的一面。只有那些依赖于受影响的对象的特定段才被标记为无效,就这一点来说,缓存的动态 SQL 语句不够敏感。又因为有更多的动作可以影响它们,所以缓存的动态 SQL 语句又是更加敏感的。除了对会影响静态 SQL 的 DDL 敏感,缓存的动态 SQL 语句还必须对较小的任何依赖对象的属性变化敏感,例如统计信息的更新、新索引的创建或者表的重新组织。之所以需要这种敏感性,是为了维持 DB2 的原则,即:动态 SQL 语句以及它们的段总是反映当前的数据库环境。

当一个动态 SQL 段失效时,接下来的 SQL 请求将导致 DB2 UDB 发起一次隐式的重编译,或为该语句作准备,以生成一个新的段


http://www.ibm.com/developerworks/cn/data/library/techarticles/idugnov9902/idugnov9902.html#ibm-pcon

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

0

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广