Docs 菜单

Docs 主页开发应用程序MongoDB Manual

聚合管道优化

在此页面上

  • 投影优化
  • 管道序列优化
  • 管道合并优化
  • 基于插槽的查询执行引擎管道优化
  • 使用索引和文档筛选器来提高性能
  • 例子

聚合管道操作包含一个优化阶段,该阶段会尝试重塑管道以提高性能。

要查看优化器如何转换特定的聚合管道,请将 explain 选项纳入 db.collection.aggregate() 方法。

优化可能因版本而异。

除了了解在优化阶段执行的聚合管道优化之外,您还将了解如何使用索引和文档筛选器提高聚合管道性能。请参阅使用索引和文档筛选器提高性能。

聚合管道可以确定它是否只需要文档中字段的子集来获取结果。如果是这样,管道将仅使用那些必填字段,从而减少通过管道的数据量。

您使用 $project 阶段时,它通常应该是管道的最后一个阶段,用于指定要返回给客户端的字段。

在管道的开头或中间使用 $project 阶段来减少传递到后续管道阶段的字段数量不太可能提高性能,因为数据库会自动执行此优化。

如果聚合管道包含投影阶段 ($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 筛选器无法移动。

maxTimeminTime 字段在 $addFields 阶段计算,但不依赖 $project 阶段。优化器已为这些字段上的筛选器创建一个新的 $match 阶段,并将其置于 $project 阶段之前。

$match 筛选器 { name: "Joe Schmoe" } 不使用在 $project$addFields 阶段计算的任何值,因此它在这两个投影阶段之前移到了新的 $match 阶段。

优化后,过滤器{ name: "Joe Schmoe" }在管道开始时处于$match阶段。这样做的另一个好处是,允许聚合在最初查询collection时使用name字段上的索引。

当序列中的 $sort 后面是 $match 时,$match 会在 $sort 之前移动,以最大限度地减少要排序的对象数量。例如,如果管道由以下阶段组成:

{ $sort: { age : -1 } },
{ $match: { status: 'A' } }

在优化阶段,优化器会将序列转换为以下内容:

{ $match: { status: 'A' } },
{ $sort: { age : -1 } }

如果可能,当管道的$redact 阶段后紧跟$match 阶段时,聚合有时会在 阶段之前添加$match $redact阶段的一部分。如果添加的 阶段位于管道的开头,则聚合可以使用索引并查询集合以限制进入管道的文档数量。有关更多信息,请参阅$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,则 $skip$project 之前移动。例如,如果管道由以下阶段组成:

{ $sort: { age : -1 } },
{ $project: { status: 1, name: 1 } },
{ $skip: 5 }

在优化阶段,优化器会将序列转换为以下内容:

{ $sort: { age : -1 } },
{ $skip: 5 },
{ $project: { status: 1, name: 1 } }

在可能的情况下,优化阶段将管道阶段合并到其前置阶段中。通常,合并发生在任何序列重新排序优化之后

4.0 版本中的更改

$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 进行序列优化

如果在 和$skip $sort$limit阶段之间有一个 阶段,MongoDB 会将$limit 合并到$sort 阶段,并将$limit 值增加$skip 量。有关示例,请参阅$sort +$skip +$limit 序列

[1]allowDiskUsetrue 并且 n 项超出聚合内存限制时,优化仍将适用。

$limit 紧随另一个 $limit 时,这两个阶段可以合并为一个 $limit,以两个初始限额中较小的为合并后的限额。例如,一个管道包含以下序列:

{ $limit: 100 },
{ $limit: 10 }

然后第二个 $limit 阶段可以合并到第一个 $limit 阶段,形成一个 $limit 阶段,新阶段的限额 10 是两个初始限额 10010 中的较小者。

{ $limit: 10 }

$skip 紧随在另一个 $skip 之后时,这两个阶段可以合并为一个 $skip,其中的跳过数量是两个初始跳过数量的总和。例如,一个管道包含以下序列:

{ $skip: 5 },
{ $skip: 2 }

然后第二个 $skip 阶段可以合并到第一个 $skip 阶段,形成一个 $skip 阶段,新阶段的跳过数量 7 是两个初始限额 52 的总和。

{ $skip: 7 }

$match 紧随另一个 $match 之后时,这两个阶段可以合并为一个 $match,用 $and 将条件组合在一起。例如,一个管道包含以下序列:

{ $match: { year: 2014 } },
{ $match: { status: "A" } }

然后第二个 $match 阶段可合并到第一个 $match 阶段并形成一个 $match 阶段

{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }

$unwind $lookup紧跟在 之后,并且$unwindas$lookup 字段进行操作时,优化器会将$unwind 合并到$lookup 阶段。这样可以避免创建大型中间文档。此外,如果在$unwind $match的任何as 子字段上,$lookup 后跟 ,则优化器也会合并$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与聚合结合使用的更多信息,请参阅有关聚合管道操作的返回信息。

以下各部分内容:

  • 使用基于槽位的引擎进行聚合时的条件。

  • 如何验证是否使用了基于插槽的引擎。

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中。

6.0 版本中的新功能

从版本6 开始。 ,如果管道中的0 所有$lookup 前面的阶段也可以由基于槽的引擎执行,并且以下条件都不成立,则 MongoDB 可以使用基于槽的执行查询引擎执行 阶段:

  • $lookup 操作在联接集合上执行管道。要查看此类操作的示例,请参阅联接集合上的连接条件和子查询

  • $lookuplocalFieldforeignField 指定数字成分。例如:{ localField: "restaurant.0.review" }

  • 管道中任何 $lookupfrom 字段指定视图或分片集合。

当将基于槽的查询执行引擎用于 $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_SCANIXSCAN 的时间比较。

对于聚合管道的早期阶段,请考虑对查询字段建立索引。可以从索引受益的阶段是:

$match 阶段
如果$match是管道中的第一阶段,则在查询规划器进行任何优化之后,可以使用索引来筛选文档。
$sort 阶段
只要$sort前面没有$project$unwind$group阶段,就可以从索引中受益。
$group 阶段

$group 如果满足以下所有条件,则可以使用索引查找每组中的第一个文档:

  • $sort阶段对$group之前的分组字段进行排序

  • 存在与分组字段的排序顺序匹配的索引

  • $first$group阶段中唯一的累加器

请参阅 $group 性能优化,查看示例。

$geoNear 阶段
$geoNear始终使用索引,因为它必须是管道中的第一阶段并且需要地理空间索引。

此外,管道中的一些后期阶段在从其他未修改的集合中检索数据时,可以使用这些集合上的索引来实现优化。这些阶段包括:

如果聚合操作只需要集合中文档的子集,请先过滤文档:

  • 使用 $match$limit$skip 阶段来限制进入管道的文档。

  • 在可能的情况下,将 $match 放在管道的开头,以使用索引扫描集合中的匹配文档。

  • 管道开头的 $match 后面跟上 $sort 等同于带有排序的单个查询,并且可以使用索引。

管道的阶段序列为:首先为 $sort,其次为 $skip,再次为 $limit

{ $sort: { age : -1 } },
{ $skip: 10 },
{ $limit: 5 }

优化器执行$sort + $limit合并以将序列转换为以下内容:

{
"$sort" : {
"sortKey" : {
"age" : -1
},
"limit" : NumberLong(15)
}
},
{
"$skip" : NumberLong(10)
}

重新排序后,MongoDB 增加了 $limit 的数量。

提示

另请参阅:

← 聚合管道 (Aggregation Pipeline)