使用XQuery查询DB2 XML数据
关于 XQuery
XQuery 在很多关键方面都与 SQL 有所不同,这很大程度上是因为这两种语言是针对两种具有不同特征的数据模型而设计的。XML 文档包含层次结构,并且有其固有的顺序。而基于 SQL 的数据库管理所支持的表格数据结构是平面的(flat),并且是基于集合的;因此,行之间不存在顺序。
这两种数据模型的不同导致它们各自的查询语言有很多基本的不同。例如,XQuery 支持路径表达式,以允许程序员在 XML 的层次结构中导航,而纯 SQL(没有 XML 扩展)则不支持。XQuery 支持有类型的和无类型的数据,而 SQL 数据总是以指定类型定义的。XQuery 没有 null 值,因为 XML 文档会忽略缺失的或未知的数据。当然,SQL 使用 null 来表示缺失的或未知的数据值。XQuery 返回一系列的 XML 数据,而 SQL 则返回各种 SQL 数据类型的结果集。
这只是 XQuery 和 SQL 之间的基本不同点中的一部分。提供一份详尽的列表超出了本文的范围,不过即将发表的 IBM Systems Journal 将更详细地讨论这些语言的不同。现在我们就探索一下 XQuery 语言的一些基本方面,并看看如何使用它来查询 Viper 中的 XML 数据。
样本数据库
本文中的查询访问在 “ Viper 快速入门”(developerWorks,2006 年 4 月)中创建的样本表。清单 1 给出了样本数据库中 “items” 和 “clients” 表的定义:
清单 1. 表定义
create table items ( id int primary key not null, brandname varchar(30), itemname varchar(30), sku int, srp decimal(7,2), comments xml ) create table clients( id int primary key not null, name varchar(50), status varchar(10), contactinfo xml ) |
图 1. 存储在 “items” 表的 “comments” 列中的样本 XML 文档
图 2. 存储在 “clients” 表的 “contactinfo” 列中的样本 XML 文档
查询环境
本文中的所有查询都是通过交互方式发出的。这可以通过 命令行处理器或 Control Center 的 Command Editor 来完成。本文中的屏幕图像和说明主要使用后一种方式。( Viper 还附带了一个基于 Eclipse 的 Developer Workbench,它可以帮助程序员以图形化的方式构造查询。本文不讨论应用程序开发问题和 Developer Workbench。)
要使用 Command Editor,启动 Control Center,并选择 Tools -> Command Editor。这时将出现如 图 3 所示的窗口。在上面的面板中输入查询,单击左上角的绿色箭头运行该查询,然后可以在下面的面板中或者在 “Query Results” 选项卡中查看输出。
图 3. Command Editor,可以从 Control Center 中启动
XQuery 例子
与在 “用 SQL 查询 XML 数据” 中一样,本文将逐步讲解一些常见的业务场景,并展示如何使用 XQuery 来满足对 XML 数据的请求。本文还探索了需要将 SQL 嵌入在 XQuery 中的更复杂的情景。
XQuery 提供了一些不同类型的表达式,这些表达式可以随意组合。每个表达式返回一系列的值,这些值又可以作为其他表达式的输入。最外面的表达式的结果就是查询的结果。
本文主要讨论两种重要的 XQuery 表达式:“FLWOR” 表达式和路径表达式。FLWOR 表达式非常像 SQL 中的 SELECT-FROM-WHERE 表达式 —— 它用于对由多项组成的一个列表进行迭代,并且可以选择返回通过在每一项上进行计算得到的值。而路径表达式则可以在分层的 XML 元素之间进行导航,并返回在路径末端找到的元素。
与 SQL 中的 SELECT-FROM-WHERE 表达式类似,XQuery FLWOR 表达式可以包含数个以某个关键词开头的子句。在 FLWOR 表达式中,有以下用于作为子句开头的关键字:
- for:对输入序列进行迭代,依次将一个变量绑定到每个输入项
- let:声明一个变量并为之赋值,可能是一个包含多项的列表
- where:指定过滤查询结果的标准
- order by:指定结果的排序顺序
- return:定义所返回的结果
在很多情况下,可以任意使用 FLWOR 表达式或路径表达式编写查询。
使用 XQuery 作为顶层查询语言
要在 Viper 中直接执行 XQuery(而不是将它嵌入在 SQL 语句中),必须以关键字 xquery 作为查询的开头。这个关键字将指示 调用它的 XQuery 解析器来处理请求。注意,只有在使用 XQuery 作为最外层(顶层)语言的时候才需要这么做。如果是将 XQuery 表达式嵌入在 SQL 中,则不需要在语句之前加上 xquery 关键字。但是,本文使用 XQuery 作为基本语言,因此所有查询之前都加上 xquery。
当 XQuery 被作为顶层语言时,它需要一个输入数据的源。XQuery 获得输入数据的一种方式是调用一个名为 db2-fn:xmlcolumn 的函数,调用时带一个参数,表明 表中 XML 列所在的表名和该列的列名。db2-fn:xmlcolumn 函数返回存储在给定列中的一系列的 XML 文档。例如,下面的查询返回一系列包含客户联系方式信息的 XML 文档:
清单 2. 返回客户联系方式数据的简单 XQuery
xquery db2-fn:xmlcolumn('CLIENTS.CONTACTINFO') |
您可能还记得,在我们的数据库模式中(参见 “样本数据库” 小节),我们将那些 XML 文档存储在 “clients” 表的 “contactinfo” 列中。注意,这里的列名和表名是大写的。这是因为表名和列名在被写入到 的内部编目之前通常要换成大写形式。由于 XQuery 是大小写敏感的,因此小写的表名和列名不能与 编目中的大写名称相匹配。
检索特定的 XML 元素
首先我们来看一个基本的任务。假设您要检索所有提供了有关传真信息的客户的传真号。清单 3 给出了编写该查询的一种方式:
清单 3. 检索客户传真数据的 FLWOR 表达式
xquery for $y in db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client/fax return $y |
第一行指示 调用它的 XQuery 解析器。接下来的一行指示 对包含在 CLIENTS.CONTACTINFO 列中的 Client 元素的 fax 子元素进行迭代。每个 fax 元素被依次与变量 $y 绑定。第三行指出在每次迭代中返回 $y 的值。结果为一系列的 XML 元素,如 清单 4 所示:
清单 4. 上述查询的示例输出
<fax>4081112222</fax> <fax>5559998888</fax> |
随便提一下,这里的输出还将包含与本文关系不大的一些信息:XML 版本和编码数据,例如 <?xml version="1.0" encoding="windows-1252" ?>,以及 XML 名称空间信息,例如 <fax xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">。为了使输出更简单,本文省略了这些信息。然而,对于很多 XML 应用程序来说这些信息可能很重要。如果使用 命令行处理器来运行查询,那么可以使用 -d 选项来省略 XML 声明信息,还可以使用 -i 选项以一种美观的方式打印结果。
清单 3 中显示的查询也可以用一种更简单的三步路径表达式来表达,如 清单 5 所示:
清单 5. 检索客户传真数据的路径表达式
xquery db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client/fax |
如果您无兴趣通过查询获得 XML 片段,而只想要符合条件的 XML 元素值的文本表示,那么可以在 return 子句中调用 text() 函数,如 清单 6 所示:
清单 6. 两个用于检索客户传真数据的文本表示的查询
xquery for $y in db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client/fax return $y/text() (or) xquery db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client/fax/text() |
清单 7. 上述查询的示例输出
4081112222 5559998888 |
这些示例查询的结果都相当简单,因为 fax 元素是基于基本数据类型的。当然,元素也可能基于复杂的数据类型 —— 即包含子元素(或嵌套层次结构)。例如客户联系方式信息中的 Address 元素就是这样。根据 “ Viper 快速入门”(developerWorks,2006 年 4 月)中定义的模式,该元素包含街道地址、门牌号、所在城市、州、国家以及邮政编码。考虑清单 8 中的 XQuery 将返回什么结果:
清单 8. 检索复杂 XML 类型的 FLWOR 表达式
xquery for $y in db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client/Address return $y |
清单 9. 上述查询的示例输出
<Address> <street>5401 Julio Ave.</street> <city>San Jose</city> <state>CA</state> <zip>95116</zip> </Address> . . . <Address> <street>1204 Meridian Ave.</street> <apt>4A</apt> <city>San Jose</city> <state>CA</state> <zip>95124</zip> </Address> |
过滤 XML 元素值
您可以修改上述 XQuery 例子,缩小选择范围。例如,我们来看看如何返回居住在邮政编码为 95116 的地区的所有客户的邮递地址。
与您想像的一样,通过 XQuery where 子句可以根据 XML 文档中 zip 元素的值来过滤结果。清单 10 说明了如何在 清单 8 中的 FLWOR 表达式中添加一个 where 子句,以获得您感兴趣的地址信息:
清单 10. 带有 “where” 子句的 FLWOR 表达式
xquery for $y in db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client/Address where $y/zip="95116" return $y |
通过在路径表达式中添加一个谓词也可以得到相同的结果,如 清单 11 所示:
清单 11. 带附加过滤谓词的路径表达式
xquery db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client/Address[zip="95116"] |
清单 12. 在 FLWOR 表达式中根据多个 XML 元素值过滤
xquery for $y in db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client where $y/Address/zip="10011" or $y/Address/city="San Jose" return $y/email |
通过路径表达式可以更简单地表达相同的查询:
清单 13. 使用路径表达式根据多个 XML 元素值进行过滤
xquery db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client[Address/zip="10011" or Address/city="San Jose"]/email; |
- 返回的结果中不包含那些符合条件但是没有提供电子邮件地址的客户的 XML 数据。换句话说,如果有 1000 个客户居住在圣河塞或邮政编码为 10011 的地区,其中有 700 个客户提供了电子邮件地址,那么返回的将是这 700 个电子邮件地址。这是由于前面提到的 XQuery 与 SQL 之间存在的基本差异 —— XQuery 不使用 null。
- 您无法知道哪些电子邮件地址来自同一个 XML 文档。换句话说,如果有 700 个居住在圣河塞或邮政编码为 10011 的地区的客户,并且每个客户提供了两个电子邮件地址,那么返回的结果是 1400 个 email 元素组成的列表。您得到的不是 一个包含 700 个记录、每个记录由两个电子邮件地址组成的序列。
有多种方法来修改这个查询,使返回的结果以某种方式表示缺失的信息,并且在有多个电子邮件地址来自相同客户记录(即相同的 XML 文档)的情况下作出说明。让我们简要地探索一下其中一种方法。不过,如果只是要检索一个列表,其中对于每个符合条件的客户包含一个电子邮件地址,那么只需对之前查询中的 return 子句略作修改:
清单 14. 只检索每个客户的第一个 email 元素
xquery for $y in db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client where $y/Address/zip="10011" or $y/Address/city="San Jose" return $y/email[1] |
转换 XML 输出
XQuery 的一个强大的方面是可以将 XML 输出从一种格式转换成另一种格式。例如,可以使用 XQuery 检索所有或部分存储的 XML 文档,并将输出转换成 HTML,以便在 Web 浏览器中显示。下面 清单 15 中的查询检索客户的地址,按照邮政编码对结果排序,并将输出转换成 XML 元素,作为一个无序的 HTML 列表中的一部分:
清单 15. 查询 XML 数据并以 HTML 格式返回结果
xquery <ul> { for $y in db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client/Address order by $y/zip return <li>{$y}</li> } </ul> |
第三行对客户地址进行迭代,依次将变量 $y 绑定到每个 address 元素。第四行包括一个新的 order by 子句,指出结果必须按照客户邮政编码(绑定到 $y 的每个 address 元素的 zip 子元素)升序排列。return 子句表明 Address 元素在返回之前要用 HTML 列表 item 标记括起来。最后一行结束查询,并结束 HTML 无序列表标记。
输出将类似 清单 16 所示:
清单 16. 上述查询的示例 HTML 输出
<ul> <li> <Address> <street>9407 Los Gatos Blvd.</street> <city>Los Gatos</city> <state>CA</state> <zip>95032</zip> </Address> </li> <li> <Address> <street>4209 El Camino Real</street> <city>Mountain View</city> <state>CA</state> <zip>95033</zip> </Address> </li> . . . </ul> |
清单 17. 在 XQuery 结果中表明缺失的值和重复的元素
xquery for $y in db2-fn:xmlcolumn('CLIENTS.CONTACTINFO')/Client where $y/Address[zip="10011"] or $y/Address[city="San Jose"] return <emailList> {$y/email} </emailList> |
清单 18. 上述查询的示例输出
<emailList> <email>love2shop@yahoo.com</email> </emailList> <emailList/> <emailList> <email>beatlesfan36@hotmail.com</email> <email>lennonfan36@hotmail.com</email> </emailList> . . . |
XQuery 的 XML 输出转换功能可以与它内置的对条件逻辑的支持相结合,以减少应用程序代码的复杂性。“items” 表中包括一个 XML 列,该列包含客户对产品作出的评论。有些客户要求对他们的评论作出响应,对于这些用户,需要创建新的 “action” 元素,其中包含产品 ID、客户 ID 和评语,以便将这些信息发送给适当的个人进行处理。但是,那些不要求响应的评论在商业上也是非常重要的信息,您不想忽视它们。所以,创建一个 “info” 元素,其中只包含产品 ID 和评语。下面展示了如何使用 XQuery if-then-else 表达式来完成这项任务:
清单 19. 在 XQuery 中使用 “if-then-else” 表达式
xquery for $y in db2-fn:xmlcolumn('ITEMS.COMMENTS')/Comments/Comment return ( if ($y/ResponseRequested = 'Yes') then <action> {$y/ProductID, $y/CustomerID, $y/Message} </action> else ( <info> {$y/ProductID, $y/Message} </info> ) ) |
使用 “let” 子句
您已经看到了如何使用 FLWOR 表达式的所有部分,除了一个部分:let 子句。该子句用于将一个值(可能包含一个由多项组成的列表)赋给一个变量,这个变量可以在 FLWOR 表达式的其他子句中使用。
假设您想得到一份列表,统计出每种产品收到的评论数量。那么可以使用以下查询:
清单 20. 使用 “let” 子句
xquery for $p in distinct-values (db2-fn:xmlcolumn('ITEMS.COMMENTS')/Comments/Comment/ProductID) let $pc := db2-fn:xmlcolumn('ITEMS.COMMENTS') /Comments/Comment[ProductID = $p] return <product> <id> { $p } </id> <comments> { count($pc) } </comments> </product> |
这个例子查询的结果如下:
清单 21. 上述查询的示例输出
<product> <id>3926</id> <comments>28</comments> </product> <product> <id>4097</id> <comments>13</comments> </product> |
至此,您已经看到了如何编写 XQuery 来检索 XML 文档片段,创建新格式的 XML 输出,以及根据查询中指定的条件返回不同的输出。简言之,您已经学会了使用 XQuery 查询存储在 中的 XML 数据的几种方法。
显然,除了本文介绍的内容外,关于 XQuery 还有更多要学的东西。但是我们不能忽略了我们还没有讲到的一个大话题:如何在 XQuery 中嵌入 SQL。如果需要编写根据 XML 和非 XML 列的值过滤数据的查询,那么这样做很有用。
您可能还记得,文章 “用 SQL 查询 XML 数据” 描述了如何将简单的 XQuery 表达式嵌入在 SQL 语句中,以便完成这样的任务。这里,我们来看看如何反过来做:将 SQL 嵌入在 XQuery 中,从而根据传统的 SQL 数据值和特定的 XML 元素值对结果进行限制。
您可以使用 db2-fn:sqlquery 函数替代 db2-fn:xmlcolumn 函数,后者返回一个表的一个列中所有 XML 数据,而前者执行一个 SQL 查询,并且只返回所选择的数据。传递给 db2-fn:sqlquery 函数的 SQL 查询必须返回 XML 数据。然后 XQuery 可以进一步处理这种 XML 数据。
清单 22 中的查询检索关于评论的信息,评论所涉及的产品是建议零售价("srp")大于 $100 的产品,并且在评论中客户请求响应。您应该记得,价格数据存储在一个十进制数类型的 SQL 列中,而客户评论则存储为 XML。对于存储在数据库中的每条符合条件的评论,返回的数据(包括产品 ID、客户 ID 和客户评论)被包括在一个 XML “action” 元素中。
清单 22. 将 SQL 嵌入在 XQuery 中
xquery for $y in db2-fn:sqlquery('select comments from items where srp > 100')/Comments/Comment where $y/ResponseRequested="Yes" return ( <action> {$y/ProductID, $y/CustomerID, $y/Message} </action> ) |
清楚这些之后,让我们考虑如何解决一个稍微不同的问题。假设您需要一份列表,其中包含居住在圣河塞的 “Gold” 客户的所有 e-mail 地址。而且,如果一个客户有多个 e-mail 地址,那么您希望将这些 e-mail 地址都包括在输出中,作为单个客户记录的一部分。最后,如果某个符合条件的 “Gold” 客户没有提供 e-mail 地址,那么需要检索他或她的邮递地址。清单 23 展示了编写该查询的一种方法:
清单 23. 将 SQL 嵌入在包含条件逻辑的 XQuery 中
xquery for $y in db2-fn:sqlquery('select contactinfo from clients where status=''Gold'' ')/Client where $y/Address/city="San Jose" return ( if ($y/email) then <emailList>{$y/email}</emailList> else $y/Address ) |
此外,这个查询中的 return 子句包含一个条件逻辑,用于判断给定客户的记录中是否存在 e-mail 元素。如果存在,那么该查询将返回一个新的 “emailList” 元素,其中包含客户的所有 e-mail 地址(也就是那个客户的所有 e-mail 元素)。如果不存在,那么该查询将返回客户的邮递地址(也就是那个客户的 Address 元素)。
索引
最后,值得注意的是,您可以专门创建 XML 索引来加快对存储在 XML 列中的数据的访问速度。由于本文只是一篇介绍性的文章,而且样本数据偏小,因此这里不打算就此话题展开讨论。但是在生产环境中,定义适当的索引对于取得最佳性能是非常关键的。请参阅 参考资料 了解关于 新的索引技术的更多信息。 本篇文章发表于
结束语
XQuery 与 SQL 之间有一些很大的不同点,本文谈到了其中的一些不同点。了解更多关于该语言的知识将有助于判断该语言在什么情况下对您的工作最有好处,并且有助于理解在什么情况下将 XQuery 与 SQL 相结合会比较有用。在将来的文章中,我们将深入研究另一个有趣的话题:如何开发利用 DB2 XML 功能的 Java 应用程序。本文提供了一个 简单的 Java 例子,其中描述了如何在 Java 应用程序中嵌入 XQuery。