内容


利用 XML 索引提升 DB2 9 中的 XML 查询性能

Comments

简介

DB2 9 提供了 pureXML 存储并以 XML 索引、XQuery 和 SQL/XML 作为查询语言、XML 模式支持和 XML 扩展,从而改进 Import/Export 和 Runstats 等实用程序。正如在关系查询中一样,索引对于高性能的 XQuery 和 SQL/XML 是至关重要的。DB2 允许在 XML 列上定义特定于路径的 XML 索引。这意味着可以使用它们来索引频繁在谓词和连接中使用的所选择的元素和属性。例如,使用 图 1 中的示例数据,可以根据 author ID 使用下面的索引 idx1 对表 books 的 XML 列 bookinfo 中的所有文档进行查找和连接。

create table books(bookinfo XML);

create index idx1 on books(bookinfo) generate keys using xmlpattern '/book/authors/author/@id' as sql double;
图 1. 以文本(连续)格式和解析(分层)格式表示的示例 XML 文档
以文本(连续)格式和解析(分层)格式表示的示例 XML 文档
以文本(连续)格式和解析(分层)格式表示的示例 XML 文档

由于 DB2 不强制要求将单个 XML 模式与 XML 列中的所有文档相关联,所以特定元素和属性的数据类型事先 是未知的。因此,要求为每个 XML 索引指定一个目标类型。稍后,您将在本文了解到为什么类型很重要。可以使用的 XML 索引数据类型如下:

  • VARCHAR(n):用于带有字符串值的节点,已知字符串值的最大长度为 n
  • VARCHAR HASHED:用于带有字符串值的节点,字符串值的长度为任意长度。该索引包含实际字符串的散列值,只能用于等式谓词,不能用于范围谓词。
  • DOUBLE:用于带有任意数值类型的节点。
  • DATE 和 TIMESTAMP:用于带有日期或时间戳值的节点。

VARCHAR(n) 索引的长度是一个强制约束。如果插入一个文档,其中索引元素或属性的值超过了最大长度 n,则插入操作将失败。同样,如果索引元素或属性的值大于 n,则 VARCHAR(n) 索引的 create index 语句将失败。

默认情况下,DOUBLE、DATE 或 TIMESTAMP 索引不是强制约束。例如,将 author ID 属性上的索引 idx1 定义为 DOUBLE,是希望这些 ID 为数值。如果插入一个文档,其中 author ID 的值为 “MN127”,它是非数型值的,则虽然仍将插入该文档,但不会将 “MN127” 值添加到索引。这是因为 DOUBLE 索引只能计算数值谓词,然而 “MN127” 值永远不会匹配一个数值搜索条件。因此,索引中没有该值是正确的。

从 DB2 9.5 往后,您就可以向 XML 索引定义添加可选的 REJECT INVALID VALUES 子句。这个子句强制将 DOUBLE、DATE 或 TIMESTAMP 类型的索引作为强制约束。如果您定义一个索引,但包含 author ID 值为 “MN127” 的文档无法插入,那么您在创建该索引时就不能将该文档包含在 XML 列中。

create index idx1 on books(bookinfo) 
generate keys using xmlpattern '/book/authors/author/@id' 
as sql double REJECT INVALID VALUES;

您可以在 “DB2 pureXML Cookbook” 中找到有关定义 XML 索引的更多详细内容。下面在讨论 XML 索引的使用时,也假定您熟悉查询 DB2 中 XML 数据的基本概念。有关更多信息,请参考以前的文章:“用 SQL 查询 DB2 XML 数据”(developerWorks,2006 年 3 月)和 “使用 XQuery 查询 DB2 XML 数据”(developerWorks,2006 年 4 月)给出了介绍,“DB2 9 中的 pureXML:怎样查询您的 XML 数据?”(developerWorks,2006 年 6 月)给出了更多示例和详细内容。

适合于 XQuery 和 SQL/XML 语句的 XML 索引

正如在关系查询中一样,索引对于高性能的 XQuery 和 SQL/XML 语句是至关重要的。当应用程序向 DB2 提交关系查询或 XML 查询时,查询编译器将比较查询谓词和现有的索引定义,然后确定是否存在可用索引用于执行查询。该过程被称为 “索引匹配”,并且为给定的查询生成一组合适的索引(可能是空集)。将该组索引输入到基于成本的优化器,这个优化器决定是否使用任何合适的索引。本文专注于索引匹配,而不是优化器的索引选择。在优化器决策方面,除了运行 “runstats” 来为优化器提供关于数据的准确的统计之外,所能做的事情不是很多。但您可以采取许多其他措施确保索引匹配。

在关系查询中,索引匹配通常是微不足道的。DB2 可以使用定义在单个关系列上的索引来响应此列上的任何等式谓词或范围谓词。但是,对于 XML 列,这将更加复杂。关系列上的索引包含了此列的所有值,而 XML 索引仅包含那些同时匹配 XML 模式 索引定义中的数据类型的节点值。因此,仅当 XML 索引拥有 “正确的” 数据类型并且 至少包含满足谓词的所有 XML 节点时,该 XML 索引才能用于计算 XML 查询谓词。对于 XML 索引的合格性,有两个主要要求:

  1. XML 索引定义的限制等同于或低于查询谓词的限制(“容纳”)。
  2. 索引的数据类型与查询谓词的数据类型相匹配。

本文说明了如何设计 XML 索引和查询以确保符合上述要求,以及如何避免常见错误。先从了解查询执行计划开始。DB2 中现有的解释工具(例如 Visual Explain 和 db2exfmt)可用于查看 XQuery 和 SQL/XML 的查询执行计划,正像它们在传统 SQL 中的作用一样。

XML 查询计算:执行计划和新操作符

为了执行 XML 查询,DB2 9 引入了三个新的内部查询操作符,名为 XSCAN、XISCAN 和 XANDOR。这些新操作符和现有的查询操作符(例如 TBSCAN、FETCH 和 SORT)允许 DB2 生成 SQL/XML 和 XQueries 的执行计划。现在看一下这三个新操作符,以及它们与 XML 索引在执行计划中是如何工作的。

XSCAN(XML 文档扫描)
DB2 使用 XSCAN 操作符来遍历 XML 文档树,如需要,还将计算谓词和提取文档片断和值。XSCAN 不是 “XML 表扫描”,但在表扫描之后,它可以出现在执行计划中,用来处理每个文档。

XISCAN(XML 索引扫描)
类似于现有的用于关系索引的关系索引扫描操作符 (IXSCAN),XISCAN 操作符在 XML 索引上执行查找或扫描。XISCAN 使用值谓词作为输入,例如类似于 /book[price = 29]where $i/book/price = 29 的路径值对。它将返回一组行 ID 和节点 ID。行 ID 用来识别包含合格文档的行,而节点 ID 用来识别这些文档中的合格节点。

XANDOR(连接 XML 索引)
XANDOR 操作符通过操作多个 XISCAN,来同时计算两个或多个等式谓词。它将返回那些满足所有谓词的文档的行 ID。

下面看一个示例查询(与 XQuery 和 SQL/XML 等效),分别了解不带索引、带一个索引和带多个索引的执行计划:

-- XQuery: 
xquery for $i in db2-fn:xmlcolumn("BOOKS.BOOKINFO")
where $i/book/title = "Database systems" and $i/book/price = 29
return $i/book/authors;

-- SQL/XML: select XMLQUERY('$i/book/authors' passing bookinfo as "i") from books where XMLEXISTS('$i/book[title = "Database systems" and price = 29]' passing bookinfo as "i");
-- Two Indexes: create index idx1 on books(bookinfo) generate keys using xmlpattern '/book/title' as sql varchar(50);
create index idx2 on books(bookinfo) generate keys using xmlpattern '/book/price' as sql double;

在图 2 中可以看到该查询的不同执行计划(简化了 db2exfmt 的输出)。因为执行计划中的逻辑流是自下而上、从左到右,所以看此类执行计划时,最好是从树中左下方的操作符开始。

如果该查询中没有合适的索引用于谓词,则使用最左边的计划 (a)。表扫描操作符 (TBSCAN) 将读取表 “BOOKS” 中的所有 行。对于每一行,嵌套循环连接 (NLJOIN) 操作符把指向相应的 XML 文档的指针传递给 XSCAN 操作符。同样地,NLJOIN 并没有充当拥有两个输入的标准连接,而是协助 XSCAN 操作符来访问 XML 数据。XSCAN 操作符将遍历每个文档、计算谓词,如果满足谓词,则提取 “authors” 元素。RETURN 操作符将完成查询执行,并将查型结果返回到 API。

图 2. 三个执行计划:(a) 没有索引、(b) 一个索引、(c) 两个索引
三个执行计划:(a) 没有索引、(b) 一个索引、(c) 两个索引
三个执行计划:(a) 没有索引、(b) 一个索引、(c) 两个索引

如果有一个索引用于一个或两个谓词,例如 /book/price 上的索引 idx1,则将看到类似于图 2 中计划 (b) 的执行计划。XISCAN 使用路径值对(/book/price, 29) 来检查索引,并返回其中价格为 29 的文档的行 ID。对这些行 ID 进行分类,以便删除相同项(如果有),并优化表的后续 I/O。然后行 ID 扫描 (RIDSCN) 操作符将扫描这些行,触发行预取,并将行 ID 传递到 FETCH 操作符。对于每一个行 ID,FETCH 操作符将读取表中相应的行。该计划的好处在于仅对表中的一小部分行进行检索,即仅对 “price” 为 29 的行进行检索。这远远低于全部 表扫描(即读取每行)的开销。对于所获取的每一行,XSCAN 操作符将处理相应的 XML 文档。它将在 “title” 上计算谓词,如果满足谓词,则提取 “authors” 元素。可能存在这样一些文档,其中第二个谓词不为真,那么 XSCAN 仍将执行一些操作来排除这些文档。因此,如果第二个谓词也用索引来替代,将会获得更好的查询性能。

如果有用于两个谓词的索引,则可以参看图 2 中的计划 (c)。该计划使用两个 XISCAN,分别用于每一个谓词和索引。XANDOR 操作符使用这些 XISCAN 来轮流检查两个索引,以便有效地找到同时匹配两个 谓词的文档的行 ID。FETCH 操作符仅对这些行进行检索,因此将表的 I/O 减到最少。随后对于每一个文档,XSCAN 将提取 “authors” 元素。如果谓词在路径中包括了 // 或 *,或者使用了范围比较(例如 < 和 >),则用索引 AND'ing (IXAND) 操作符代替 XANDOR。从逻辑上讲,这两个操作符执行了相同的操作,只不过是用于不同类型的谓词且使用不同的优化方法。

优化器可以决定不使用索引,即使该索引是可以使用的。例如,如果第二个索引没有有效减少表中需要检索的行数,例如访问索引所需的开销比节约表的 I/O 更重要,则优化器可能选择计划 (b) 而不是计划 (c)。然而,需要确保优化器考虑了所有合适的索引,从而以最低的开销和最短的执行时间来选择计划。换句话说,要遵守 XML 索引合格性的两个要求:

  • XML 索引至少包含了满足谓词的所有 XML 节点。
  • 查询谓词中的数据类型与索引定义是兼容的。

XML 索引和查询谓词中的通配符

通配符 // 和 * 可以影响索引和查询谓词之间的包含关系。这是因为路径表达式是不同的,例如 /book/price//price。路径 /book/price 识别了所有 price 元素,它们是元素 “book” 的直接子元素。而路径 //price 识别了 XML 文档的所有层次上的 price 元素。因此 /book/price 识别的元素是 //price 所指定的元素的子集。即 //price “包含” /book/price,但反之则不行。

现在,看一下通配符是如何影响索引的合格性的。以下面的查询为例。表 1 中演示了其 where 语句的四种变化。

XQUERY
for $i in db2-fn:xmlcolumn("BOOKS.BOOKINFO")
where $i/book/price = 29
return $i/book/authors

表 1 最右侧的两列表示两个可选择的索引定义,表中各行展示了谓词可以 (+) 或不可以 (-) 由这两个索引进行计算。下面浏览一下表 1 中的行,来研究每个谓词的索引合格性。

对于第一个谓词,因为它仅包含了 “book” 的直接子元素 “price”,所以 /book/price 上的索引是不合格的。索引没有包含更深层次的 “price” 元素,表中可能存在该元素且可能与谓词路径 $i//price 相匹配。因此,如果 DB2 在 /book/price 价格上使用索引,则可能返回不完全的结果。第二个索引 //price 是合格的,因为它包含了文档各层次的所有 price 元素,正如谓词所要求的。

第三个谓词使用了星号 (*) 作为通配符,这样它将查找 “book” 下值为 29 的任何 子元素。不仅是 “price” 元素可以满足该谓词。例如,元素 /book/title 的值为 29 的文档就是一个有效匹配。但是表 1 中的两个索引都不包括 title 元素。由于 DB2 可能为该谓词返回不完全的结果,因此两个索引均不能使用。

表 1. 索引合格性与 XML 索引和谓词中的通配符
#谓词/索引定义...using xmlpattern '/book/price' as sql double;...using xmlpattern '//price' as sql double;
1当 $i//price = 29 时-+
2当 $i/book/price = 29 时++
3where $i/book/* = 29--
4where $i/*/price = 29-+

第四个谓词 $i/*/price = 29 将查找任何根元素下的 price 元素,而不仅仅是在 “book” 元素下的。如果存在具有路径 /journal/price 的文档,则可能会满足谓词 $i/*/price = 29,但不会被包括在 /book/price 上的索引中。由于 DB2 可能将面临返回不完全查询结果的风险,因此不能使用该索引。但是 //price 上的索引包含任何 price 元素,与根元素无关。

简而言之,DB2 查询编译器必须始终能够检验索引的限制等同于或低于谓词的限制,以便包含谓词正在查找的所有内容。

应意识到,在索引定义中使用通配符可能 会不经意地索引更多节点(多于所需节点)。只要有可能,建议使用索引定义和查询中所需元素或属性的准确路径,而不使用通配符。诸如 //*//text() 这类很普通的 XML 索引模式是可以接受的,但是应慎重使用。//* 上的索引甚至会索引非末端元素,非末端元素通常是没有用的且很容易超出 Varchar(n) 索引的长度限制。

XML 索引和查询谓词中的名称空间

需要注意 XML 索引合格性是否包含了名称空间。首先,如果表的 XML 文档包含了名称空间,则索引定义必须考虑名称空间。这再次涉及到索引/谓词的容纳性。以下面的 XML 文档和索引定义为例:

<bk:book xmlns:bk="http://mybooks.org">
   <bk:title>Database Systems</bk:title>
   <bk:price>29</bk:price>
</bk:book>
CREATE INDEX idx3 ON books(bookinfo)
GENERATE KEYS USING XMLPATTERN '/book/price' AS SQL DOUBLE;

因为将索引 idx3 定义为 /book/price 元素的索引,具有空的名称空间,所以该索引未包含此示例文档的任何索引条目。但是,以下任一索引定义都可适当地用于索引 price 元素:

CREATE INDEX idx4 ON books(bookinfo) GENERATE KEYS USING XMLPATTERN 
'declare namespace bk="http://mybooks.org"; /bk:book/bk:price' AS SQL DOUBLE

CREATE INDEX idx5 ON books(bookinfo) GENERATE KEYS USING XMLPATTERN 
'declare default element namespace "http://mybooks.org"; /book/price' AS SQL DOUBLE

CREATE INDEX idx6 ON books(bookinfo) GENERATE KEYS USING XMLPATTERN 
'/*:book/*:price' AS SQL DOUBLE

索引 idx4 显式地声明了名称空间和前缀以匹配文档。索引 idx5 将名称空间声明为默认名称空间,由于名称空间暗含了前缀,因此在 XML 模式 /book/price 中没有使用前缀。索引 idx6 仅使用了通配符以匹配任何名称空间。用 XQuery 或 SQL/XML 语句表示谓词时,可以使用相同的选项:

查询 4:

-- XQuery:
XQUERY declare namespace bk="http://mybooks.org";
for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")/bk:book
where $b/bk:price < 10
return $b

-- SQL/XML: select bookinfo from books where XMLEXISTS('declare namespace bk="http://mybooks.org"; $b/bk:book[bk:price < 10]' passing bookinfo as "b")

查询 5:

-- XQuery:
XQUERY declare default element namespace "http://mybooks.org";
for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")/book
where $b/price < 10
return $b

-- SQL/XML: select bookinfo from books where XMLEXISTS('declare default element namespace "http://mybooks.org"; $b/book[price < 10]' passing bookinfo as "b")

查询 6:

-- XQuery:
XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")/*:book
where $b/*:price < 10
return $b

-- SQL/XML: select bookinfo from books where XMLEXISTS('$b/*:book[*:price < 10]' passing bookinfo as "b")

表 2 中每一行用于一个查询,每一列分别用于前面定义的索引 idx3idx6。可以先观察一下表 2。首先,不带名称空间的 idx3 不能用于任何考虑了名称空间的查询。其次,可以发现查询 4 和查询 5 对应的行拥有同样的条目,且索引 idx4idx5 对应的列也拥有同样的条目。这是因为显式的名称空间定义和默认的名称空间定义在逻辑上是相同的,仅仅是同一个事物的不同说法。可以使用其中任一个,而不会影响到索引匹配。带有名称空间通配符的索引 idx6 对于所有示例查询都是合格的,它甚至可以用于不带名称空间的谓词,例如 $b/price < 10。索引 idx6 还是与查询 6 中的谓词相匹配的惟一索引。索引 idx4idx5 包含了用于一个特定名称空间的索引条目,但是因为查询 6 是查找任何 名称空间中的书籍价格,所以这两个索引不能用于查询 6。因此,违反了包容性要求。

表 2. 索引合格性与 XML 索引和谓词中的名称空间
#查询/索引定义idx3(没有名称空间)idx4(显式的名称空间)idx5(默认的名称空间)idx6(名称空间通配符)
1查询 4(显式的名称空间)-+++
2查询 5(默认的名称空间)-+++
3查询 6(名称空间通配符)---+

XML 索引和查询谓词中的数据类型

除了索引或谓词与通配符和名称空间的包容性之外,索引合格性的第二个要求是谓词和索引的数据类型必须匹配。在所有上述示例中,/book/price 元素始终作为 DOUBLE 进行索引。但是,也可以将书籍价格作为 VARCHAR 进行索引,如表 3 所示。不过,请注意值谓词也拥有由文字值类型确定的数据类型。双引号中的值始终是字符串,而不带引号的数字值被认为是数字。正如您在表 3 中所看到的,字符串谓词只能由 VARCHAR 类型的 XML 索引进行计算,而数值谓词只能由 DOUBLE 类型的索引进行计算。

关系索引的数据类型始终由索引列的类型来确定。不过,由于 DB2 不强制要求将 XML 模式与 XML 列相关联,所以元素或属性的数据类型不是预先确定的。因此,每个 XML 索引需要一个目标类型。而且类型是很重要的。假定 price 元素拥有值 9。字符串谓词 "9" < "29" 为假,而数值比较 9 < 29 为真。这里强调了如果想从语义上进行正确的数值比较,则应使用 DOUBLE 索引。最好将 “price” 元素作为 DOUBLE 进行索引。

表 3. XML 索引和谓词中的数据类型
#谓词或索引定义...using xmlpattern '/book/price' as sql double; ...using xmlpattern '/book/price' as sql varchar(10);
1当 $i/book/price < "29" 时-+
2当 $i/book/price < 29 时+-

使用适于 XML 连接谓词的索引

在上述示例中,看到了包括文字值的值谓词。这些文字值决定了比较的数据类型。通常此类决定不适用于连接谓词。假定有一个表 “authors”,以 XML 格式表示详细的作者信息,包括出现在 book 数据中的 author ID。现在想使用连接来检索详细的 author 数据,且仅对 books 表中书籍的 authors 进行检索。在 author ID 上定义索引看起来很有用:

create table books (bookinfo xml);
create table authors (authorinfo xml);

create index authorIdx1 on books(bookinfo) generate key using
xmlpattern '/book/authors/author/@id' as sql double;

create index authorIdx2 on authors(authorinfo) generate key using
xmlpattern '/author/@id' as sql double;

XQUERY
for $i in db2-fn:xmlcolumn("BOOKS.BOOKINFO") 
    for $j in db2-fn:xmlcolumn("AUTHORS.AUTHORINFO") 
where $i/book/authors/author/@id = $j/author/@id
return $j;

该查询检索了所需的作者信息,但是没有将索引用于连接处理。请注意,author ID 上的连接谓词没有包含文字值,该文字值将表明比较的数据类型。因此,DB2 必须考虑匹配任何数据类型的 author ID。例如,考虑表 4 中的 book 和 author 数据。Author John Doe 有一个数值 ID 值 (47),而 author Tom Noodle 有一个非数值 ID 值 (TN28)。在其他表中二者都有有效匹配。因此,二者必须包括在连接结果中。但是,如果 DB2 使用数值索引 authorIdx1authorIdx2,则不会找到 author ID “TN28”,并且会返回一个不完全的结果。因此,DB2 不能使用那些索引,而是会采取表扫描来确保正确的查询结果。

表 4. 示例 book 和 author 数据
BookAuthor
<book>
    <authors>
     <author  id="47">John Doe</author>
    </authors>
    <title>Database Systems</title>
    <price>29</price>
</book>
<author id="47">
    <name>John Doe</name>
     <addr>
           <street>555 Bailey Av</street>
           <city>San Jose</city>
           <country>USA</country>
      </addr>
      <phone>4084511234</phone>
</author>
<book>
    <authors>
     <author  id="TN28">Tom Noodle</author>
    </authors>
    <title>International Pasta</title>
    <price>19.95</price>
</book>
<author id="TN28">
    <name>Tom Noodle</name>
     <addr>
           <street>213 Rigatoni Road</street>
           <city>Toronto</city>
           <country>Canada</country>
      </addr>
      <phone>4162050745</phone>
</author>

不过,在很多情况下您的文档中的特定元素和属性都不可能同时包含数字和非数字值。如果您知道所有 author ID 都是数字,则可以在查询中指明,从而允许 DB2 使用 DOUBLE 索引。以下查询明确地将连接谓词的两端转换为 DOUBLE。这要求进行数值比较,并且显然不接受非数值连接匹配。因此,DB2 可以使用 DOUBLE 索引用于快速连接处理。

XQUERY
for $i in db2-fn:xmlcolumn("BOOKS.BOOKINFO") 
    for $j in db2-fn:xmlcolumn ("AUTHORS.AUTHORINFO") 
where $i/book/authors/author/@id/xs:double(.) = $j/author/@id/xs:double(.)  
return $j;

由于该查询没有包含用来限制 books 或 authors 表的值谓词,因此 DB2 必须在两个表上执行表扫描,以便读取所有 author ID。对于每个 author ID,使用索引来检查该 ID 是否在另一个表中出现。这比不使用任何索引的两个表扫描的嵌套循环连接要快得多。DB2 的基于成本的优化器决定了要扫描的表以及通过索引要访问的内容。表 5 展示了这两个执行计划。如果您以以下方式之一用 SQL/XML 标记编写同样的连接,那么这两个执行计划也是可行的:

查询 1:

select authorinfo
from books, authors
where xmlexists('$b/book/authors[author/@id/xs:double(.) = 
                               $a/author/@id/xs:double(.)]'
                 passing bookinfo as "b", authorinfo as "a");

查询 2:

select authorinfo
from books, authors
where xmlexists('$a/author[@id/xs:double(.) = 
                         $b/book/authors/author/@id/xs:double(.)]'
                 passing bookinfo as "b", authorinfo as "a");

查询 1 和查询 2 的区别在于 XMLEXISTS 谓词的 “方向”。在这两个查询中,方括号中表示了连接谓词。查询 1 中,方括号中的谓词是以 $b 开始的表达式上的谓词,因此它是 books 表的上的谓词。DB2 9.7 忽略了这个语法差别,并选择表 5 中开销更小的执行计划。

不过,在 DB2 9.7 之前,查询 1 和查询 2 中的 XMLEXISTS 的方向决定了使用表 5 中的哪个执行计划。由于查询 1 表达了 books 表中的连接谓词,所以 DB2 9.1 和 9.5 执行一个扫描 authors 的表,然后使用索引 AUTHORIDX1 查找 books 表。如表 5 左侧所示。

查询 2 对 authors 表应用连接谓词。因此 9.1 和 9.5 在 books 上执行表扫描,然后使用索引 AUTHORIDX2 来检查 authors 表(如表 5 右侧所示)。因此,编写 XMLEXISTS 谓词的方式可以影响以前的 DB2 版本连接顺序。如果无法避免使用表扫描,则将其用在最小的表上。

表 5. XML 连接查询的执行计划,由 db2exfmt 产生
查询 1查询 2
                      Rows
                     RETURN
                     (   1)
                      Cost
                       I/O
                       |
                  3.59881e-005
                     NLJOIN
                     (   2)
                     5410.62
                       743
                /-------+-------\
        1.29454e-007              278
           NLJOIN               NLJOIN
           (   3)               (   6)
           4311.96              1098.66
             570                  173
          /---+--\               /-+\
       556    2.32831e-010    139      2
     TBSCAN      XSCAN      FETCH   XSCAN
     (   4)      (   5)     (   7)  (  11)
     106.211     7.5643     47.237  7.56421
       14           1         34       1
       |                   /---+---\
       556              139          556
 TABLE: MATTHIAS      RIDSCN   TABLE: MATTHIAS
     AUTHORS          (   8)        BOOKS
                      15.2133
                         2
                        |
                        139
                      SORT
                      (   9)
                      15.2129
                         2
                        |
                        139
                      XISCAN
                      (  10)
                      15.1542
                         2
                        |
                        556
                  XMLIN: MATTHIAS
                    AUTHORIDX1
                       Rows
                      RETURN
                      (   1)
                       Cost
                        I/O
                        |
                   8.37914e-015
                      NLJOIN
                      (   2)
                      5410.63
                        743
                /--------+--------\
        1.29454e-007           6.47269e-008
           NLJOIN                 NLJOIN
           (   3)                 (   6)
           4311.96                1098.67
             570                    173
          /---+--\               /---+--\
       556    2.32831e-010    139    4.65661e-010
     TBSCAN      XSCAN      FETCH       XSCAN
     (   4)      (   5)     (   7)      (  11)
     106.211     7.56429    47.2365     7.5643
       14           1         34           1
       |                   /---+---\
       556              139          556
 TABLE: MATTHIAS      RIDSCN   TABLE: MATTHIAS
      BOOKS           (   8)       AUTHORS
                      15.2128
                         2
                        |
                        139
                      SORT
                      (   9)
                      15.2124
                         2
                        |
                        139
                      XISCAN
                      (  10)
                      15.1537
                         2
                        |
                        556
                  XMLIN: MATTHIAS
                    AUTHORIDX2

总结一下关于 XML 连接查询的建议,通常将连接谓词转换为应使用的 XML 索引类型。否则,查询语义不允许使用索引。如果将 XML 索引定义为 DOUBLE,则用 xs:double 转换连接谓词。如果将 XML 索引定义为 VARCHAR,则用 fn:string 转换连接谓词,如表 6 所示。(严格来说,DB2 9.7 不一定要使用 fn:string 来为连接谓词启用 VARCHAR 索引。但是,在 DB2 9.5 中仍然要使用 fn:string,并且在谓词中使用它没有不良影响)。

表 6. 转换连接谓词以便允许使用 XML 索引
索引 SQL 类型转换连接谓词时使用:注释
DOUBLExs:double用于任何数值比较
VARCHAR(n), VARCHAR HASHEDfn:string用于任何字符串比较
DATExs:date用于日期比较
TIMESTAMPxs:dateTime用于时间戳比较

“between” 谓词的索引支持

XQuery 没有类似于关系查询中 “between” 谓词的专用功能或操作符。另外,当表达 “between” 条件时,需要注意 XQuery 一般比较谓词的存在本质。

假定您想找到价格在 20 到 30 之间的书。直观上您可能使用谓词 /book[price > 20 and price < 30],但是它并没有构成 “between 谓词”。这表示如果您有一个 /book/price 上的索引,DB2 也无法在 20 到 30 之间执行索引的范围扫描以便找到该价格范围内的书。这是因为 book 文档可能有多个 price 子元素,如以下示例所示:

<book>
   <title>Database Systems</title>
   <price currency="RMB">40</price>
   <price currency="USD">10</price>
</book>

由于一般比较(>、<、= 和 <= 等等)有存在语义,如果存在值大于 20 的子元素 “price”,同时存在值小于 30 的子元素 “price”,则谓词 /book[price > 20 and price < 30] 将选择该 book 元素。这些子元素可以是同一个 “price” 元素或两个不同的 “price” 元素。上述示例文档满足了谓词,因为有一个高于 20 的价格,且还有一个低于 30 的(不同的)价格。但是,这两个价格都不在 20 到 30 之间。

如果 DB2 在 20 到 30 之间使用单个索引范围扫描,则会错过该文档,并返回不完全的查询结果。或者 DB2 必须计算两个索引扫描的交集,通常这样做的开销将相当高。执行计划的差别如图 3 所示。左侧的执行计划展示了索引 AND'ing 的执行计划,DB2 必须考虑捕获示例文档。该计划是无效的,因为两个 XISCAN 可能产生相当多数量的行 ID,需要使用上述 IXAND 操作符从中排除许多 ID。原因是很多书价格高于 $20,同时还有很多价格低于 $30 的书。实际上,两个组合的 XISCAN 产生的行 ID 多于表中的行。这需要 IXAND 操作符仅仅为了找到一个小交集而进行繁重的工作。

如果您的目的是要实现真正的 “between” 谓词,则右侧的执行计划会更好一些,因为带有启动谓词的单个范围扫描仅传递匹配的行 ID。只有相当少的索引访问且没有索引 AND'ing,这样整体性能将提高一个或两个数量级 —— 取决于谓词选择。

图 3. 对比索引 AND'ing 和单个范围扫描,计算一对范围谓词
                     RETURN 
                       |
                     NLJOIN 
                       |
                     /-+-\
                    /     \
                 FETCH    XSCAN  
                   |
               /---+---\
              /         \
          RIDSCN       TABLE:   
            |	      BOOKS
          SORT   
            |
          IXAND  
            |
        /---+---\
    XISCAN      XISCAN
 price > 20    price < 30
                  RETURN 
                    |
                  NLJOIN 
                    |
                  /-+-\
                 /     \
              FETCH    XSCAN  
                |
            /---+---\
           /         \
       RIDSCN       TABLE:   
         |	          BOOKS
       SORT   
         |
     XISCAN
 20 < price < 30

仅当 DB2 可以确定数据项是单个项且不是多于一项的序列时,相同数据项上的一对范围谓词才可以由 DB2 编译器解释为 “between”,并由单个索引扫描进行计算。换句话说,必须这样来表示谓词,即 between(> 和 <)的两个部分始终用于相同的单个项。利用值比较(>、< 和 = 等)、self 轴或属性可以实现上述要求。

值比较
如果知道某个 book 至多有一个 price 元素,则可以使用 XQuery 的值比较来编写查询,这迫使进行比较的操作数是单个数。例如,可以安全地将 /book[price gt 20 and price lt 30] 解释为 “between”,且通过 price 索引的单个范围扫描进行计算。如果出现某个 book 拥有多个 price 子元素,则运行时查询将失败,并产生错误。

Self 轴
除了值比较,还可以使用 self 轴(由点 “.” 表示)来表达 “between” 谓词。表达式 /book/price[. > 20 and . < 30] 中的 self 轴确保将那两个谓词用于相同的 price 元素。由于 self 轴始终对单个值进行计算,因此上述表达式构成了 “between” 谓词。该谓词允许 book 拥有多个 price,但是要求所有 price 的取值在 20 到 30 之间。这种方式较之使用值比较的优势在于不会有运行时错误的风险。

属性
如果 book price 是属性,则该属性在每个 book 元素至多出现一次。在表达式 /book[@price>20 and @price<30] 中,范围谓词的操作数是单个数,因此 DB2 可以执行单个索引范围扫描来计算 “between”。

索引文本节点以及 XPath “/text()”

简单回顾一下什么是文本节点。图 4 展示了示例文档及其在 XML 数据模式中的分层格式。每个元素由元素节点表示,实际的数据值由文本节点表示。在 XML 数据模型中,将元素的值定义为该元素下子树中所有文本节点的串联。因此,元素 “book” 的值为 “Database Systems29”。最底层元素的值等于其文本节点,例如,元素 “price” 的值为 29。

图 4. 示例文档的 XML 数据模型
<book>
   <title>Database Systems</title>
   <price>29</price>
</book>
示例文档的 XML 数据模型
示例文档的 XML 数据模型

XPath 表达式 /book/price/book/price/text() 是不同的。前者识别了元素节点 “price”,而后者指向值为 “29” 的文本节点。因此,下面两个查询将返回不同的结果。

XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")/book
return $b/price

XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")/book
return $b/price/text()

第一个查询将返回完整的元素节点,即 <price>29</price>,而第二个查询仅返回其文本节点值 29。如果在查询谓词中使用了不带 /text() 的 XPath 表达式,例如下面查询中的 $b/book,则 DB2 将自动使用元素的值来计算谓词。由于 “book” 元素的值是 “Database Systems29”,因此该查询将返回示例文档作为有效匹配。

XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")
where $b/book = "Database Systems29"
return $b

但是,在下一个查询中,将 /text() 添加到 where 子句的路径中,将不会返回示例文档。这是因为文本节点不是直接位于元素 “book” 下。

XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")
where $b/book/text()= "Database Systems29"
return $b

因此,一般情况下,查询语义是不同的,这取决于 /text() 的使用。对于仅拥有单个文本节点的最底层元素,不论是否使用 /text(),它们都将可能展示相同的行为。例如,下面两个查询可能返回相同的结果,但条件是仅当 查询执行过程中所遇到的全部 “price” 元素都拥有单个文本节点且没有其他子节点。

XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")
where $b/book/price < 10
return $b

XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")
where $b/book/price/text() < 10
return $b

既然 /text() 通常会使查询语义不同,它也会使索引合格性不同。在表 7 中,可以看到带有 /text() 的谓词只能由在其 XML 模式中也指定了 /text() 的索引来计算。如果索引没有使用 /text(),则谓词也不应使用 /text()

表 7. 带有或不带有 /text() 的索引和谓词
谓词或索引定义...using xmlpattern '/book/title/text()' as sql varchar(128);...using xmlpattern '/book/title' as sql varchar(128);
当 $i/book/title = "Database Systems" 时-+
当 $i/book/title/text() = "Database Systems" 时+-

简单地说,建议不要在 XML 索引定义或查询谓词中使用 /text()。为了支持以 /text() 结尾的任何路径表达式的谓词,可能会在 XML 模式 //text() 上定义索引。但是此类索引将包含 XML 列中所有 文档的全部 文本节点值。因此,该索引是非常庞大的,并且在插入、更新和删除操作过程中进行维护的开销也是很大的。一般情况下,应该避免使用此类索引,除非应用程序大部分是只读的,并且确实无法预测在搜索条件中使用哪些元素。

索引非末端元素

在上一小节中,看到了在元素 /book 上的谓词,该元素被称为非末端(非原子)元素,因为它包含了其他元素。尽管可以在非末端元素上定义索引,但是仅在少数情况下这些索引才有用。考虑下面的 XML 文档。XML 模式 /book 上的索引包含了该文档的单个索引项,此索引项的值为 “John DoePeter PanDatabase Systems29SQLrelational”。此索引项是没有用的,因为普通查询不会在其谓词中使用此类串联值。大多数索引总是位于末端元素上。

<book>
   <authors>
      <author id="47">John Doe</author>
      <author id="58">Peter Pan</author>
   </authors>
   <title>Database Systems</title>
   <price>29</price>
   <keywords>
      <keyword>SQL</keyword>
      <keyword>relational</keyword>
   </keywords>
</book>

有一些情况下,非末端元素上的索引是有意义的。例如,假定您的查询包含区号和完整电话号码上的谓词。在这种情况下,您可以选择设计 phone 元素,如本文档中所示。

<author  id="47">
    <name>John Doe</name>
    <phone>
           <areacode>408</areacode>
           <number>4511234</number>
    </phone>
</author>

然后,可以在非末端元素 “phone” 和元素 “areacode” 上分别定义一个 XML 索引:

create index phoneidx on authors(authorinfo) generate key using
xmlpattern '/author/phone' as sql double;

create index areaidx on authors(authorinfo) generate key using
xmlpattern '/author/phone/areacode' as sql double;

这将允许下面两个查询来使用索引访问,而不是表扫描。

select authorinfo from authors
where xmlexists('$a/author[phone=4084511234]' passing authorinfo as "a");

select authorinfo from authors
where xmlexists('$a/author[phone/areacode=408]' passing authorinfo as "a");

XML 索引不能组合类似多列关系索引的关键索引。即,不能在两个或多个 XML 模式上定义单个索引。但是如果对元素进行了适当地嵌套,则有时可以模拟组合索引。例如,上述索引 phoneidx 更像是 /phone/areacode/phone/number 上的组合索引。

不能使用 XML 索引的特殊情况

XMLQUERY 和 XMLEXISTS 的特殊情况

所有已讨论的 XML 索引合格性的指导原则都适用于 XQuery 和 SQL/XML 查询。另外,对于 SQL/XML 函数 XMLQUERY 和 XMLEXISTS,还需要考虑一些特定因素。

如果在 SQL 语句的 select 子句的 XMLQUERY 函数中使用 XML 谓词,则这些谓词不会从结果集中排除任何行,因此不能使用索引。它们一次只能用于一个文档,且返回文档片断(可能为空)。因此,应该将任何文档过滤谓词和行过滤谓词放入 SQL/XML 语句的 where 子句的 XMLEXISTS 谓词中。

在 XMLEXISTS 中表示谓词时,请确保使用了方括号,例如 $a/author[phone=4084511234],而不是 $a/author/phone=4084511234。后者是 Boolean 谓词,如果 phone 元素没有所需的值,则返回 “false”。由于 XMLEXISTS 实际上是检查值的存在,甚至 “false” 值的存在也将满足 XMLEXISTS,因此任何文档都可以成为结果集。如果使用了方括号,则 XPath 表达式将对存在测试失败的空序列进行计算,然后排除相应的行(如果文档没有所需的电话号码)。

有关这些 XMLQUERY 和 XMLEXISTS 语义的更多详细示例,请参考 “DB2 9 中 15 个 pureXML 性能最佳实践”(developerWorks,2006 年 10 月)。

Let 和 return 子句

请注意 XQuery letreturn 子句不会过滤结果集。因此,如果涉及到元素结构,则它们不能使用索引。下面两个查询不能使用索引,因为必须为每个 author 返回元素 “phone408”,即使是区号 408 之外的 author 的空元素。

XQUERY for $a in db2-fn:xmlcolumn("AUTHORS.AUTHORINFO")/author
let $p := $a/phone[areacode="408"]//text()
return <phone408>{$p}</phone408>

XQUERY for $a in db2-fn:xmlcolumn("AUTHORS.AUTHORINFO")/author
return <phone408>{$a/phone[areacode="408"]//text()}</phone408>

父级

DB2 9 也不会将索引用于出现在父级(“..”)下的谓词,例如下面两个查询中 “price” 上的谓词:

XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")/book/title[../price < 10]
return $b

XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")/book/title 
where $b/../price < 10
return $b

这不是一个很重要的限制,因为可以始终不用 parent 轴来表示这些谓词:

XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")/book[price < 10]/title
return $b

XQUERY for $b in db2-fn:xmlcolumn("BOOKS.BOOKINFO")/book
where $b/price < 10
return $b/title

带有双斜线 (//) 的特殊情况

另一个需要注意的情况是使用带有子轴或 self 轴(一般省略为 //)的谓词。假定您想找到 ID 为 129 的作者的书籍。如果 author ID 属性出现在多个层次,或者您不确定 ID 属性位于哪一层或哪一个元素下,则您可能会编写以下这个不适宜的查询:

错误!

select bookinfo
from books
where XMLEXISTS('$b/book/authors[//@id = 129]'
                 passing bookinfo as "b")

该查询的意图是检查 “authors” 元素中或 “authors” 元素下任何位置的 ID 属性。但是,方括号中谓词前的单斜线或双斜线(/ 或 //)没有上下文,所以引用到文档的根。因此,查询将返回下面的文档作为结果,但这并不是预期的结果。

<book id="129">
    <authors>
      <author  id="47">John Doe</author>
      <author  id="58">Peter Pan</author>
    </authors>
    <title>Database Systems</title>
    <price>29</price>
</book>

为了避免此类问题,您必须添加一个点(self 轴),以便表明您想应用文档树中从 “authors” 元素向下的子轴或 self 轴 (//)。

正确!

select bookinfo
from books
where XMLEXISTS('$b/book/authors[.//@id = 129]'
                 passing bookinfo as "b")

这还允许 DB2 使用定义在 /book//@id//@id 上的索引。如果没有点,则不会使用任何索引。

您可以在 “On the Path to Efficient XML Queries” 一文中找到有关 XQuery 和 SQL/XML 语言语义对索引合格性的影响的更多示例。

结束语

XML 索引对于 XML 查询的高性能是至关重要的,但是 XML 查询的通配符、名称空间、数据类型、连接、文本节点以及其他语义方面决定了是否可以使用某个索引。需要注意这些方面,以确保 XML 索引定义和查询谓词是兼容的。本文给出了一组指导原则和示例,用来展示如何使用 XML 索引来避免表扫描并提高查询性能。最重要的指导原则在可下载的 “XML 索引摘要” 中进行了总结。

致谢

感谢 Andrey Balmin、Kevin Beyer、Christina Lee、Henrik Loeser、Fatma Ozcan、Bryan Patterson、Vitor Rodrigues、Marcus Roy 和 Cindy Saracco 对本文的帮助。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Information Management, XML
ArticleID=180843
ArticleTitle=利用 XML 索引提升 DB2 9 中的 XML 查询性能
publish-date=09212009