当你开始使用 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对学生进行分组,并计算了课程费用的总和。在这个示例中,我们使用聚合管道来转换课程费用。
如果我们通过管道表达上述示例,它将如下所示: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聚合管道的最佳特性之一是它自动重新塑形查询以提高其性能。话虽如此,以下是一些考虑以优化查询性能的事项。
管道阶段有100MB的RAM限制。如果某个阶段超过此限制,MongoDB将产生错误。为了处理大数据集,使用allowDiskUse
选项启用聚合管道阶段将数据写入临时文件。请注意,allowDiskUse
会将数据存储到磁盘而不是内存中,这可能会导致性能较慢。
db.aggregate()
命令可以返回游标或将结果存储在集合中。当返回游标或将结果存储在集合中时,结果集中的每个文档都受BSON文档大小限制,目前为16MB;如果任何单个文档超过BSON文档大小限制,该命令将产生错误。
如果你的管道中有多个阶段,了解每个阶段的开销总是更好的。例如,如果你在管道中同时有$sort
和$match
阶段,强烈建议你在$sort
之前使用$match
,以最小化需要排序的文档数量。
那么,我们如何优化MongoDB以加快聚合管道?这取决于数据在嵌入文档中的存储方式。如果你将一百万条记录存储到嵌入文档中,它将在如$unwind
的阶段中产生性能开销。(为了更快的查询,你可能需要查看MongoDB的多键索引和分片方法,以及特别了解如何优化管道。)