管道、阶段和可调参数
聚合框架是 MongoDB 中的一组分析工具,可以对一个或多个集合中的文档进行分析。
聚合框架基于管道的概念。使用聚合管道可以从 MongoDB 集合获取输入,并将该集合中的文档传递到一个或多个阶段,每个阶段对其输入执行不同的操作。每个阶段都将之前阶段输出的内容作为输入。所有阶段的输入和输出都是文档——可以称为文档流。
如果你熟悉 Linux shell 中的管道,比如 bash,那么这是一个非常相似的概念。每个阶段都有其特定的工作。它会接收特定形式的文档并产生特定的输出,该输出本身就是文档流。可以在管道的终点对输出进行访问,这与执行 find 查询的方式非常相似。
现在来更深入地研究各个阶段。在聚合管道中,一个阶段就是一个数据处理单元。它一次接收一个输入文档流,一次处理一个文档,并且一次产生一个输出文档流
每个阶段都会提供一组旋钮或可调参数(tunables),可以通过控制它们来设置该阶段的参数,以执行任何感兴趣的任务。一个阶段会执行某种类型的通用任务,我们会为正在使用的特定集合以及希望该阶段如何处理这些文档设置阶段的参数。
这些可调参数通常采用运算符的形式,可以使用这些运算符来修改字段、执行算术运算、调整文档形状、执行某种累加任务或其他各种操作。
管道是与 MongoDB 集合一起使用的。它们由阶段组成,每个阶段对其输入执行不同的数据处理任务,并生成文档以作为输出传递到下一个阶段。最终,在处理结束时,管道会产生一些输出,这些输出可以用来在应用程序中执行某些操作,或者被发送到某个集合以供后续使用。在许多情况下,为了执行所需的分析,我们会在单个管道中包含多个相同类型的阶段。
阶段入门
聚合管道主要由匹配(match)、投射(project)、排序(sort)、跳过(skip)和限制(limit)这 5 个阶段构建组成。
1、过滤
>db.test.aggregate([
{“$match”:{start_time:2022}}
])
以上类似于
>db.test.find({“start_time”:2022})
2、投射
>db.test.aggregate([
{“$match”:{“start_time”:2022}},
{“$project”:{“_id”:0,“name”:1,“start_time”:1}}
])
匹配阶段会对集合进行过滤,并将结果文档一次一个地传递到投射阶段。然后投射阶段会执行其操作,调整文档形状,并从管道中将输出传递回来。
现在进一步扩展管道,再包括一个限制阶段。我们将使用相同的查询进行匹配,但是把结果集限制为 5,然后投射出想要的字段。为简单起见,将输出限制为每个公司的名称:
>db.test.aggregate([
{“$match”:{“start_time”:2022}},
{$limit:5},
{$project:{_id:0,name:1}}
])
注意,构建的这条管道已在投射阶段之前进行限制。如果先运行投射阶段,然后再进行限制,那么就像下面的查询一样,将得到完全相同的结果,但这样就必须在投射阶段传递数百个文档,最后才能将结果限制为 5 个:
>db.test.aggregate([
{“$match”:{“start_time”:2022}},
{$project:{_id:0,name:1}},
{$limit:5}
])
无论 MongoDB 查询规划器在给定版本中进行何种类型的优化,都应该始终注意聚合管道的效率。确保在构建管道时限制从一个阶段传递到另一个阶段的文档数量。
如果顺序很重要,那么就需要在限制阶段之前进行排序。排序的工作方式与我们已经看到的类似,只是在聚合框架中,会将排序指定为管道中的一个阶段,如下所示:
>db.test.aggregate([
{“$match”:{“start_time”:2022}},
{$sort:{“name”:1}},
{$limit:5},
{$project:{_id:0,name:1}}
])
最后,再将跳过阶段包含进来。先进行排序,然后跳过前 10 个文档,并再次将结果集限制为 5 个文档:
>db.test.aggregate([
{$match:{start_time:2022}},
{$sort:{name:1}},
{$skip:10},
{$limit:5},
{$project:{_id:0,name:1}}
])
表达式
聚合框架支持许多表达式类型:
1、布尔表达式允许使用 AND、OR 和 NOT。
2、集合表达式允许将数组作为集合来处理。特别地,可以取两个或多个集合的交集或并集,也可以取两个集合的差值并执行一些其他的集合运算。
3、比较表达式能够表达许多不同类型的范围过滤器。
4、算术表达式能够计算上限(ceiling)、下限(floor)、自然对数和对数,以及执行简单的算术运算,比如乘法、除法、加法和减法。甚至可以执行更复杂的运算,比如计算值的平方根。
5、字符串表达式允许连接、查找子字符串,以及执行与大小写和文本搜索相关的操作。
6、数组表达式为操作数组提供了强大的功能,包括过滤数组元素、对数组进行分割或从特定数组中获取某一个范围的值。
7、变量表达式允许处理文字、解析日期值及条件表达式。
8、累加器提供了计算总和、描述性统计和许多其他类型值的能力。
$project
提取嵌套字段:
>db.test.aggregate([
{$match:{user.name:“zhangsan”}},
{$project:{_id:0,“username:$user.name”}}
])
$ 字符表示这些值应被解释为字段路径,并分别用于为每个字段选择应投射的值。
投射阶段,唯一不能做的事情就是更改值的数据类型。
$unwind
在聚合管道中处理数组字段时,通常需要包含一个或多个展开(unwind)阶段。这允许我们将指定数组字段中的每个元素都形成一个输出文档.
数组表达式
1、过滤器表达式根据过滤条件选择数组中的元素子集。
比如以下列子,只选择那些 “funding_rounds” 的 “raised_amount” 大于或等于 100 000 000的元素。
>db.test.aggregate([
{$match:{“funding_rounds.finacil”:“zhangsan”}},
{$project:{
_id:0,
name:1,
rounds:{$filter:{
input:“$funding_rounds”,
as:“round”,
cond:{“$gte”:[“$$round.raised_amount”,100000000]}
}}
}}
])
其中rounds 字段使用了一个过滤器表达式。$filter 运算符用来处理数组字段,并指定必须提供的选项。$filter 的第一个选项是 input。对于 input,只需为其指定一个数组。接下来指定这个 “funding_rounds” 数组在过滤器表达式的其余部分中使用的名称。然后,作为第三个选项,需要指定一个条件。这个条件应该提供用于过滤作为输入的任何数组的条件,选择一个子集。
在指定条件时,我们使用了 $$。$$ 用来引用在表达式中定义的变量。as 子句在过滤器表达式中定义了一个变量。由于在 as 子句中对这个变量进行了标记,因此这个变量的名称是 “round”。
2、$arrayElemAt 运算符允许选择数组中特定位置的元素.比如定位数组第一个和最后一个元素:
>db.test.aggregate([
{$match:{“startTime”:2022}},
{$project:{
_id:0,
name:1,
first:{$arrayElemAt:[“$arr”,0]},
last:{$arrayElemAt:[“$arr”,-1]}
}}
])
3、与 $arrayElemAt 相关的是 $slice 表达式,其允许在数组中从一个特定的索引开始按顺序返回多个元素,如从索引 1 开始并在数组中获取 3 个元素:
>db.test.aggregate([
{$match:{“startTime”:2022}},
{$project:{
_id:0,
name:1,
early:{$slice:[“$arr”,1,3]}
}}
])
4、过滤和选择数组的单个元素或片段是对数组执行的常见操作之一。然而,最常见的操作可能是确定数组的大小或长度。可以使用 $size 运算符执行此操作:
>db.test.aggregate([
{$match:{“starttime”:2022}},
{$project:{
_id:0,
name:1,
total:{$size:“$arr”}
}}
])
累加器
聚合框架提供的累加器可以执行对特定字段中的所有值进行求和($sum)、计算平均值($avg)等操作。$first 和 $last 也被视为累加器,因为在它们所在的阶段中所有经过的文档的值都会被检查。$max 和 $min 是另外两个累加器的例子,它们会查看文档流并只保存看到的其中一个值。可以使用 $mergeObjects 将多个文档合并为单个文档。
用于数组的累加器。当文档通过管道传递时,可以将值$push 到数组中。$addToSet 与 $push 非常相似,只是它可以确保结果数组中不包含重复的值。
在 MongoDB 3.2 之前,累加器只能在分组阶段使用。MongoDB 3.2 引入了在投射阶段访问部分累加器的功能。累加器在分组阶段和投射阶段的主要区别是,在投射阶段,像 $sum 和 $avg 这样的累加器必须在单个文档中对数组进行操作,而分组阶段中的累加器能够跨多个文档对值进行计算。
1、在投射阶段使用累加器
以下为识别出数组中的最大值,方法是进入数组中的内嵌文档,并将最大值投射到输出文档中
>db.test.aggregate([
{$project:{
_id:0,
large:{$max:“$Arr”}
}}
])
2、$sum 累加器来计算集合中数据的总和
>db.test.aggregate([
{$project:{
_id:0,
total:{$sum:“$Arr”}
}}
])
分组
在 MongoDB 以前的版本中,累加器只能在聚合框架的分组阶段中使用。分组阶段执行的功能类似于 SQL 中的 GROUP BY 命令。在分组阶段,可以将多个文档的值聚合在一起并对它们执行某种类型的聚合操作,比如计算平均值。来看一个例子:
>db.test.aggregate([
{$group:{
_id:{startTime:“$startTime”},
avg:{$avg:“$numbers”}
}},
{$sort:{avg:-1}}
])
分组阶段的基础是"_id" 字段,我们将其指定为文档的一部分。这是 $group 运算符本身的值,其解释是非常严格的。
我们使用这个字段来定义分组阶段使用什么来组织文档。因为分组阶段是第一阶段,所以 aggregate 命令会将 test 集合中的所有文档都传递给此阶段。分组阶段会将所有 “startTime” 具有相同值的文档视为一组。在构造这个字段的值时,该阶段会使用 $avg 累加器计算具有所有相同 “startTime” 公司的平均数。
将聚合管道结果写入集合中
作为两个特定的阶段,$out 和 $merge 可以将聚合管道生成的文档写入集合中。这两个阶段不能同时使用,并且任一阶段必须是聚合管道的最后一个阶段。$merge 是在 MongoDB 4.2 中引入的,如果可以使用的话,它是将结果写入集合中的首选方式。$out 有一些限制:它只能写入相同的数据库;如果集合已经存在,那么它会覆盖任何现有的集合;它不能写入已分片的集合中。$merge 可以写入任何数据库和集合中,无论是否分片。$merge 还可以在处理现有集合时对结果进行合并(插入新文档、与现有文档合并、操作失败、保留现有文档或使用自定义更新处理所有文档)。但使用 $merge 的真正优势在于,它可以创建按需生成的物化视图(materialized view),在管道运行的过程中,输出到集合的内容会进行增量更新。