认识
一、认识
MongoDB
聚合 Aggregation
操作处理多个文档并返回计算结果。若要执行聚合操作, 我们可以通过以下方式:
-
单一聚合(
Single Purpose Aggregation
) -
Map-Reduce
聚合 -
聚合管道(
Aggregation Pipeline
)
我们通常使用 聚合管道 Aggregation Pipeline
来完成聚合操作。聚合管道 由一个或多个处理文档的阶段组成, 每个阶段对输入文档执行一个操作, 从一个阶段输出的文档将传递到下一阶段, 一个聚合管道可以返回针对文档组的结果, 通过多个阶段对数据进行转换和汇总, 从而得到复杂查询的结果。常用的聚合阶段有: $match
, 类似 SQL
中的 WHERE
子句, 用于过滤文档; $group
, 类似 SQL
中的 GROUP BY
, 通过指定分组字段来对文档进行分组,并可在每组上执行聚合运算(如求和、计数、平均等); $project
, 用于重塑输出文档的结构,可以添加、修改或删除字段,相当于选择性地返回数据; $sort
, 对数据进行排序,类似 SQL
的 ORDER BY
; $limit
和 $skip
, 用于控制结果集的大小和分页操作; $lookup
, 实现跨集合的连接查询,可以将来自其他集合的数据合并到当前聚合管道中, 类似于 SQL JOIN
,但比 SQL
连接慢,因为 MongoDB
不是关系型数据库,不擅长跨集合查询; $facet
, 允许在同一个聚合查询中进行多个并行的数据处理管道,适用于需要同时获得多种统计结果的场景。
聚合管道 Aggregation Pipeline
优化: 优化的目标是降低管道中数据传输的数量, 移动 $match
以减少不必要的数据处理, 合并可以合并的阶段, 让索引尽可能早地生效。虽然, 我们 MongoDB
内部已经做了许多优化工作, 我们可以基于 MongoDB
内部的优化策略, 我们在代码中就就已经将优化做到极致, 这样我们不仅可以节省 MongoDB
进一步的优化工作, 也提高了我们的代码质量。可以通过 db.collection.aggregate()
方法中添加 explain
选项, 查看优化后的管道。 我们可以在阶段顺序上, $match
应尽早执行,减少后续阶段的数据量, $group
尽量靠后,避免不必要的计算, $project
一般在管道的最后,控制返回的字段; 合并可合并的阶段; 另外, 我们可以增加索引, 确保 $match
和 $lookup
关联字段有索引,否则会导致全表扫描; 我们可以使用 $facet
来实现并行统计; 可以结合 $merge
来进行 ETL
任务, 提高查询效率。在 MongoDB
中,$facet
是聚合管道的一个阶段,它允许你在同一个聚合查询中并行运行多个子管道, 可以并行处理, 每个子管道可以独立处理相同的一组输入文档,并生成各自的输出。这样你就可以一次性获得多种不同角度的聚合结果,而无需多次查询。 注意: $facet
在管道中的第一个阶段, 不会使用索引, 所以, 不要将 $facet
放到第一阶段。
-
策略一、投影优化: 聚合管道可确定是否只需文档中的部分字段即可获取结果。如果是,管道则仅会使用这些字段,从而减少通过管道传递的数据量。比如:
$project
阶段放置, 使用$project
阶段时,它通常应该是管道的最后一个阶段,用于指定要返回给客户端的字段。在管道的开头或中间使用$project
阶段来减少传递到后续管道阶段的字段数量不太可能提高性能,因为数据库会自动执行此优化。因此,$project
一般在管道的最后,控制返回的字段, 在前面放置$project
可能无效,MongoDB
会自动优化。 -
策略二、管道序列优化:
$match
应尽早执行,减少后续阶段的数据量。$group
尽量靠后,避免不必要的计算。-
(
$project
、$unset
、$addFields
或$set
)+$match
序列优化: 如果聚合管道包含投影阶段 ($addFields
、$project
、$set
或$unset
),且其后跟随$match
阶段,MongoDB
会将$match
阶段中无需使用投影阶段计算的值的所有过滤器移动到投影前的新的$match
阶段。如果聚合管道包含多个投影或$match
阶段,MongoDB
会对每个$match
阶段执行此优化,将每个$match
过滤器移到过滤器不依赖的所有投影阶段之前。{
$addFields: {
maxTime: { $max: "$times" },
minTime: { $min: "$times" }
}
},
{
$project: {
_id: 1,
name: 1,
times: 1,
maxTime: 1,
minTime: 1,
avgTime: { $avg: ["$maxTime", "$minTime"] }
}
},
{
$match: {
name: "Joe Schmoe",
maxTime: { $lt: 20 },
minTime: { $gt: 5 },
avgTime: { $gt: 7 }
}
}优化器会将
$match
阶段分解为四个单独的过滤器,每个过滤器对应$match
查询文档中的一个键。然后,优化器会将每个过滤器移至尽可能多的投影阶段之前,从而按需创建新的$match
阶段。{ $match: { name: "Joe Schmoe" } },
{ $addFields: {
maxTime: { $max: "$times" },
minTime: { $min: "$times" }
} },
{ $match: { maxTime: { $lt: 20 }, minTime: { $gt: 5 } } },
{ $project: {
_id: 1, name: 1, times: 1, maxTime: 1, minTime: 1,
avgTime: { $avg: ["$maxTime", "$minTime"] }
} },
{ $match: { avgTime: { $gt: 7 } } }$match
筛选器{ avgTime: { $gt: 7 } }
依赖$project
阶段来计算avgTime
字段。$project
阶段是该管道中的最后一个投影阶段,因此avgTime
上的$match
筛选器无法移动。maxTime
和minTime
字段在$addFields
阶段计算,但不依赖$project
阶段。优化器已为这些字段上的筛选器创建一个新的$match
阶段,并将其置于$project
阶段之前。$match
筛选器{ name: "Joe Schmoe" }
不使用在$project
或$addFields
阶段计算的任何值,因此它在这两个投影阶段之前移到了新的$match
阶段。优化后,筛选器
{ name: "Joe Schmoe" }
在管道开始时会处于 $match 阶段。此举还允许聚合在最初查询该集合时使用针对name
字段的索引。 -
$sort
+$match
序列优化: 当序列中的$sort
后面是$match
时,$match
会在$sort
之前移动,以最大限度地减少要排序的对象数量。例如,如果管道由以下阶段组成:{ $sort: { age : -1 } },
{ $match: { status: 'A' } }在优化阶段,优化器会将序列转换为以下内容:
{ $match: { status: 'A' } },
{ $sort: { age : -1 } } -
$redact
+$match
序列优化: 如果可能,当管道有$redact
阶段紧接着$match
阶段时,聚合有时可以在$redact
阶段之前添加$match
阶段的一部分。如果添加的$match
阶段位于管道的开头,则聚合可以使用索引并查询集合以限制进入管道的文档数量。例如,如果管道由以下阶段组成:{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }优化器可以在
$redact
阶段之前添加相同的$match
阶段:{ $match: { year: 2014 } },
{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } } -
$project/$unset + $skip
序列优化: 如果序列中的$project
或$unset
后面是$skip
,则$skip
在$project
之前移动。例如,如果管道由以下阶段组成:{ $sort: { age : -1 } },
{ $project: { status: 1, name: 1 } },
{ $skip: 5 }在优化阶段,优化器会将序列转换为以下内容:
{ $sort: { age : -1 } },
{ $skip: 5 },
{ $project: { status: 1, name: 1 } }
-
-
策略三、管道合并优化: 在可能的情况下,优化阶段将管道阶段合并到其前置阶段中。通常,合并发生在任何序列重新排序优化之后。
-
$sort + $limit
合并: 当$sort
在$limit
之前时,,the optimizer can coalesce the into the
如果没有干预阶段(例如$unwind
、$group
)修改文档的数量,则优化器可以将$limit
阶段合并到$sort
。如果有管道阶段更改了$sort
和$limit
阶段之间的文档数量,则MongoDB
不会将$limit
合并到$sort
中。例如,如果管道由以下阶段组成:{ $sort : { age : -1 } },
{ $project : { age : 1, status : 1, name : 1 } },
{ $limit: 5 }在优化阶段,优化器会将此序列合并为以下内容:
{
"$sort" : {
"sortKey" : {
"age" : -1
},
"limit" : NumberLong(5)
}
},
{ "$project" : {
"age" : 1,
"status" : 1,
"name" : 1
}
}此操作可让排序操作在推进时仅维护前
n
个结果,其中n
为指定的限制,而MongoDB
仅需要在内存中存储n
个项目[1]
。 -
$limit + $limit
合并: 当$limit
紧随另一个$limit
时,这两个阶段可以合并为一个$limit
,以两个初始限额中较小的为合并后的限额。例如,一个管道包含以下序列:{ $limit: 100 },
{ $limit: 10 }然后第二个
$limit
阶段可以合并到第一个$limit
阶段,形成一个$limit
阶段,新阶段的限额10
是两个初始限额100
和10
中的较小者。{ $limit: 10 }
-
$skip + $skip
合并: 当$skip
紧随在另一个$skip
之后时,这两个阶段可以合并为一个$skip
,其中的跳过数量是两个初始跳过数量的总和。例如,一个管道包含以下序列:{ $skip: 5 },
{ $skip: 2 }然后第二个
$skip
阶段可以合并到第一个$skip
阶段,形成一个$skip
阶段,新阶段的跳过数量7
是两个初始限额5
和2
的总和。{ $skip: 7 }
-
$match + $match
合并: 当$match
紧随另一个$match
之后时,这两个阶段可以合并为一个$match
,用$and
将条件组合在一起。例如,一个管道包含以下序列:{ $match: { year: 2014 } },
{ $match: { status: "A" } }然后第二个
$match
阶段可合并到第一个$match
阶段并形成一个$match
阶段{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }
-
$lookup、$unwind 和 $match Coalescence
: 当$unwind
紧随$lookup
,且$unwind
在$lookup
的as
字段上运行时,优化器将$unwind
合并到$lookup
阶段。这样可以避免创建大型中间文档。此外,如果$unwind
后接$lookup
的任意as
子字段上的$match
,则优化器也会合并$match
。例如,一个管道包含以下序列:{
$lookup: {
from: "otherCollection",
as: "resultingArray",
localField: "x",
foreignField: "y"
}
},
{ $unwind: "$resultingArray" },
{ $match: {
"resultingArray.foo": "bar"
}
}优化器 将
$unwind
和$match
阶段合并到$lookup
阶段。如果使用explain
选项运行聚合,explain
输出将显示合并阶段:{
$lookup: {
from: "otherCollection",
as: "resultingArray",
localField: "x",
foreignField: "y",
let: {},
pipeline: [
{
$match: {
"foo": {
"$eq": "bar"
}
}
}
],
unwinding: {
"preserveNullAndEmptyArrays": false
}
}
}在之前的
explain
输出中显示的unwinding
字段与$unwind
阶段不同。unwinding
字段显示了该管道如何在内部进行优化。$unwind
阶段会从输入文档解构数组字段,并为每个元素输出文档。
-
-
策略四、内存限制优化:
$group
阶段: 如果$group
阶段超过100
兆字节RAM
,MongoDB
会将数据写入临时文件。但是,如果将allowDiskUse
选项设置为false
,$group
将返回错误。因此, 对于大数据量的操作,注意内存使用情况,并可以通过设置allowDiskUse: true
来允许使用磁盘进行临时存储,避免内存溢出。
-
策略五、基于插槽的查询执行引擎管道优化: 满足特定条件时,
MongoDB
可以使用基于槽的查询执行引擎来执行某些管道阶段。在大多数情况下,与经典查询引擎相比,基于槽的执行引擎可提供更高的性能并降低CPU
和内存成本。要验证是否使用了基于槽的执行引擎,请使用explain
选项运行聚合。此选项输出有关聚合的查询计划的信息。-
group
优化: 从版本5.2
开始,如果满足以下任一条件,MongoDB
使用基于槽位的执行查询引擎来执行$group
阶段: 1.$group
是管道中的第一个阶段; 2. 管道中的所有先前阶段也可以由基于槽位的执行引擎执行。当将基于槽的查询执行引擎用于$group
时,解释结果包括queryPlanner.winningPlan.queryPlan.stage: "GROUP"
。queryPlanner
对象的位置取决于管道中是否包含无法使用基于槽位的执行引擎执行的$group
阶段之后的阶段。总之,$group
尽量靠后,避免不必要的计算。-
如果
$group
是最后一个阶段,或者$group
之后的所有阶段都可以使用基于槽的执行引擎来执行,则queryPlanner
对象位于顶层explain
输出对象 (explain.queryPlanner
) 中。 -
如果管道包含
$group
之后的阶段,而这些阶段无法使用基于槽的执行引擎,则queryPlanner
对象位于explain.stages[0].$cursor.queryPlanner
。
-
-
$lookup
优化: 从版本6.0
开始,MongoDB
可以使用基于槽的执行查询引擎执行$lookup
阶段,前提是在此管道中,前面的所有阶段也可以由基于槽的执行引擎来执行,并且以下条件都不成立:-
$lookup
操作在联接集合上执行管道。要查看此类操作的示例,请参阅联接集合上的连接条件和子查询。 -
$lookup
的localField
或foreignField
指定数字成分。例如:{ localField: "restaurant.0.review" }
。 -
管道中任何
$lookup
的from
字段指定视图或分片集合。
当将基于槽的查询执行引擎用于
$lookup
时,解释结果包括queryPlanner.winningPlan.queryPlan.stage: "EQ_LOOKUP"
。EQ_LOOKUP
表示 相等查询。queryPlanner
对象的位置取决于管道中是否包含无法使用基于槽位的执行引擎执行的$lookup
阶段之后的阶段。-
如果
$lookup
是最后一个阶段,或者$lookup
之后的所有阶段都可以使用基于槽的执行引擎来执行,则queryPlanner
对象位于顶层explain
输出对象 (explain.queryPlanner
) 中。 -
如果管道包含
$lookup
之后的阶段,而这些阶段无法使用基于槽的执行引擎,则queryPlanner
对象位于explain.stages[0].$cursor.queryPlanner
。
-
-
-
策略六、使用索引和文档筛选器来提高性能:
-
索引: 聚合管道可以使用输入集合中的索引来提高性能。使用索引可限制阶段处理的文档数量。理想情况下,索引可以覆盖阶段查询。覆盖查询性能特别高,因为索引返回所有匹配的文档。例如,由
$match
、$sort
、$group
组成的管道可以从每个阶段的索引中受益:-
$match
查询字段上的索引可高效地识别相关数据 -
针对排序字段的索引会按排序顺序为
$sort
阶段返回数据 -
与
$sort
顺序匹配的针对分组字段的索引则会返回$group
阶段所需的所有字段值,从而使其成为一个覆盖查询。
对于聚合管道的早期阶段,请考虑对查询字段建立索引。可以从索引受益的阶段是:
-
$match
阶段: 在$match
阶段,如果$match
是管道中的第一个阶段,则在查询规划器进行任何优化之后,服务器可以使用索引。确保$match
关联字段有索引,否则会导致全表扫描。 -
$sort
阶段: 在$sort
阶段,如果该阶段之前没有$project
、$unwind
或$group
阶段,则服务器可以使用索引。 -
$group
阶段: 在$group
阶段,如果该阶段满足以下两个条件,服务器可以使用索引快速查找每个群组中的$first
或$last
文档: 管道sorts
和groups
按照同一字段执行;$group
阶段仅使用$first
或$last
累加器运算符。优化以返回每个群组的第一份或最后一份文档, 如果同一字段和$group
阶段的管道sorts
和groups
仅使用$first
或$last
累加器操作符,请考虑在分组字段上添加与排序顺序匹配的索引。在某些情况下,$group
阶段可以使用索引来快速找到每组的第一个文档。 -
$geoNear
阶段: 服务器始终为$geoNear
阶段使用索引,因为它需要地理空间索引。 -
$lookup
阶段: 确保$lookup
关联字段有索引,否则会导致全表扫描。
总而言之, 确保
$match
和$lookup
关联字段有索引,否则会导致全表扫描。 -
-
文档筛选器: 如果聚合操作只需要集合中文档的子集,请先过滤文档:
-
使用
$match
、$limit
和$skip
阶段来限制进入管道的文档。 -
在可能的情况下,将
$match
放在管道的开头,以使用索引扫描集合中的匹配文档。 -
管道开头的
$match
后面跟上$sort
等同于带有排序的单个查询,并且可以使用索引。
-
-