当你开始在 MongoDB 中进行高级查询时,你会发现基本的 find()
命令无法提供你所需要的灵活性和健壮性。别担心:聚合管道,一个多阶段管道,可以将文档转换为聚合结果,现在可以帮助你。
在 MongoDB 中执行聚合有三种方式
Map-reduce 使用自定义 JavaScript 函数执行 map 和 reduce 操作。这种方法无法提供简单的接口,需要你在 MongoDB 中实现 JavaScript,因此会遭受性能开销。
单用途聚合 提供对常见聚合过程(如计算单个集合中文档的数量和/或返回唯一文档)的简单访问。但它们的最大缺点是缺乏聚合管道和 map-reduce 的灵活性和功能。
聚合管道 通常被首选,也是 MongoDB 中执行聚合的推荐方式。它专门设计用于提高聚合的性能和可用性。管道操作符不需要为每个输入文档生成一个输出文档,也可以生成新文档或过滤掉文档。此外,从 MongoDB 版本 4.4 开始,它还可以使用 $accumulator
和 $function
定义自定义聚合表达式。
聚合管道指的是一系列特定的操作流程,用于处理、转换并返回结果。在管道中,后续操作由前一个结果告知。
让我们来看一个典型的管道
输入 -> $match
-> $group
-> $sort
-> 输出
在上面的示例中,输入指的是一个或多个文档。 $match
、$group
和 $sort
是管道中的不同阶段。从 $match
阶段输出的结果被输入到 $group
,然后从 $group
阶段输出的结果被输入到 $sort
。这三个阶段共同构成了一个 聚合管道。
实现管道有助于我们将查询分解成更易于管理的阶段。每个阶段使用同名的操作符来完成转换,以便我们达到目标。
在查询中使用的阶段数量没有限制,但需要注意的是阶段的顺序很重要,并且有一些优化可以帮助您的管道性能更好。例如,在管道开始时使用 $match
可以显著提高整体性能。
让我们以一个名为 students 的集合为例,该集合记录了报名在线课程的人员。这将是我们的输入集合。学生被赋予一个 id(一个唯一的值)、他们的班级、节和课程费用。基于此数据模型的文档可能看起来像这样
{
student_id: "P0001"
class: 101,
section: "A",
course_fee: 12
},
{
student_id: "P0002"
class: 102,
section: "A",
course_fee: 8
},
{
student_id: "P0002"
class: 101,
section: "A",
course_fee: 12
},
{
student_id: "P0004"
class: 103,
section: "B"
course_fee: 19
}
现在,如果我们想在 mongo shell 或 MongoDB Compass 中运行 aggregate()
函数,以计算 A 节所有学生的总课程费用,我们将必须创建一个类似于以下的查询
db.students.aggregate([
{ $match: { section: "A" } },
{ $group: { student_id: "student_id", total: {$sum: "$course_fee" }}}
])
在上面的例子中,$group
阶段将使用前面聚合的输出,即 $match
,作为下一个函数的输入。
如果我们执行此查询,我们将得到以下结果。
{
student_id : "P001",
total: 12
},
{
student_id : "P002"
total: 20
}
在这里,最终结果显示了学生的总数。计算字段是 studentid 和总计。在这个查询中,我们使用了 $match
来限制学生到 A 节。然后,我们按 studentid 对学生进行分组,并计算了课程费用的总和。在这个例子中,我们使用聚合管道转换了 coursefee。
如果我们通过管道表达上述示例,它将呈现为:students
=> $match
=> $group
=> 所需结果。
根据您的需求,您可以在管道的任何阶段添加其他聚合函数。但请记住,在管道开始处放置匹配阶段将限制管道中的文档总数并减少处理时间。
如果将匹配放置在管道的非常开始处,查询还可以利用索引。通常,排序阶段在最后执行表现更好,因为如果放在其他位置,往往意味着额外的计算或聚合可能会影响排序顺序,从而使得排序阶段的输出变得无关紧要。
通过 MongoDB Compass,MongoDB 还允许您图形化地创建您的聚合管道。有关更多信息,请访问 聚合管道构建器。
MongoDB 提供了一系列的操作符,您可以在各种聚合阶段中使用。这些操作符中的每一个都可以用于在聚合管道阶段中构建表达式。
操作符表达式类似于接受参数的函数。通常,这些表达式接受一个参数数组,并具有以下形式
{ <operator> : [ <argument1>, <argument2>, ... ] }
如果您只想使用接受单个参数的操作符,则可以省略数组字段。它将采取以下形式
{ < operator> : <argument> }
这些操作符可用于在聚合管道阶段中构建表达式。(有关每个操作符的更多信息,请访问 聚合管道操作符。)
算术表达式操作符 对数字执行数学运算。
数组表达式操作符 对数组执行操作。
布尔表达式操作符 将其参数表达式评估为布尔值,并返回布尔值作为结果。
比较表达式操作符 返回布尔值,除了 $cmp,它返回数字。
条件表达式运算符帮助构建条件语句。
自定义聚合表达式运算符定义自定义聚合函数。
数据大小运算符返回数据元素的大小。
日期表达式运算符返回日期对象或日期对象的组件。
字面表达式运算符返回一个值,而无需解析。
对象表达式运算符拆分/合并文档。
集合表达式运算符在数组上执行集合操作,将数组视为集合。
字符串表达式运算符对ASCII字符的字符串执行定义良好的行为。
文本表达式运算符允许访问与聚合相关的每个文档的元数据。
三角学表达式运算符在数字上执行三角运算。
类型表达式运算符在数据类型上执行操作。
累加器($group)在文档通过管道时维护状态。
其他阶段的累加器不维护其状态,可以接受单个参数或多个参数作为输入。
变量表达式运算符在子表达式的范围内定义变量,并返回子表达式的结果。
聚合管道的每个阶段都会在文档通过时对其进行转换。然而,一旦输入文档通过一个阶段,它不一定产生一个输出文档。一些阶段可能会生成多个文档作为输出。
MongoDB在mongo shell中提供了db.collection.aggregate()
方法以及db.aggregate()
命令来运行聚合管道。
一个阶段可以在管道中多次出现,但$out
、$merge
和$geoNear
阶段除外。在本文中,我们将简要讨论您在MongoDB中聚合文档时经常遇到的七个主要阶段。有关所有可用阶段的列表,请参阅聚合管道阶段。
$project
$match
$group
$group
消耗所有输入文档,并为每个不同的组输出一个文档。输出文档只包含标识符字段(分组ID
)和,如果有指定,累加字段。$sort
:根据指定的排序键重新排序文档流。文档未经修改,除了文档的顺序。对于每个输入文档,输出将是一个文档。$skip
$limit
$unwind
MongoDB聚合管道的最佳特性之一是它将自动重新塑造查询以提高其性能。话虽如此,以下是一些需要考虑以优化查询性能的事项。
管道阶段有100兆字节的RAM限制。如果一个阶段超出此限制,MongoDB将产生错误。为了允许处理大型数据集,使用allowDiskUse
选项启用聚合管道阶段将数据写入临时文件。请注意,allowDiskUse
将数据存储到磁盘而不是内存中,这可能会降低性能。
db.aggregate()
命令可以返回一个游标或将结果存储在集合中。当返回游标或将结果存储在集合中时,结果集中的每个文档都受BSON文档大小限制的约束,当前为16兆字节;如果任何单个文档超过BSON文档大小限制,则命令将产生错误。
如果您在管道中有多个阶段,了解每个阶段的关联开销总是更好的。例如,如果您的管道中既有$sort
阶段又有$match
阶段,强烈建议您在$sort
之前使用$match
,以最大限度地减少需要排序的文档数量。
那么,我们如何优化MongoDB以提高聚合管道的速度?这取决于数据在嵌入文档中的存储方式。如果您将一百万条记录存储到嵌入文档中,它将在$unwind
等阶段创建性能开销。(为了实现更快的查询,您可能需要了解MongoDB的多键索引和分片方法,特别是如何优化管道。)