posts - 72, comments - 19, trackbacks - 0, articles - 0

从 Oracle 移植递归 SQL 到 DB2 UDB

developerWorks
文档选项
将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项


最新推荐

Java 应用开发源动力 - 下载免费软件,快速启动开发


级别: 初级

Torsten Steinbach , IBM Germany

2003 年 7 月 01 日

首先本文描述了递归SQL如何工作以及它解决哪种类型的问题,然后解释了如何解决在使用递归 SQL从 Oracle 到 DB2 UDB 移植应用程序时可能发生的问题。

重要 :在阅读本文之前,请阅读 免责声明

简介

递归 SQL 是用于查询数据层次结构的一种非常强大的方式。组织结构(部门、子部门、子子部门,等等)、讨论论坛(发贴、响应、对响应的响应,等等)、原料帐单、产品分类以及文档层次结构都是层次型数据的例子。

IBM® DB2® Universal Database™ (UDB)是实现了递归 SQL 的几种关系数据库产品中的一种。通常,可以将 DB2 方法看作一种高度强大和灵活的实现。DB2 在递归优势上的一个体现就是在单个的 DB2 表中查询多个层次结构的能力。(要了解更多这方面的细节,请参考在 DB2 开发者园地(DB2 Developer Domain)上由 Srini Venigalla 撰写的文章 使用 DB2 v7.2 中的 SQL UDF 扩大递归机会

如果您要将数据从一个 RDBMS 移植到另一个 RDBMS,那么重要的是要知道递归 SQL 的实现因产品而异。特别地,在 Oracle 与 DB2 UDB 之间的差异 这一部分,我将解释在将项目从 Oracle 移植到 DB2 并且涉及递归 SQL 时经常会出现的一个问题。

最根本的问题就是,在 Oracle 和 DB2 中,查询的默认排序次序各不相同。乍一看来这并不重要,因为通常应用程序并不十分依赖于默认的排序次序(没有使用 ORDER BY 子句)。然而在实际中,需要用 Oracle 提供的默认排序次序来解决许多问题,例如显示讨论的线索。很多应用程序都是基于 Oracle 的排序次序的假设,因而当要将那些应用程序移植到 DB2 UDB 时,要理解这一点。

当然,除了解释这个问题之外,我还会给出针对 DB2 中这一难题的解决方案的要点。要看这方面的内容,参见 在 DB2 UDB 中仿效 Oracle 的行为这一部分。

为了给读者提供有关一般递归,尤其是递归 SQL 的一些背景信息,我将从简要地介绍 DB2 递归 SQL 开始我们的话题。


递归 SQL 如何工作?

递归通常表现为三个基本的步骤:

  1. 初始化。
  2. 递归,或者在整个层次结构中重复对逻辑的迭代。
  3. 终止。

在初始步骤中,要准备好工作区域,并用初始值设置好变量。递归由工作区域中的商业逻辑操作以及随后对下一递归的调用组成,这里采用一种嵌套的方式。最后,终止步骤用于限定递归。打个比方,可以理解为对嵌套级数进行计数,当达到某一特定级数时便停止执行。

这一原理也可以应用到 DB2 中的递归 SQL。递归 SQL 是一种可以分为三个执行阶段的查询:

  1. 创建初始结果集。
  2. 基于现有的结果集进行递归。
  3. 查询完毕,返回最终的结果集。

初始的结果集建立在对基本表的常规 SQL 查询的基础上,这是公共表表达式(CTE)的第一部分。公共表表达式是用于支持递归的手段,它的第二部分对自己进行调用并将其与基本表相连接。从该 CTE 中进行选择的查询便是终止步骤。

下面的例子演示了这一过程。DEPARTMENT是一个包含了有关某个部门的信息的表:

												
														CREATE TABLE departments (deptid INT,
			  deptname VARCHAR(20),
			  empcount INT,
			  superdept INT)

												
										

这个表的内容代表了一个层次结构。下面的 图 1就是一个例子:


图 1. 一个表层次结构的例子
Table hierarchy

对于一个给定的部门,该部门包括所有的子部门,要获得该部门的雇员人数,需要一个递归查询:

												
														WITH temptab(deptid, empcount, superdept) AS
   (    SELECT root.deptid, root.empcount, root.superdept
            FROM departments root
            WHERE deptname='Production'
     UNION ALL
        SELECT sub.deptid, sub.empcount, sub.superdept
            FROM departments sub, temptab super
            WHERE sub.superdept = super.deptid
   )
SELECT sum(empcount) FROM temptab

												
										

在这个例子中,CTE 被称作 temptab,随着查询的继续执行,temptab 会逐渐变大。下面给出了所有的递归元素:

  1. 在 temptab 中建立初始结果集。它包含了部门“Production”的雇员人数:
    SELECT root.deptid, root.empcount, root.superdept
                FROM departments root
                WHERE deptname='Production'
    

  2. 当在 temptab 中针对于各个子部门加入每一行记录时,便发生了递归。该递归每一次执行的结果都通过 UNION ALL 加入到 temptab 中:
    SELECT sub.deptid, sub.empcount, sub.superdept
                FROM departments sub, temptab super
                WHERE sub.superdept = super.deptid
    

  3. 最后的查询就是从 CTE 中提取出所需的信息。在本例中,进行的是总计操作:
    SELECT sum(empcount) FROM temptab
    

下面是例子查询的结果:

												
														1
-----------
SQL0347W  The recursive common table expression "TORSTEN.TEMPTAB" may contain
an infinite loop.  SQLSTATE=01605
         50
  1 record(s) selected with 1 warning messages printed.

												
										

通过 DB2 解释工具可以检查 DB2 是如何执行这种递归查询的。嵌套的循环连接(NLJOIN)以一个临时结果表(TEMP)为基础,而这次连接的的结果又再次通过 UNION 被放到这个临时表中。


图 2. 对递归 SQL 的解释
Explain of recursive SQL




回页首


Oracle 与 DB2 UDB 之间的差异

Oracle 通过使用 CONNECT BY PRIOR 提供了类似的特性。在 Oracle 中,上面的例子可以这样来实现:

												
														SELECT sum(empcount) FROM STRUCREL
   CONNECT BY PRIOR superdept = deptid
     START WITH deptname = 'Production';

												
										

除了语法上的不同之外,DB2 与 Oracle 在功能性上也有差异。当使用 CONNECT BY PRIOR 时,Oracle 提供了内建的伪列 level。在 Oracle 中,下面的查询提供了所有的部门以及这些部门所在的层次结构:

												
														SELECT deptname, level FROM departments 
  CONNECT BY PRIOR superdept = deptid
  START WITH deptname = 'Samples & Co.';
DEPTNAME             LEVEL
-------------------- -----------
Samples & Co.                  1
Production                     2
QA                             3
Manufacturing                  3
Prebuilding                    4
Finalbuilding                  4
Sales                          2
North                          3
East                           3
South                          3	
West                           3
IT                             2

												
										

这种伪列通常用于限制那些查询的递归深度。例如,为了检索“Sales”这个部门的直属子部门,在 Oracle 中可以使用下面的查询:

												
														SELECT deptname FROM departments CONNECT BY PRIOR superdept = deptid
START WITH deptname = 'Sales' AND level=2;
DEPTNAME
--------------------
North
East
South
West

												
										

在 DB2 中可以轻易地仿效这一特性,只需像下面这样在 CTE 中维护一个自定义的伪列:

												
														WITH temptab(deptid, deptname, superdept, level) AS
   (    SELECT root.deptid, root.deptname, root.superdept, 1
            FROM departments root
            WHERE deptname='Sales'
     UNION ALL
        SELECT sub.deptid, sub.deptname, sub.superdept, super.level+1
            FROM departments sub, temptab super
            WHERE sub.superdept = super.deptid
   )
SELECT deptname FROM temptab WHERE level=2;

												
										

除了 level 伪列,在 DB2 和 Oracle 中另一个非常重要的差异就是由递归查询生成的结果集的搜索次序。在 Oracle 中,层次结构是由深度优先算法创建的。这样一来,当检索整个例子层次结构时,产生的结果集就是这个样子:

												
														SELECT deptname, level FROM departments CONNECT BY PRIOR superdept = deptid
START WITH deptname = 'Samples & Co.;
DEPTNAME             LEVEL
-------------------- -----------
Samples & Co.                  1
Production                     2
QA                             3
Manufacturing                  3
Prebuilding                    4
Finalbuilding                  4
Sales                          2
North                          3
East                           3
South                          3	
West                           3
IT                             2

												
										

这个结果集说明,在查询延伸到邻节点之前,先要浏览完每个子节点。然而,在 DB2 中,层次结构是通过广度优先算法创建的:

												
														WITH temptab(deptid, deptname, superdept, level) AS
   (    SELECT root.deptid, root.deptname, root.superdept, 1
            FROM departments root WHERE deptname='Samples & Co.'
     UNION ALL
        SELECT sub.deptid, sub.deptname, sub.superdept, super.level+1
            FROM departments sub, temptab super
            WHERE sub.superdept = super.deptid
   )
SELECT deptname, level FROM temptab;
DEPTNAME             LEVEL
-------------------- -----------
Samples & Co.                  1
Production                     2
Sales                          2
IT                             2
QA                             3
North                          3
East                           3
South                          3	
West                           3
Manufacturing                  3
Prebuilding                    4
Finalbuilding                  4

												
										

这意味着,结果集是一级一级地创建的。在本例中,这种差异或许算不了什么。但是在有些递归 SQL 的案例中,默认的排序次序则是至关重要的。例如,有一个包含讨论论坛的表:

												
														CREATE TABLE discussion (postid INTEGER,
superid INTEGER,
title VARCHAR2(100),
text VARCHAR2(1000) )

												
										

为了获得对所有讨论线索的了解,在 Oracle 中可以这样来查询这个表:

												
														SELECT RPAD('', level-1, '--') || title FROM discussion
CONNECT BY PRIOR superid = postid START WITH postid = 0;
1
-------------------------------------
Install Problem
--Re: Install Problem
----Re: Install Problem
------Re: Install Problem
--------Got it
--General comment
----Re: General Comment
Cannot find file
--Re: Cannot find file
----Re: Cannot find file
--Re: Cannot find file
Help! Documentation missing!

												
										

在 DB2 中使用无格式(plain)递归 SQL 时,不能以这样的次序重新得到结果集。如果非要尝试这么做的话,将得到下面的结果:

												
														WITH temptab(superid, postid, title, text, level)
     AS 
     (    SELECT root.superid, root.postid, root.title, root.text, 1
              FROM discussion root
              WHERE postid=0
       UNION ALL
          SELECT sub.superid, sub.postid, sub.title, sub.text, super.level+1
             FROM discussion sub, temptab super
             WHERE sub.superid = super.postid
     )
SELECT VARCHAR(REPEAT('--', level-1) || title , 60) FROM temptab;
1
------------------------------------
Problem Discussions
--Install Problem
--Cannot find file
--Help! Documentation missing!
----Re: Install Problem
----General comment
----Re: Cannot find file
----Re: Cannot find file
------Re: Install Problem
------Re: General Comment
------Re: Cannot find file
--------Re: Install Problem
----------Got it

												
										

显然,对于用户来说该结果集完全没有用,因为这里失去了论坛上各个帖子之间的相关性。





回页首


DB2 UDB 中仿效 Oracle 的行为

在 DB2 中,要生成 Oracle 中那样的深度优先次序,解决方案的基础就是引入一个附加的伪列,这个伪列可以在 ORDER BY 属性中使用。这个列的类型是 VARCHAR,包含了到每个节点的路径,其格式为“1.3.1”。另外还引入了一个用户定义的表函数,这个函数可以返回一个给定节点的所有子节点。通过将子节点的序号连接到上级节点的路径上,能够可靠地维护伪列代码。可以使用 DB2 的 RANK() 函数来检索一个子节点的序号。之后,递归查询从这个函数中进行选择,并提供当前节点的 id 以及它的路径作为输入。

下面的例子将创建与上一例子中 Oracle 中的查询完全一致的结果集:

												
														CREATE FUNCTION GetResponses(code VARCHAR(100), superid INTEGER)
   RETURNS TABLE(code VARCHAR(100), superid INTEGER, postid INTEGER,
                 title VARCHAR(100), text VARCHAR(1000))
   READS SQL DATA DETERMINISTIC NO EXTERNAL ACTION
   RETURN SELECT GetResponses.code || '.'
                  || RTRIM(CHAR(RANK() OVER (ORDER BY postid))),
                 T.superid , T.postid, T.title, T.text
              FROM discussion T
              WHERE T.superid = GetResponses.superid;
WITH TEMPTAB(code, superid, postid, title, text, level)
     AS 
     (    VALUES(CAST('1' AS VARCHAR(100)), CAST(NULL AS INTEGER), 0,  
                 CAST(NULL AS VARCHAR(100)), CAST(NULL AS VARCHAR(1000)), 0)
       UNION ALL
          SELECT t.code, t.superid, t.postid, t.title, t.text, level+1
             FROM TEMPTAB,
               TABLE(GetResponses(TEMPTAB.code, TEMPTAB.postid)) AS T
     )
SELECT VARCHAR(REPEAT('--', level-1) || title , 60)
     FROM TEMPTAB T
     WHERE t.superid is not null
     ORDER BY code;
1
-------------------------------------
Install Problem
--Re: Install Problem
----Re: Install Problem
------Re: Install Problem
--------Got it
--General comment
----Re: General Comment
Cannot find file
--Re: Cannot find file
----Re: Cannot find file
--Re: Cannot find file
Help! Documentation missing!

												
										

为了使应用程序中的语句简单一些,这里同样可以将这些递归语句包装到一个 UDF 中。





回页首


一种更好地使用 DB2 Node 类型的方法

您必须清楚,基于一个字符串使用伪列以强制性地使结果集具有某一特定的次序,这只能保证总体上的层次结构次序。如果某个节点的直属子节点的数量超过 9 的话,这样做未必能够正确地对这些子节点排序。这是因为像“1.2.13”这样的字符串比“1.2.13”有着更低的次序。但是从语义上讲,事情刚好相反。如果您要依赖于这种方法,而又不能保证最多只有 9 个直属子节点,那么您就决不能为伪列使用一个字符串。

相反,您可以使用 DB2 Node 类型,这是一个 DB2 扩展,当前在 IBM DB2 Developer Domain 上(由 Jacques Roy 撰写的 Using the Node Data Type to Solve Problems with Hierarchies in DB2 Universal Database )可以获得。您必须使用最低版本为 1.1 的 Node 类型扩展。可以通过 nodeVersion() 函数来检查版本。如果该函数不存在,那么就说明您使用的是更老版本的 DB2 Node 类型。

因此,现在我们不使用 VARCHAR 类型来维护伪列代码,而是使用用户定义类型的 Node。下面的例子对此作了演示。该例子将创建与上面使用 VARCHAR 的例子一样的结果集:

												
														CREATE FUNCTION GetResponsesN(code Node, superid INTEGER)
   RETURNS TABLE(code Node, superid INTEGER, postid INTEGER,
                 title VARCHAR(100), text VARCHAR(1000))
   READS SQL DATA DETERMINISTIC NO EXTERNAL ACTION
RETURN SELECT nodeInput(nodeOutput(GetResponsesN.code) || '.' ||  
                                 RTRIM(CHAR(RANK() OVER (ORDER BY postid)))),
              T.superid , T.postid, T.title, T.text
         FROM discussion T
         WHERE T.superid = GetResponsesN.superid;
WITH TEMPTAB(code, superid, postid, title, text, level)
     AS 
     (    VALUES(nodeInput('1.1'), CAST(NULL AS INTEGER), 0,  
                 CAST(NULL AS VARCHAR(100)), CAST(NULL AS VARCHAR(1000)), 0)
       UNION ALL
          SELECT t.code, t.superid, t.postid, t.title, t.text, level+1
             FROM TEMPTAB,
               TABLE(GetResponsesN(TEMPTAB.code, TEMPTAB.postid)) AS T
     )
SELECT VARCHAR(REPEAT('--', level-1) || title , 60)
     FROM TEMPTAB T
     WHERE t.superid is not null
     ORDER BY code;
1
-------------------------------------
Install Problem
--Re: Install Problem
----Re: Install Problem
------Re: Install Problem
--------Got it
--General comment
----Re: General Comment
Cannot find file
--Re: Cannot find file
----Re: Cannot find file
--Re: Cannot find file
Help! Documentation missing!

												
										

为了创建一个 Node 值,我们必须使用函数 nodeInput(),并为之提供像“1.2” 这样的一个字符串作为输入。对于根节点,输入是“1.1”(由于 DB2 节点类型的具体实现,我们只能从 1.1 开始,而不是从 1 开始)。对于所有其他的节点,我们同样使用 DB2 的 RANK() 函数来为直属子节点分配序号。这是在 GetResponsesN() 函数中进行的。之后,这个序号被连接到上级节点的字符表示(通过 nodeOutput() 获得)上,再将这样得到的字符串作为输入,通过 nodeInput() 函数创建新的 Node 值。





回页首


结束语

DB2 UDB 为递归 SQL 而设的方法提供了一种非常灵活的方式来处理层次结构。正如本文所演示的,DB2 UDB 能够轻易地仿效其他数据库供应商的行为,因为 DB2 通过用户定义的函数提供了方便自如的可扩展性。而且,DB2 UDB 还提供了处理非常高级的递归查询的方法,例如那些在单个表中有多重层次结构的情况下的递归查询。通过使用我描述过的这些技术,在移植应用程序时您可以充分利用 DB2 的长处。





回页首


免责声明

本文包含示例代码。IBM 授予您(“被许可方”)使用这个样本代码的非专有的、版权免费的许可证。然而,该样本代码是以“按现状”的基础提供的,没有任何形式的(不论是明示的,还是默示的)保证,包括对适销性、适用于特定用途或非侵权性的默示保证。IBM 及其许可方不对被许可方由于使用该软件所导致的任何损失负责。任何情况下,无论损失是如何发生的,也不管责任条款怎样,IBM 或其许可方都不对由使用该软件或不能使用该软件所引起的收入的减少、利润的损失或数据的丢失,或者直接的、间接的、特殊的、由此产生的、附带的损失或惩罚性的损失赔偿负责,即使 IBM 已经被明确告知此类损害的可能性,也是如此。





回页首


致谢

感谢 Serge Rielau (IBM)为我指引了正确的方向;感谢 Jacques Roy (IBM)认真审查并正确无误地输入了这篇文档;同时也感谢 Jacques Terrasse (Aldata)为我提供了机会,使我得以在客户环境中得出这一解决方案。





回页首


下载示例

描述 文件类型 文件大小 下载方法
recursive.zip zip 2 KB HTTP HTTP download




回页首






回页首


关于作者

Photo: Torsten Steinbach

Torsten Steinbach是 IBM Partner Enablement in EMEA 在 DB2 和 WebSphere ® 方面的高级技术顾问。可以通过 torsten@de.ibm.com与他联系。

只有注册用户登录后才能发表评论。