编写更有效的xquery代码(避免冗余迭代)

时间:2010-05-13 01:50:39

标签: xml xslt xquery

以下是我正在处理的问题的简化版本:我有一堆xml数据可以编码有关人员的信息。每个人都由'id'属性唯一标识,但它们可能有许多名称。例如,在一个文档中,我可能会找到

<person id=1>Paul Mcartney</person>
<person id=2>Ringo Starr</person>

在另一个我可能会发现:

<person id=1>Sir Paul McCartney</person>
<person id=2>Richard Starkey</person>

我想使用xquery生成一个新文档,列出与给定id关联的每个名称。即:

<person id=1>
    <name>Paul McCartney</name>
    <name>Sir Paul McCartney</name>
    <name>James Paul McCartney</name>
</person>
<person id=2>
    ...
</person>

我现在在xquery中这样做的方式是这样的(伪代码):

let $ids := distinct-terms( [all the id attributes on people] )
for $id in $ids
    return <person id={$id}>
    {
    for $unique-name in distinct-values
            (
            for $name in ( [all names] )
            where $name/@id=$id
            return $name
            )
        return <name>{$unique-name}</name>
    }
    </person>

问题是这真的很慢。我想瓶颈是最里面的循环,它为每个id(其中大约有1200个)执行一次。我正在处理相当多的数据(300 MB,分布在大约800 xml文件上),所以即使在内循环中单次执行查询大约需要12秒,这意味着重复1200次将需要大约4小时(这可能是乐观的 - 到目前为止,该过程已经运行了3个小时)。它不仅速度慢,而且使用了大量的虚拟内存。我正在使用Saxon,我必须将java的最大堆大小设置为10 GB(!)以避免出现内存错误,并且它当前正在使用6 GB的物理内存。

所以这就是我真的喜欢这样做(在Pythonic伪代码中):

persons = {}
for id in ids:
    person[id] = set()
for person in all_the_people_in_my_xml_document:
    persons[person.id].add(person.name)

在那里,我只是在线性时间内完成了它,只有一次扫描xml文档。现在,有没有办法在xquery中做类似的事情?当然,如果我可以想象它,一个合理的编程语言应该能够做到(他说是古怪的)。我想,问题在于,与Python不同,xquery(据我所知)并没有像关联数组那样的东西。

这有什么聪明的方法吗?如果做不到的话,有什么比xquery更好的东西可以用来实现我的目标吗?因为实际上,我在这个相对简单的问题上投入的计算资源有点荒谬。

4 个答案:

答案 0 :(得分:4)

遗憾的是,这是XQuery 1.0中的一个缺点

XQuery 1.1将group by子句添加到语法中以解决此问题,您的问题将通过以下方式解决:

for $person in /person
let $id = $person/@id
group by $id
return  <people id="{$id}">{
          for $name in distinct-values($person)
          return <name>{$name}</name>
        }</people>

不幸的是,XQuery 1.1并没有被广泛实现,所以目前你没有使用group by子句。

作为XQSharp的开发人员,我不能代表任何其他实现,但我们花了很多时间调整我们的优化器来发现XQuery 1.1中的常见分组模式,并使用您指定的算法执行它们。

特别是以下版本的查询:

declare variable $people as element(person, xs:untyped)* external;

for $id in distinct-values($people/@id)
return <people id="{$id}">{
          for $person in $people
          where $person/@id = $id
          return <name>{$person}</name>
       }</people>

被视为分组,如以下查询计划所示:

library http://www.w3.org/2005/xpath-functions external;
library http://www.w3.org/2001/XMLSchema external;
declare variable $people external;

for $distinct-person in $people
let $id := http://www.w3.org/2005/xpath-functions:data($distinct-person/attribute::id)
group by
  $id
aggregate
  element {name} { fs:item-sequence-to-node-sequence($distinct-person) }
as
  $:temp:19
return
  element {person} { (attribute {id} { $id } , fs:item-sequence-to-node-sequence($:temp:19)) }

请注意,类型注释as element(person, xs:untyped)*是必需的,因为不知道节点是无类型的(未针对模式验证),查询处理器无法知道$person/@id没有其数据值中有多个项目。 XQSharp还不支持group by表达式,其中每个节点可以有多个键。但是在这种情况下,左外连接仍然被发现,因此复杂性应该大致 n log n ,而不是您遇到的二次方。

不幸的是,虽然在组中添加了一组人员的明确值(以过滤掉重复的名称)似乎阻止XQSharp找到连接;这已被提交为一个错误。现在,这可以通过两次传递进行查询来解决 - 通过id对名称进行分组,并删除重复的名称。

总之,在XQuery 1.0中没有更好的方法,但是一些实现(例如XQSharp)将能够有效地评估它。如有疑问,请查看查询计划。

有关XQSharp执行的连接优化的更详细信息,请查看此blog post

答案 1 :(得分:1)

另一种选择:使用地图。

let $map := map:map()
let $people :=
  for $person in $all-people
  return map:put($map, $person/@id, 
    (map:get($map, $person/@id), <name>{$person/text()}</name>))
return
  for $id in map:keys($map)
  return 
    <person id="{$id}">{map:get($map, $id)}</person>

答案 2 :(得分:1)

  

失败了,有什么东西   比我可能会使用的xquery更好   完成我的目标?因为真的,   我是计算资源   扔这个比较简单   问题有点荒谬。

这是一个简单的XSLT 2.0解决方案(为方便起见,三个文档中的两个由<xsl:variable> s表示):

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:variable name="vDoc2">
  <persons>
   <person id="1">Sir Paul McCartney</person>
   <person id="2">Richard Starkey</person>
  </persons>
 </xsl:variable>

 <xsl:variable name="vDoc3">
  <persons>
   <person id="1">James Paul McCartney</person>
   <person id="2">Richard Starkey - Ringo Starr</person>
  </persons>
 </xsl:variable>

 <xsl:template match="/">
  <xsl:for-each-group group-by="@id" select=
   "(/ | $vDoc2 | $vDoc3)/*/person">

   <person id="{current-grouping-key()}">
     <xsl:for-each select="current-group()">
       <name><xsl:sequence select="text()"/></name>
     </xsl:for-each>
   </person>

  </xsl:for-each-group>
 </xsl:template>
</xsl:stylesheet>

将此转换应用于以下XML文档

<persons>
    <person id="1">Paul Mcartney</person>
    <person id="2">Ringo Starr</person>
</persons>

产生了想要的正确结果

<person id="1">
   <name>Paul Mcartney</name>
   <name>Sir Paul McCartney</name>
   <name>James Paul McCartney</name>
</person>
<person id="2">
   <name>Ringo Starr</name>
   <name>Richard Starkey</name>
   <name>Richard Starkey - Ringo Starr</name>
</person>

答案 3 :(得分:0)

如果您使用支持更新的XML数据库(例如eXist db),那么您可以将Pythonesque代码直接分组到XML文档中,这可能无论如何都需要结果以供以后处理。

let $persons := doc("/db/temp/p3.xml")/persons
let $person-groups := doc("/db/temp/p2.xml")/person-groups
for $person in $persons/person
let $name := element name {$person/text()}
let $person-group := $person-groups/person-group[@id=$person/@id]
return
   if ($person-group) 
   then update insert $name into $person-group
   else update insert element person-group {attribute id {$person/@id}, $name} 
       into $person-groups

对于我的100个不同ID的10,000个人节点的实验,我们服务器上的eXist的吞吐量大约为每秒100个节点。

请注意,eXist中XQuery的更新扩展与XQuery Update语法的语法不完全相同