在XQuery中,group by子句用于对FLWOR表达式的中间结果进行分组操作。它通常位于where子句之后、return子句之前,允许开发者按照一个或多个表达式的计算结果,将序列中的项划分成不同的组,然后对每个组分别进行聚合或转换处理。

group by的基本语法结构
XQuery的group by必须写在FLWOR表达式内部。一个典型的包含分组的FLWOR语句由for、let、where、group by、order by和return组成。其中group by后面跟着一个或多个用逗号分隔的分组键表达式,这些表达式在逻辑上对当前绑定的变量求值,值相同的项被归入同一组。
需要注意的是,一旦使用了group by,在return子句中原本的迭代变量(如for绑定的变量)将不再逐条可用,而是被转换为“组内所有原项的序列”。同时,group by所声明的分组键变量可以在return中直接引用,其值为该组内共同的分组键值。这种变量作用域的变化是初学者最容易混淆的地方。
for $book in doc("books.xml")/library/book
group by $category := $book/@category
return
<group category="{$category}">
<count>{count($book)}</count>
<titles>{$book/title/string()}</titles>
</group>
分组键的计算与多字段分组
分组键不仅可以使用节点属性,也可以是任意合法的XQuery表达式。例如,我们可以根据书名的长度分组,或者根据价格所在的区间分组。只要表达式结果可比较,XQuery处理器就能正确归类。如果需要对多个维度分组,只需在group by后用逗号列出多个绑定即可,此时只有所有键都相同的项才会进入同一组。
下面示例展示按分类和出版年份两个字段分组,并统计每组的平均价格。这里用let提前计算年份,再在group by中引用,使逻辑更清晰。多字段分组时,返回结果中必须包含所有分组键变量,否则会导致静态错误。
for $item in doc("products.xml")/product
let $year := year-from-date($item/@pubDate)
group by $cat := $item/@type, $yr := $year
return
<stat type="{$cat}" year="{$yr}">
<avgPrice>{avg($item/@price/xs:decimal(.))}</avgPrice>
<total>{sum($item/@stock)}</total>
</stat>
与SQL group by的核心差异
很多熟悉SQL的开发者会默认XQuery的group by和SQL行为一致,但实际上二者有明显区别。SQL在group by后只能选择分组列或聚合列,而XQuery在分组后,原迭代变量变成组内序列,你可以对它做任意映射,比如提取每组前两个元素,或拼接字符串。这种灵活性来自XQuery以序列为第一等公民的设计。
另一个差异是执行顺序。XQuery先执行for和where,形成待分组序列,再应用group by;而某些SQL优化器可能提前做部分聚合。因此在XQuery中,如果where条件过滤不当,分组结果会直接受影响。建议在分组前用where明确缩小数据范围,提升可读性与性能。
| 对比维度 | SQL group by | XQuery group by |
|---|---|---|
| 非分组列引用 | 禁止直接引用 | 可引用组内完整序列 |
| 分组键形式 | 通常为列名 | 任意表达式 |
| 返回结构 | 行集 | XML节点或序列 |
常见错误与避坑建议
最常见的错误是在return中继续使用单条记录的属性,例如写$book/@category,但此时$book已经是同组所有原元素的序列,这样会输出多个属性造成结构混乱。正确做法是用group by绑定的$category变量。另外,若分组键表达式返回空序列,该项会被分到同一个“空键组”,需要提前用where过滤空值。
还有一类性能误区:对大文档分组时不加索引。虽然XQuery本身不强制建索引,但在支持索引的数据库(如eXist-db)中,对分组键路径建立索引能显著加速。同时应避免在group by表达式里调用高开销函数,可先用let计算并绑定到变量,减少重复求值。
(: 错误示例:在return直接使用原序列的属性 :)
for $b in doc("books.xml")/book
group by $c := $b/@category
return <x>{$b/@category}</x>
(: 正确示例:使用分组键变量 :)
for $b in doc("books.xml")/book
where exists($b/@category)
group by $c := $b/@category
return <x cat="{$c}">{count($b)}</x>
实际应用场景示例
假设我们有一个存放订单的XML,需要按用户所在城市统计消费总额并列出订单号。利用group by可以一步完成。下面代码先筛选已支付订单,按城市分组,再在return中聚合金额并拼接订单标识,最终生成报表片段。
该场景体现了XQuery分组在报表生成中的优势:不需要借助外部程序,仅用查询脚本就能输出结构化的XML结果,方便直接被前端或下游系统消费。配合order by还能对城市按金额排序,进一步增强实用性。
for $o in doc("orders.xml")/order[status = "paid"]
group by $city := $o/customer/city
return
<city name="{$city}">
<revenue>{sum($o/amount)}</revenue>
<orders>{$o/@id/string()}</orders>
</city>