聚合管道优化
聚合管道操作包含一个优化阶段,该阶段会尝试重塑管道以提高性能。
要查看优化器如何转换特定的聚合管道,请将 explain
选项纳入 db.collection.aggregate()
方法。
优化可能因版本而异。
除了了解在优化阶段执行的聚合管道优化外,您还将了解如何使用 索引和文档过滤器来提高聚合管道的性能。
您可在用户界面中为 MongoDB Atlas 中托管的部署运行聚合管道。
投影优化
聚合管道可确定是否只需文档中的部分字段即可获取结果。如果是,管道则仅会使用这些字段,从而减少通过管道传递的数据量。
$project
阶段放置
您使用 $project
阶段时,它通常应该是管道的最后一个阶段,用于指定要返回给客户端的字段。
在管道的开头或中间使用 $project
阶段来减少传递到后续管道阶段的字段数量不太可能提高性能,因为数据库会自动执行此优化。
管道序列优化
($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]。有关更多信息,请参阅 $sort
操作符和内存。
注意
使用 $skip 进行序列优化
[1] | 当 allowDiskUse 为 true 并且 n 项超出聚合内存限制时,优化仍将适用。 |
$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
合并
当 $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 } } }
您可以在解释计划中查看此优化后的管道。
基于插槽的查询执行引擎管道优化
满足特定条件时,MongoDB 可以使用基于槽的查询执行引擎来执行某些管道阶段。在大多数情况下,与经典查询引擎相比,基于槽的执行引擎可提供更高的性能并降低 CPU 和内存成本。
要验证是否使用了基于槽的执行引擎,请使用 explain
选项运行聚合。此选项输出有关聚合的查询计划的信息。有关将 explain
与聚合结合使用的详细信息,请参阅返回聚合管道操作的信息。
以下各部分内容:
使用基于槽的执行引擎进行聚合时的条件。
如何验证是否使用了基于槽的执行引擎。
$group
优化
5.2 版本中的新增功能。
从版本 5.2 开始,如果满足以下任一条件,MongoDB 使用基于槽位的执行查询引擎来执行 $group
阶段:
$group
是管道中的第一个阶段。管道中的所有先前阶段也可以由基于槽位的执行引擎执行。
当将基于槽的查询执行引擎用于 $group
时,解释结果包括 queryPlanner.winningPlan.queryPlan.stage:
"GROUP"
。
queryPlanner
对象的位置取决于管道中是否包含无法使用基于槽位的执行引擎执行的 $group
阶段之后的阶段。
如果
$group
是最后一个阶段,或者$group
之后的所有阶段都可以使用基于槽的执行引擎来执行,则queryPlanner
对象位于顶层explain
输出对象 (explain.queryPlanner
) 中。如果管道包含
$group
之后的阶段,而这些阶段无法使用基于槽的执行引擎,则queryPlanner
对象位于explain.stages[0].$cursor.queryPlanner
。
$lookup
优化
6.0 版本中的新功能。
从版本 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
阶段所需的所有字段值,从而使其成为一个覆盖查询。
要确定管道是否使用了索引,请查看查询计划并查找 IXSCAN
计划或 DISTINCT_SCAN
计划。
注意
在某些情况下,查询计划器使用 DISTINCT_SCAN
索引计划,该计划可为每个索引键值返回一个文档。如果每个键值有多个文档,则DISTINCT_SCAN
的执行速度比 IXSCAN
快。但是,索引扫描参数可能会影响 DISTINCT_SCAN
和 IXSCAN
的时间比较。
对于聚合管道的早期阶段,请考虑对查询字段建立索引。可以从索引受益的阶段是:
$match
阶段- 在
$match
阶段,如果$match
是管道中的第一个阶段,则在查询规划器进行任何优化之后,服务器可以使用索引。 $sort
阶段- 在
$sort
阶段,如果该阶段之前没有$project
、$unwind
或$group
阶段,则服务器可以使用索引。 $group
阶段在
$group
阶段,如果该阶段满足以下两个条件,服务器可以使用索引快速查找每个群组中的$first
或$last
文档:请参阅 $group 性能优化,查看示例。
$geoNear
阶段- 服务器始终为
$geoNear
阶段使用索引,因为它需要地理空间索引。
此外,管道中的一些后期阶段在从其他未修改的集合中检索数据时,可以使用这些集合上的索引来实现优化。这些阶段包括:
文档筛选器
如果聚合操作只需要集合中文档的子集,请先过滤文档:
例子
$sort
+$skip
+$limit
序列
管道的阶段序列为:首先为 $sort
,其次为 $skip
,再次为 $limit
:
{ $sort: { age : -1 } }, { $skip: 10 }, { $limit: 5 }
优化器执行 $sort
+ $limit
合并以将此序列转换为以下内容:
{ "$sort" : { "sortKey" : { "age" : -1 }, "limit" : NumberLong(15) } }, { "$skip" : NumberLong(10) }
重新排序后,MongoDB 增加了 $limit
的数量。