原生 XML 数据存储和检索
原生 XML 数据库中的设计和实现权衡对使用它的应用程序的性能、可扩展性和可用功能产生重大影响。本文重点关注存储的 XML 文档的粒度和索引,这是两个最关键的设计考虑因素。本文的讨论基于 Sleepycat Software 的 Berkeley DB XML (www.sleepycat.com/products/xml.shtml)。
XML 数据库的基本功能是存储文档、查询文档和处理查询结果。当然,需要索引才能获得可接受的查询性能。
在关系数据库中,关系表的片段被存储,查询是 SQL,结果是表格形式的。这种抽象和标准化从应用程序开发人员的角度来看是有用的。开发人员对文档的存储和索引方式以及查询如何利用存储格式、索引和查询语言的组合来快速回答问题不太了解。
相同的概念存在于原生 XML 数据库中,例如 Berkeley DB XML。在这种情况下,数据是 XML 文档,查询可以是 XPath 或 XQuery 表达式。结果可以是 XML 文档、DOM、SAX 或专有形式。在原生 XML 数据库中,存储、索引和查询的机制从应用程序开发人员的角度来看并不明显,但它们对于整个系统的功能、性能和可扩展性至关重要。
原生 XML 数据库公开了存储和检索 XML 文档的逻辑模型;但是,其内部存储模型可能与文档不相同。索引是任何数据库的关键组成部分。如果没有智能索引,数据库在信息检索方面也好不到哪里去。查询处理建立在存储格式和索引之上,但超出了本文的范围。
大多数原生 XML 数据库都面向存储 XML 文档,其中一个关键问题是文档存储的粒度。在数据库术语中,粒度可以用几种不同的方式描述:外部访问、内部可寻址性和并发性。
访问粒度和可寻址性之间存在区别。可寻址性是指可以在系统中命名和直接访问的对象,而无需导航。可以通过 DOM 访问具有 XML 文档可寻址粒度的系统,方法是解析文档。从这个意义上讲,访问粒度是用户可见的,而可寻址性是一个内部概念。并发性是指对象如何并发修改,如果支持此功能。
在如何存储文档方面,有两个主要选择——完整或不完整。完整存储 XML 文档的系统通常会解析 XML,以确保其格式良好且有效,但在其他方面会保持文档不变。这对于需要检索整个字节对字节文档或进行往返的应用程序非常有用。此外,对于倾向于整体检索和处理的相对较小的文档,这样的系统是理想的。完整文档存储的主要问题是如何在文档集合中寻址目标文档。有两种主要的机制可以做到这一点:唯一标识符,例如名称或文档 ID,或查询表达式,例如 XQuery。前者正好产生一个文档,而后者可能会在结果集中返回许多文档。
对于大型集合,必须可以在查询中定位一小部分结果文档。对于完整文档存储,这意味着需要索引机制。如果在将文档插入集合时对其进行了解析,则也可以根据系统的索引规范对其进行索引。这种系统中的索引使用文档粒度寻址。最好避免解析文档以解决查询。如果可以从索引中明确回答查询,并且应用程序所需的访问粒度在文档级别,而不是 DOM 粒度访问级别,则可以避免额外的解析。
完整文档存储的一个明显缺点是,对于某些应用程序和查询,处理请求可能需要很长时间和大量内存。这主要是由于需要解析文档以满足查询。但是,对于只读文档,可以进行优化,例如引用文档内的偏移量。
完整文档存储的优点包括其简单性和字节对字节的往返。Berkeley DB XML 具有完整存储文档的选项。
一些原生 XML 数据库(例如 Berkeley DB XML)存储文档的粒度比文档更细。此类系统的属性包括:可寻址性为子文档级别,访问粒度为子文档级别,并发粒度可能但不一定比文档级别更细。
将文档分片存储具有许多优点,包括
能够直接引用文档中的元素或其他对象。
能够检索部分文档而无需解析。
通过仅物化评估查询所需的文档部分,无需解析即可进行高效查询。
能够修改大型文档的一小部分。
将文档分片存储的决定导致更多选择
支持的往返程度(如果有)。
存储的信息或存储的数据模型。
可寻址性的粒度。
支持部分文档修改,而无需重写整个文档。
信息的物理格式。
如果需要能够按字节返回原始文档,则细粒度文档存储系统必须选择支持的往返程度。实际上,对文档进行任何分解以进行存储都会导致信息丢失或更改,例如属性的重新排序或 XML 声明的更改。这是因为 XML 信息集与文档中的字节之间没有 1:1 的映射。也就是说,XML 文档中存在一些字节,这些字节被认为与信息集无关,因此甚至可能不会被解析器传递。
为了支持往返,细粒度文档存储系统必须跟踪在解析期间扩展的实体引用,以及可忽略的空白和命名空间前缀映射。就查询和检索部分文档而言,这些机制并不重要,但对于某些应用程序,它们对于文档序列化至关重要。由于往返程度意味着额外的成本,因此某些系统导出配置选项以确定如何处理这些问题。
完整文档存储具有巨大的简化优势,因为它不必关心其存储的 XML 文档的数据模型。细粒度文档存储必须决定数据模型,这与查询处理和查询语言支持密切相关。例如,XQuery 的数据模型是类型化的,类型信息可以出现在 XQuery 表达式中。但是,XPath 1.0 表达式不是富类型化的,因此不需要额外的类型信息。
数据模型问题的一个简单示例是 DOM 与 XQuery。DOM 相对简单。在 DOM 中,几乎每个对象都是一个节点,有些节点有名称,有些节点有值,有些节点有子节点和兄弟节点。DOM 本质上是一棵语义信息很少的树,几乎所有的信息都包含在 XML 文档本身中。相反,XQuery 数据模型是类型化的。XQuery 确实支持简单的、格式良好的 XML;但是,它也支持类型信息,这些类型信息是从经过模式验证的文档中获得的,其中模式信息来自文档外部。
可以选择等效于 XML 信息集或 DOM 的存储数据模型,但这样 XPath 2.0 和 XQuery 1.0 的强大类型功能就无法完全使用。经过模式验证的文档在解析和验证时具有可用的类型信息。解析、验证和查询同时发生的系统在获取类型信息以满足查询方面没有问题。但是,在细粒度存储系统中,解析和查询事件是不相关的。这意味着在查询时,如果类型信息要用于查询,则必须找到类型信息。系统可以采用几种方法来实现类型
将类型信息与每个文档和类型化对象一起存储,并在查询时将其物化。
存储对相关模式文件的引用,并在查询时重新加载(解析)它们。
将每种类型映射到 XML Schema 建议中最接近的原子类型并存储该信息。
完全不支持类型信息,这限制了查询并迫使它们使用自己的复杂类型定义。
可寻址性的粒度与数据模型密切相关。一个极端是选择 DOM 对象作为可寻址单元。这意味着每个 DOM 节点,无论是文档、元素还是属性值,都是可寻址且单独存储的对象。虽然很简单,但这种方法在内存、磁盘空间和 CPU 方面非常昂贵。还有其他更粗粒度的解决方案。一种是使用元素作为可寻址单元,并将其属性和子文本节点关联起来。另一种是寻址元素和文本节点,并将属性与元素关联起来。如果元素及其属性和文本节点可能一起被引用,则前者可能更适合引用的局部性。
将文档存储为细粒度节点的原生 XML 数据库必须为可寻址单元分配可寻址节点标识符(节点 ID)。节点 ID 用于在处理期间检索特定节点。在物理存储方面,大小很重要。较小的节点和节点 ID 意味着更好的引用局部性和更少的磁盘访问来读取和写入数据。
Berkeley DB XML 将节点存储在 B 树中,其中节点 ID 按文档顺序分配,这也是 B 树上的迭代顺序。这意味着一旦找到节点,就可以通过迭代而不是额外的查找操作来进行序列化或子节点导航。
使用适当的排序/比较函数,作为 B 树键的节点 ID 可以采用多种物理形式。它可以像整数一样简单,也可以是复杂的数组或字符串。节点编号是原生 XML 数据库中更有趣和重要的设计选择之一。有些节点编号方案能够允许插入和删除任意节点而无需重新编号,并允许仅基于节点编号和索引执行与查询相关的操作,从而消除节点查找。
Berkeley DB XML 使用一种编号方案,该方案允许进行一些直接关系比较,并尝试最大限度地减少物化节点以进行导航的需求。该方案还避免在部分修改文档时重新编号。
细粒度存储的一个优点是能够修改文档的某些部分而无需触及其余部分。这种“外科手术式”更改在性能和可扩展性方面具有显着优势;但是,有效地执行它可能很困难。许多系统不支持部分修改文档,如果支持,也只是通过明确定义的接口(例如 XUpdate),而不是直接的 DOM 操作。
部分修改可能会使文档无效,或者更糟,格式错误。但是,重新解析以进行验证会抵消部分修改的大部分好处。插入或删除可寻址对象(例如元素)会影响系统的节点编号方案,如上所述。索引也会受到影响,必须更新。数据库可以选择在修改后重新验证或解析,或者允许应用程序显式请求它。
细粒度文档存储在序列化整个文档方面存在缺点。在这种情况下,迭代器必须遍历文档的可寻址部分。如果这是一个常见的操作,则可能值得优化或缓存序列化的文档以供重用,这会产生可能的并发问题。可以通过按文档顺序维护可寻址单元、在存储节点中保留名称而不是名称 ID 以及使用更粗的粒度来优化文档序列化,这会导致从磁盘检索的对象更少。
正确指定和使用索引可以将查询速度提高几个数量级。但是,索引会占用磁盘和缓存中的空间——这是经典的空间与速度之间的权衡。在某些情况下,索引的存在会减慢操作速度。当频繁更新索引数据时,重新索引所花费的时间可能会抵消索引访问的好处。
用于查询 XML 的数据模型意味着几乎所有索引都处理元素、属性及其各自的文本内容,以及由其值字符串表示的可能数据类型。但是,对于如何指定索引,甚至索引什么以及如何索引,都没有标准或约定。不同的 XML 数据库在这些领域的索引方面做出了不同的选择
索引类型——结构、值、全文。
索引范围——文档、集合。
索引目标——文档、节点。
索引控制——自动、自愿、必需。
结构索引用于跟踪结构和路径信息,例如“跟踪路径为 /a/b/c 的所有元素节点的存在”或“跟踪到节点 c 的所有路径”。此类索引对于查询的导航部分很有用。一些索引将结果集缩小为一组较小的可能结果,而不是给出单个明确的结果。例如,上面跟踪所有路径 /a/b/c 的索引可以肯定地回答查询 /a/b/c。跟踪到 c 的所有路径的索引不能是明确的,因为它还包含路径(例如 /e/f/c)的条目。
值索引用于跟踪特定元素或属性的所有值。元素“color”上的值索引将为颜色的每个单独实例创建一个索引条目,并且对于如下查询非常有用//color[.='green']。此外,值索引可以是类型化的,以便可以正确执行比较。XPath 2.0 和 XQuery 1.0 的类型化数据模型带来了 XML Schema 建议中的一长串潜在数据类型,例如 xs:date、xs:time 和各种数字格式。对类型化索引的支持允许应用程序直接使用它们,而不是修改其内容以将例如 xs:datetime 映射到整数,以便可以使用基于范围的比较。
全文索引本身就是一个很大的主题。XQuery 的全文扩展有一个工作草案,但尚未普遍使用。一些原生 XML 数据库产品实现了他们所谓的全文索引,这至少是文档上的单词索引。由于没有标准,全文索引需要专有的查询语言或扩展作为接口。
大多数原生 XML 数据库将文档存储在集合中。给定索引的范围可以是集合范围,也可以限制为单个文档。原生 XML 数据库系统可以选择其实现的索引范围。针对集合的查询可以返回文档或文档中的节点集。为了支持将查询有效地限制为可管理的文档集,系统必须支持集合范围的索引。这并不意味着也不可能具有文档范围的索引,该索引包含仅适用于给定文档的条目。
与范围相关的是索引条目引用的目标或对象。它可以是文档或文档中的对象。索引能够指向系统中的可寻址单元,但这种粒度并不总是必要的,并且可能很昂贵。由于在以细粒度存储的文档中进行导航操作不如用于完整文档存储的操作昂贵(由于解析),因此返回文档元素以进行进一步导航就足够了。尽管这是可能的,但大多数具有细粒度文档存储的数据库系统都直接引用索引中的节点,而不是引用包含文档。
索引类型的另一个维度是如何指定索引。自愿索引由系统的接口显式指定。这些索引允许进行一些实验,以找到最小的有用索引集。某些系统具有自动索引,其中始终会创建一组定义明确的索引,除非通过配置或接口显式禁用这些索引。系统也可能具有必需索引,这些索引无法禁用,因为它们对于系统的正常运行是必需的。
George Feinberg 是 Sleepycat Software 的 Berkeley DB XML 的架构师。在此之前,Feinberg 是 eXcelon 原生 XML 数据库的架构师之一,该数据库现在称为 XML Information Server (XIS),由 Progress Software 拥有。他是 eXcelon 在 W3C 和 XML Schema 工作组的代表。Feinberg 之前的经验包括在 Open Software Foundation(现在的 The Open Group)、Hewlett-Packard 和一家存储系统初创公司担任操作系统设计师和开发人员。