RSS、Atom、mashup、高级搜索要求和其他发展正使得原生 XML 数据库成为搜索应用程序和服务的一个重要组成部分。XML 数据库类型的优势在于擅长高效地在大量半结构化(semi-structured)的数据中进行搜索。在本文中,您将发现一些用于最大化使用 XQuery 和 XML 数据库的应用程序的性能的一般原则。
XQuery 和原生 XML 数据库
在某些情况下,在原生 XML 数据库系统中使用 XQuery(一种用于查询 XML 数据集合的函数型语言)可能非常有用。与标准关系数据库相比,原生 XML 数据库在服务于主要是只读的复杂查询时能够提供更快的响应时间和开发时间。XQuery 是目前最简单、最强大的数据转换系统,它完美地内置在查询语言中。借助 XQuery,可以实现更快的开发时间,因为无需设计一个单独的全文本索引系统,或者为用户组装大量数据。
以减慢插入和更新速度为代价,原生 XML 数据库能够提供无与伦比的开箱即用响应时间,因为它们保持数据基本上非规格化(denormalized),提供默认索引,并能极好地利用可用 RAM。但是,在处理超大型数据集时,您还可以通过遵循以下一般原则进一步改善原生 XML 数据库的查询响应时间。
1、避免规格化
2、采用唯一的元素名称
3、预先计算值
4、通过查询转换数据
5、剖析 XQuery 代码
6、保留优化列表
这些原则是通用的,适用于当今可用的许多原生 XML 数据库,包括 IBM DB2 Express-C、Mark Logic Server、eXist、甚至 Oracle Berkeley DB XML(参见 参考资料 中的链接)。接下来,我们将详细探讨这些优化原则。
避免规格化
设计原生 XML 数据库模式时,最重要的事情是避免使用设计关系数据库时采用的方法来规格化数据。
原生 XML 数据库的数据规格化过程涉及到设计多个 XML 文档类型,这些文档类型相互链接的方式与关系模型表相互链接的方式类似。但是,在多数情况下,需要尽可能少(如果有的话)地规格化原生 XML 数据库的数据。将原本应该驻留在几十个关系模型表中的数据存储到一个 XML 文档类型中的做法是十分常见的。
现今的大多数 XQuery 实现执行连结(join)操作的效率很低,即使是一个只涉及几千条记录的简单查询都需要耗费大量的、令人难以接受的处理时间。这就使得下面这条决定是否应该规格化数据的标准很明确:永远不要规格化数据,以免受支持的查询需要执行连结操作来选择记录。
受支持的查询是这样一种查询,即您可以合理地预期用户怎么对待您的数据。例如,如果构建一个用于销售录像带的应用程序,您可能预期用户会查询标题中包含某个关键字并且由某个导演执导的所有视频。因此,您肯定希望表示视频的 XML 文档包含视频标题和导演姓名。另一方面,对于这个特定的应用程序,您也许不希望支持用户查询标题中包含某个关键字并且由纽约出生的某位导演执导的所有视频。换句话说,对于这个视频应用程序示例,如果您拥有导演的详细信息(不仅仅是导演姓名),可以考虑将这些信息保存在一个单独的 XML 文档中。
使用以下两个互相链接的 XML 文档类型来描绘数据库:video-rec 和 director-rec,前者带有关于视频的信息,其中包含一个 director-rec 标识符;后者带有关于导演的信息。要查询标题中包含某个关键字且导演出生于纽约的记录,必须执行连结操作以选择记录。如前所述,也可以不支持这种类型的查询,因为这是一种更侧重于数据挖掘的查询,而不是大多数浏览在线视频商店的用户通常执行的查询类型。但是,除非您有具体原因需要将关于导演的详细信息移动到一个单独的文档类型中,否则应该将这些信息保存在 video-rec 文档中。
尽管在原生 XML 数据库中执行连结操作来选择记录总是低效的,但是在转换搜索结果中的数据时从多个 XML 文档提取数据通常是可取的。我此前描述的视频商店能够轻松高效地呈现包含导演出生地的结果,尽管获取这个地址需要从原始搜索结果之外的文档提取数据。以这种方法组装结果,所需的操作仅限于应用程序已经选择和计划显示的少数几条记录,与常规的搜索查询中连结多个文档类型所需的资源相比,这种方法的计算和内存需求可以忽略不计。
采用唯一的元素名称
唯一元素 总是指向一个 XML 文档中的同一元素。非唯一元素 可以出现在 XML 文档中的任意位置,必须前置一个路径才有意义。例如,如果一个 XML 文档包含 10 个类型完全不同的节点,每个节点都包含一个日期元素作为其子节点,那么日期元素就是非唯一元素名称。采用非唯一元素名称会影响您评估或剖析一些用于定位数据的 XQuery 或 XPath 替代方法。例如,非唯一元素能够阻止您正确评估执行较少索引查询的代码。此外,非唯一元素还会阻碍正在兴起的对面搜索结果(faceted search result)的支持。
以下小节提供各种优化的例子,您可以通过更改类似于 清单 1 中的文档的设计以便它使用唯一的元素名称,来支持这些优化。
清单 1. 带有非唯一元素名称的基础文档
#+BEGIN_SRC nxml-mode <class-info> <school>Lusher Elementary School</school> <grade>10</grade> <teachers> <teacher> <name> <first>Carol</first> <last>Osborne</last> </name> </teacher> <teacher> <name> <first>Dan</first> <last>Silver</last> </name> </teacher> </teachers> <students> <student> <name> <first>Barrie</first> <last>Stoff</last> </name> </student> <student> <name> <first>Andrew</first> <last>Silver</last> </name> </student> <student> <name> <first>Larry</first> <last>Cracchiolo</last> </name> </student> <student> <name> <first>Richard</first> <last>Hughes</last> </name> </student> <student> <name> <first>Bruce</first> <last>Silver</last> </name> </student> . . . </students> </class-info> #+END_SRC |
执行较少的索引查询
要查询姓 Silver 的学生的名,应该使用类似于 清单 2 中的 XPath 表达式。
清单 2. 查询姓 Silver 的学生的 XPath 表达式
: /class-info/students/student/name[last = "Silver"]/first |
如果将数据限制为 清单 1 中单个文档中的可见数据,那么计算 清单 2 中的 XPath 表达式总是正确地返回 清单 3 中的结果。
清单 3. XPath 结果
<first>Andrew</first> <first>Bruce</first> |
如果数据没有被索引,那么 清单 2 总是获得结果的最快方法。这个表达式限制了数据库找到相关结果所必须搜索的分支的数量。
但是,如果数据已经索引,并且根据您使用的特定数据库实现,假设您有一个非常大的数据集,那么类似于 清单 4 中的表达式可能计算速度更快。
清单 4. 数据已索引时使用的 XPath 表达式
: //name[last = "Silver"]/first |
性能可能会改进的原因是,系统只需检查索引中较少的元素。但是,鉴于 清单 1 中文档的设计(使用非唯一元素名称),清单 4 中的 XPath 表达式将返回错误的结果:包含一个教师的名字 Dan。这种设计阻止您编写利用较少索引的查询。更好的设计是使用唯一元素名称替换 清单 1 中的非唯一元素名称,如 清单 5 所示。
清单 5. 使用唯一元素名称替换清单 1 中的非唯一元素名称
//teacher/name => //teacher/teacher-name //teacher/name/first => //teacher/teacher-name/teacher-first //teacher/name/last => //teach/teacher-name/teacher-last //student/name => //student/student-name //student/name/first => //student/student-name/student-first //student/name/last => //student/student-name/student-last |
支持面搜索结果
面搜索的目标是显示一些链接,允许用户沿各种轴快速直观地缩小搜索的范围。在一个支持面搜索结果的应用程序中,一个列出数据库中所有教师的查询可能在用户界面中返回类似于 清单 6 中的信息。
清单 6. 面搜索
Tabor, Gavin Nance, Jamey Haas, Carlene Davies, Yesenia Singer, Lupe
Narrow your search:
School
Lusher Elementary School (35) Academy of the Sacred Heart (34) Isidore Newman School (32) Audubon Charter School (28) Benjamin Franklin Elementary Math-Science Magnet (25)
Grades
9 (5) 10 (6) 11 (6) 12 (6) |
清单 6 提供了两个面:School 和 Grades。每个面包含 4 到 5 个值,这些值链接到一个搜索,该搜索用于缩小最近的搜索的范围。每个面值旁边有一个数字(位于圆括号中),表示单击这个链接将会找到的教师总数。面搜索结果通常只显示每个面的几个可能值。如果一个面的确切值的数量很少,比如说 Grades 面,那么应用程序通常会显示所有面,并按照各个面的重要程度排序。但是,如果一个面包含许多可能值,那么应用程序通常只显示将返回最多结果的那些值,并根据结果数量按降序排列。
一些原生 XML 数据库正在包含对面搜索的支持,但是它们需要特殊的索引才能提供最佳性能。随着数据库中的记录数量的增加和一个面的可能值的数量增加,获取一个面的显示值的典型 XQuery 算法很快成为一个瓶颈。对于一个拥有多个包含数千个值的面的大型数据库来说,这样的算法是行不通的。要发挥面搜索的威力,原生 XML 引擎需要能够从一个元素在数据库中具有的值构建词典。这些词典可以从特殊的索引实现,而索引又需要唯一元素名称。
如果您拥有一个相对较小的不支持面搜索的原生 XML 数据库,并且需要自己编写代码来支持这种功能,那么您将明白,唯一元素名称对您的代码有多么重要,就跟它们对更高级的数据库中当前存在的面搜索支持代码一样重要。
预先计算值
将冗余数据添加到 XML 文档的想法对于老练的关系数据库管理员来说是不可思议的。但是,当您的主要关注点是性能时(例如,当您必须为查询几千万条记录的查询返回面搜索结果时),基于 XML 文档中的数据预先计算一些值并将结果添加到 XML 文档中有助于极大地改善响应时间。原生 XML 数据库都是以牺牲存储空间并容忍冗余为代价来换取性能的。
假定您拥有一些图像元数据 XML 文档。每个 XML 文档都有一个或多个以下元素:camera、device 和 scanner,这些元素都包含关于创建图像的设备的信息。device 元素表示一个复杂节点,它包含一个带有设备名称的元素和其他几个带有额外信息的元素。在本例中,所有这些 device 元素都需要在应用程序的其他部分使用,因此不能丢弃。这个应用程序实现面搜索并调用一个名为 scanning device 的面,该面显示创建图像的设备的名称。
类似地,这些图像元数据文档还具有高度和宽度元素,但是应用程序调用一个名为 size 的面,这个面可以从 height 和 width 元素轻松得到。
清单 7 是一个示例。
清单 7. 第一个图像元数据文档示例
<image> <id>123456789</id> <date>2009-11-16T03:14:42</date> <description>Eiffel Tower</description> <device> <device-name>Scanmelter 2000</device-name> <device-resolution>300dpi</device-resolution> <device-manufacturer>Scanners Inc.</device-manufacturer> <service-tag>ASDFQWER</service-tag> </device> <width>1200</width> <height>1024</height> </image> |
清单 8 展示了第二个示例。
清单 8. 第二个图像元数据文档示例
<image> <id>123456788</id> <date>2009-11-16T03:14:42</date> <description>Empire State Building</description> <scanner>Pixel Maker LS</scanner> <width>800</width> <height>600</height> </image> |
现在假设一个数据库具有足够的记录,查询拍摄于 2009-11-16 的图像将返回 5,000 个图像。应用程序显示了其中 30 个图像。搜索结果视图显示各个面,包括 scanning device 和 size,每个面提供一个短的值列表。scanning device 面值包括 Scanmelter 2000 (1202) 和 Pixel Maker LS (207)。size 面值包括 1200x1024 (2302) 和 800x600 (113)。
想想您要编写什么代码才能满足上述要求。这个代码编写起来非常容易,但是伸缩性不好,因为它必须完成大量的工作来计算满足每个面值表示的查询的记录数。面值可能有数百个,代码需要计算每个面值的结果数量,以便确定为该面列出哪 5 个面值。随着数据库中的记录数量、应用程序显示的面数量以及每个面可能的面值数量的增加,情况会迅速变得更糟。如果应用程序需要显示 50 个面并处理数百万条记录,那么您别无选择,只能预先计算面值并将其包含在记录中。
清单 7 和 清单 8 中的 XML 文档都将包含两个新元素:scanner-name 和 size。这个简单的更改将允许这个实现具有更好的伸缩性。
通过查询转换数据
XQuery 的最大优势在于能够严格按照调用者的需求提供数据,但这个优势可能利用得最不好。通常,架构师倾向于将原生 XML 数据库当作一个后端 XML Web 服务,它返回前端应该根据需要进行转换和呈现的 XML 文档。
已经使用 XQuery 从原生 XML 数据库检索数据的公司,很容易说出返回 XML 格式的数据、然后使用(例如)XSLT 在前端转换数据的所有原因。以下是一些最常见的原因:
我们计划创建其他产品,它们将相同的数据用于其他目的。
我们有前端员工已经了解如何使用 XSLT、Perl、PHP、JavaScript 和 Java 语言。
我们想要一个面向服务的架构(Service-Oriented Architecture)。
我们都不了解 XQuery,因此我们想尽量限制它的使用。
们有一个现成的数据管道。
XQuery 看起来很复杂。
XQuery 不能做什么什么。
这里没有足够的篇幅来一一驳斥这些理由,但请记住以下几点:
准备呈现给浏览器的数据通常比数据库存储的原始数据小得多。
无论根据何种标准,XQuery 都比 XSLT 更简单、更强大、更精简。
在输出时转换数据比稍后使用 XSLT(或其他任何工具)转换数据要快得多。
您可以编写 XQuery 以便客户端能够请求各种格式的数据。
如果转换数据的代码接近定位数据的代码且二者使用相同的语言编写,那么应用程序的常规复杂性将极大地降低。 上述第一点 — 对比要呈现的数据的大小和原始的、未转换的数据的大小 — 需要额外说一下。注意大型 XML 记录。尽量避免发送大型记录到前端并在那里转换。将尽可能小的数据块放到网络上通常能够改善应用程序的响应性和伸缩性。应该经常这样做:在数据从数据库出来的过程中转换数据时,将较小的数据块放到网络上。
对于那些未能利用 XQuery 的全部威力的公司,真正的原因是害怕未知事物。这一点完全可以理解,但如果您已经在使用原生 XML 数据库了,那么发挥它的全部能力最终只会使事情变得更好。
剖析 XQuery 代码
一般来说,剖析代码意味着确定计算机在代码的每一部分所花费的时间。目的是识别代码中最容易取得优化效果的部分。这些部分并不一定是运行得最慢的;有时,一段已经十分高效的代码可能将是您最想关注的部分。例如,一段运行时间为 10 秒的代码很容易优化为在 1 秒内运行。但是,如果那段代码每天只运行一次,那么您最好将精力放在改进每天运行 1,000,000 次的函数速度上,尽管提高的幅度可能很微小。
大多数原生 XML 数据库拥有一些工具来对代码进行基准测试或剖析。使用这些工具吧。有时,这些工具不会以您想要的方式测量某些特定代码段的性能。如果遇到这种情况,应该毫不犹豫地创建自己的代码来对进程进行基准测试。在自己的 XQuery 模块中插入代码来标记和测量时间不会产生任何问题,尤其是您可以在生产中禁用基准测试代码时。
此外,由于 XQuery 是一种函数型编程语言,所以每个函数都是独立的。在很大程度上,一个 XQuery 函数的返回序列只取决于调用该函数时使用的参数。因此,开发单元测试和性能测试来访问 XQuery 函数比使用标准过程型编程语言(如 Java、Python、Perl、C 和 PHP)来开发函数测试更容易。可以用外部进程(比如一个快速脚本)轻松地测量 XQuery 代码中函数的执行时间。例如,一个精巧的 Emacs 脚本允许您运行您正在编辑的 XQuery 代码并测量代码的执行时间,您需要做的只是突出显示相应代码并敲击一个组合键。这个脚本可以将代码发送到服务器,让服务器评估代码,然后将结果返回到一个带有执行时间戳的新缓冲区。
保留优化列表
您应该保留一个您的平台适用的潜在优化列表,并在每次需要改善应用程序的性能时浏览整个列表。除了本文已经介绍过的优化外,我还将以下各项保留在我的列表中。
尽可能预先编译代码
一些原生 XML 数据库具有预先编译或解析 XQuery 代码的能力。对于在服务器上频繁运行的代码,如果您能够确保服务器在每次遇到该代码时都不必解析或编译它,那么会看到明显的性能改善。
针对索引编写代码
在很多情况下,原生 XML 数据库都能够计算 XQuery 或 XPath 表达式并从索引直接处理查询,无需检索任何文档。如果可能,您应该尽量编写这种类型的查询。寻找一个数据库选项或搜索函数选项,以允许您的查询直接从索引检索结果并避免任何种类的结果有效性过滤。
从索引检索结果并且不进行过滤也有其缺点。如果您在搜索的节点不是顶级节点(或片段根),那么您必须小心。做好理解结果的准备,因为结果中可能包含经过过滤的搜索结果中不包含的节点。
考虑 XQuery 扩展
大多数原生 XML 数据库提供用于快速运行的 XQuery 扩展。当您偏离严格的 XQuery 时,您的应用程序将更加依赖于某个特定的产品,但在实践和生产中,当处理大量数据时,您肯定想考虑性能扩展的优势。轻便是有代价的。
理解森林、树木和合并
一些原生 XML 数据库将它们的数据以二进制文件格式保存在森林(目录)中,森林又包含树木(文件)。新记录通常进入新树木中。系统(或管理员)定期合并树木以提高性能 — 森林中的树木越少,查询响应时间就越好。但您不会希望森林变得太大。优化系统时,应该在开始(或配置系统以开始)合并之前检查森林的最优大小和树木的最优数量。
因此,数据加载将触发合并。合并可能会降低接近极限吞吐量的系统的性能。如果可能,应该在系统负载最低时安排数据加载和合并。与数据合并相比,数据加载对性能的影响要小得多。如果数据加载要运行很长一段时间,那么考虑在加载峰值期间禁用数据合并。
结束语
原生 XML 数据库目前驱动的站点支持对包含数千万条记录的数据库进行复杂的搜索。对于某些应用程序而言,在适当的情况下,这些数据库能够让使用它们的公司相对于较慢的采用者,拥有更大的竞争优势。但正像其他数据库技术一样,原生 XML 数据库将让那些知道如何优化系统以提高效率和响应性的公司获得最大的收益。
后注:
常用缩写词:
RAM:随机存取存储器
RSS:真正简单聚合
XML:可扩展标记语言
XSLT:可扩展样式表语言转换 |