COLLECT 操作
COLLECT操作允许用户根据一个或多个分组条件对数据进行分组,并提取所有不重复的值,统计各值出现的次数,并有效计算统计各种属性
COLLECT 操作的不同变体几乎涵盖了大部分数据分组和聚合的需求。而对于滑动窗口形式的聚合操作,可以采用WINDOW操作
语法
COLLECT操作有几种语法变体:
# 分组:所有表达式expression计算结果相同的文档分组,并将结果赋值给variableName。
COLLECT variableName = expression
# 分组并收集到变量:将所有表达式expression计算结果相同的文档分组,并将同一组的文档放入名为groupsVariable的数组中。
COLLECT variableName = expression INTO groupsVariable
# 分组并投影:类似于上面的分组操作,但同时将每一组的文档投影为projectionExpression的结果,并将这些结果存储到groupsVariable数组中。
COLLECT variableName = expression INTO groupsVariable = projectionExpression
# 分组并保留变量:分组的同时保留keepVariable所指定的变量值,将其添加至每个分组结果(数组元素)中。
COLLECT variableName = expression INTO groupsVariable KEEP keepVariable
# 分组并计算总数:在分组的基础上,计算每个分组的文档数量,并将数量存入countVariable。
COLLECT variableName = expression WITH COUNT INTO countVariable
# 带聚合表达式的分组:根据expression进行分组,并对每个分组执行指定的聚合操作aggregateExpression,例如求和、平均值等。
COLLECT variableName = expression AGGREGATE variableName = aggregateExpression
# 分组并聚合到变量:类似于前面的分组和聚合操作,但同时也将聚合结果与相应的分组一同存储到groupsVariable数组中。
COLLECT variableName = expression AGGREGATE variableName = aggregateExpression INTO groupsVariable
# 仅聚合:不进行分组,直接对所有文档执行指定的聚合操作aggregateExpression。
COLLECT AGGREGATE variableName = aggregateExpression
# 仅聚合并收集到变量:如果aggregateExpression返回的是一个数组(例如多维度聚合),则将其结果存储到groupsVariable数组中。
COLLECT AGGREGATE variableName = aggregateExpression INTO groupsVariable
# 仅计算总数:不进行分组,直接计算所有文档的数量,并将结果存入countVariable。
COLLECT WITH COUNT INTO countVariable
所有COLLECT操作变体都可以选择性地附加一个OPTIONS { … }子句,以提供更多配置选项来定制COLLECT行为
注:在AQL查询语言中,执行COLLECT操作后,当前作用域内的所有局部变量都将被消除。这意味着在COLLECT语句之后,仅能访问由COLLECT自身引入的变量。这些变量通常包含了分组或聚合的结果。
Grouping 语法
第一种形式的COLLECT语法仅根据在expression中定义的分组标准对结果进行分组。为了进一步处理由COLLECT产生的结果,引入了一个新的变量(通过variableName指定)。这个变量包含了分组值。
下面是一个查询示例,它查找u.city中的不同值,并将其保存在变量city中:
FOR u IN users
COLLECT city = u.city
RETURN {
"city" : city
}
第二种形式的COLLECT与第一种形式相似,除了它还引入了一个变量(由groupsVariable指定),该变量包含了落入同一组的所有元素。其工作原理如下:groupsVariable是一个数组,数组中的元素数量与分组内的元素数量相同。数组中的每个成员都是一个JSON对象,其中查询中定义的所有变量值都绑定到相应的属性上。请注意,这会考虑在COLLECT语句之前定义的所有变量,但不包括顶层(FOR循环外部)定义的变量,除非COLLECT语句本身就在顶层,那样的话所有变量都会被考虑进去。此外,请注意为了提高性能,优化器可能会将LET语句移出FOR语句。
FOR u IN users
COLLECT city = u.city INTO groups
RETURN {
"city" : city,
"usersInCity" : groups
}
在上述示例中,users数组将按属性city进行分组。结果是一个新的文档数组,其中每个元素代表一个唯一的u.city值。由于使用了INTO子句,因此每座城市的原始数组(此处为users)中的元素都可在变量groups中访问到。
COLLECT 也允许指定多个分组标准。各个分组标准可以通过逗号进行分隔:
FOR u IN users
COLLECT country = u.country, city = u.city INTO groups
RETURN {
"country" : country,
"city" : city,
"usersInCity" : groups
}
在上述示例中,数组users首先按国家进行分组,然后按城市进一步分组。对于每一对独特的国家和城市组合,都会返回该组合下的所有用户。\
丢弃不必要的变量
COLLECT的第三种形式允许使用任意投影表达式来重写groupsVariable的内容:
FOR u IN users
COLLECT country = u.country, city = u.city INTO groups = u.name
RETURN {
"country" : country,
"city" : city,
"userNames" : groups
}
在上述示例中,仅使用u.name作为投影表达式。因此,对于每篇文档,只有这个属性会被复制到groupsVariable变量中。相比没有指定投影表达式时将作用域内所有变量都复制到groupsVariable的情况,这种方式通常效率更高。
INTO后面的表达式也可以用于任意计算:
FOR u IN users
COLLECT country = u.country, city = u.city INTO groups = {
"name" : u.name,
"isActive" : u.status == "active"
}
RETURN {
"country" : country,
"city" : city,
"usersInCity" : groups
}
COLLECT 还提供了一个可选的 KEEP 子句,用于控制哪些变量将被复制到由 INTO 创建的变量中。如果不指定 KEEP 子句,则作用域内的所有变量都将作为子属性被复制到 groupsVariable 中。这种方式虽然安全,但如果作用域内有许多变量或这些变量包含大量数据时,可能对性能产生负面影响。
以下示例仅将变量name复制到groupsVariable中。作用域中存在的其他变量u和someCalculation不会被复制,因为它们未在KEEP子句中列出
KEEP 子句只能与 INTO 结合使用,并且只能包含有效的变量名称。KEEP 支持指定多个变量名称。在这个例子中,只保留了用户的名字,并按城市分组后返回每个城市的用户名列表。
组长度计算
COLLECT 还提供了一个特殊的 WITH COUNT 子句,可以高效地计算每个分组的成员数量。
最简单的形式是仅仅返回进入 COLLECT 的项目数量:
FOR u IN users
COLLECT WITH COUNT INTO length
RETURN length
上述代码等同于,但不如以下代码高效:
RETURN LENGTH(users)
WITH COUNT 子句也可用于高效计算每个分组内的项目数量:
FOR u IN users
COLLECT age = u.age WITH COUNT INTO length
RETURN {
"age" : age,
"count" : length
}
WITH COUNT 子句只能与 INTO 子句一起使用
聚合
使用COLLECT语句可以实现对数据按组进行聚合操作。如果只想确定每组的长度(即每组元素的数量),可以使用前面所述的WITH COUNT INTO变体。
对于其他的聚合操作,可以在COLLECT结果上运行聚合函数:
FOR u IN users
COLLECT ageGroup = FLOOR(u.age / 5) * 5 INTO g
RETURN {
"ageGroup" : ageGroup,
"minAge" : MIN(g[*].u.age),
"maxAge" : MAX(g[*].u.age)
}
然而,上述方法需要在所有组的收集操作期间存储所有组值,这可能是低效的。
COLLECT操作的特殊AGGREGATE变体允许在收集操作过程中逐步构建聚合值,因此通常更为高效
使用AGREGATE 将上面查询变为
FOR u IN users
COLLECT ageGroup = FLOOR(u.age / 5) * 5
AGGREGATE minAge = MIN(u.age), maxAge = MAX(u.age)
RETURN {
ageGroup,
minAge,
maxAge
}
AGGREGATE 关键字只能在 COLLECT 关键字之后使用。如果使用 AGGREGATE,它必须紧跟在分组键声明的后面。如果没有使用分组键,则它必须直接跟随 COLLECT 关键字:
FOR u IN users
COLLECT AGGREGATE minAge = MIN(u.age), maxAge = MAX(u.age)
RETURN {
minAge,
maxAge
}
- 每个AGGREGATE赋值的右侧只允许使用特定的表达式:
在顶层,聚合表达式必须是对支持的聚合函数之一的调用:- LENGTH() / COUNT()
- MIN()
- MAX()
- SUM()
- AVERAGE() / AVG()
- STDDEV_POPULATION() / STDDEV()
- STDDEV_SAMPLE()
- VARIANCE_POPULATION() / VARIANCE()
- VARIANCE_SAMPLE()
- UNIQUE()
- SORTED_UNIQUE()
- COUNT_DISTINCT() / COUNT_UNIQUE()
- BIT_AND()
- BIT_OR()
- BIT_XOR()
- 聚合表达式不能引用COLLECT本身引入的变量
COLLECT vs. RETURN DISTINCT
为了使结果集唯一,可以使用COLLECT或RETURN DISTINCT。
FOR u IN users
RETURN DISTINCT u.age
FOR u IN users
COLLECT age = u.age
RETURN age
在幕后,这两种变体都创建了一个CollectNode。但是,它们使用具有不同属性的COLLECT的不同实现:
- RETURN DISTINCT保持结果的顺序,但仅限于一个值。
- COLLECT更改结果的顺序(排序或未定义),但它支持多个值,并且比RETURN DISTINCT更灵活。
除了COLLECT复杂的分组和聚合功能外,它还允许您在RETURN之前放置LIMIT操作,从而可能提前停止COLLECT操作。
COLLECT options
method
COLLECT有两种变体可供优化器选择:排序变体和散列变体。方法选项可以在COLLECT语句中用于通知优化器首选方法“排序”或“哈希”。
COLLECT ... OPTIONS { method: "sorted" }
如果用户未指定任何方法,则优化器将创建一个使用排序方法的计划,如果COLLECT语句符合条件,则将使用哈希方法创建一个附加计划。
如果显式设置为sorted,则优化器总是会选择排序版本的COLLECT方法,并且不会生成使用哈希版本的执行计划。如果显式设置为hash,则只有当COLLECT语句满足条件时,优化器才会创建使用哈希方法的执行计划。不是所有的COLLECT语句都能使用哈希方法,尤其是那些不执行任何分组操作的语句。若COLLECT语句符合条件,将会仅有一个使用哈希方法的执行计划。否则,优化器将默认使用排序方法。
COLLECT语句使用sorted方法时,它要求输入数据按照在COLLECT子句中指定的分组条件预先排序。为了保证结果正确性,优化器会在COLLECT语句之前自动插入一个SORT操作。如果有对分组条件已经存在的排序索引,优化器可能会在后续阶段进一步优化掉这个SORT操作。
若某个COLLECT语句满足使用哈希变体的条件,优化器会在规划阶段开始时为其生成一个额外的执行计划。在这个计划中,COLLECT前面不会添加额外的SORT语句,因为哈希版本的COLLECT不需要有序输入。相反,通常会在COLLECT之后添加一个SORT语句来对输出结果进行排序,不过这个SORT也可能在后续阶段被优化掉。
如果用户对COLLECT的结果排序顺序并不关心,那么在COLLECT之后添加额外的SORT null指令可以指示优化器完全移除排序操作。这是因为SORT null告诉优化器无需对结果进行实际排序,从而可能避免不必要的排序开销。然而,请注意,这个功能的存在与否取决于具体的数据库系统和其优化器的实现细节。在ArangoDB中,并未明确提到SORT null这样的语法,但在某些数据库中类似的概念可用于指导优化器做出更高效的执行计划。在实践中,确认是否以及如何能够消除这类冗余排序操作,最好参考具体数据库系统的文档或咨询相关的专家资源。
FOR u IN users
COLLECT age = u.age
SORT null /* note: will be optimized away */
RETURN age
当没有显式设置首选方法时,查询优化器会基于成本估算选择最适合的COLLECT变体。针对不同COLLECT变体创建的执行计划会经过常规优化流程处理。最终,优化器会选择预计总成本最低的那个计划来执行。
一般而言,如果在分组条件上存在已排序的索引,则应当优先考虑使用COLLECT的有序变体。这种情况下,优化器可以消除COLLECT前的SORT操作,因此不会有额外的排序步骤。
反之,如果分组条件上没有可用的排序索引,那么有序变体所需的前置排序操作可能会非常昂贵。在这种情况下,优化器很可能倾向于使用哈希变体的COLLECT,因为它不要求输入数据有序。
要确切知道查询中实际使用的COLLECT变体,可以通过查看查询的执行计划来确定,特别是检查CollectNode节点的注释部分,该注释通常会显示所采用的具体收集策略及其相关属性。这样就能了解优化器选择的是有序收集还是哈希收集方式。通过分析执行计划,开发者可以深入了解数据库是如何执行查询并作出优化决策的。
Execution plan:
Id NodeType Est. Comment
1 SingletonNode 1 * ROOT
2 EnumerateCollectionNode 5 - FOR doc IN coll /* full collection scan, projections: `name` */
3 CalculationNode 5 - LET #2 = doc.`name` /* attribute expression */ /* collections used: doc : coll */
4 CollectNode 5 - COLLECT name = #2 /* hash */
6 SortNode 5 - SORT name ASC /* sorting strategy: standard */
5 ReturnNode 5 - RETURN name