聚合管道优化

在本页面

聚合管道操作具有优化阶段,该阶段尝试重塑管道以提高性能。

若要查看优化程序如何转换特定的聚合管道,请在db.collection.aggregate()方法中包括explain选项。

优化可能会在版本之间进行更改。

Projection Optimization

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

管道 Sequences 优化

$ project 或$ addFields $ match 序列优化

对于包含投影阶段($project$addFields)后跟$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阶段。

Note

经过优化后,过滤器{ 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" } } }

$ skip $ limit 序列优化

当序列中带有$skip后跟$limit时,$limit将移动到$skip之前。通过重新排序,$limit值增加了$skip数量。

例如,如果管道包括以下阶段:

{ $skip: 10 },
{ $limit: 5 }

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

{ $limit: 15 },
{ $skip: 10 }

此优化为$ sort $ limit 合并提供了更多机会,例如$sort $skip $limit序列。有关合并的详细信息,请参见$ sort $ limit 合并;有关示例,请参见$ sort $ skip $ limit 序列

对于sharded collections上的聚合操作,此优化减少了每个分片返回的结果。

$ project $ skip 或$ limit 序列优化

3.2 版中的新功能。

如果您有一个序列,其中$project后跟$skip$limit,则$skip$limit将移动到$project之前。例如,如果管道包括以下阶段:

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

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

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

此优化为$ sort $ limit 合并提供了更多机会,例如$sort $limit序列。有关合并的详细信息,请参见$ sort $ limit 合并

管道合并优化

如果可能,优化阶段将流水线阶段合并到其前身。通常,在任何序列重新排序优化之后*会发生合并。

$ sort $ limit 合并

$sort紧接在$limit之前时,优化器可以将$limit合并为$sort。这样,排序操作就可以在处理过程中仅保留最高的n结果,其中n是指定的限制,而 MongoDB 仅需要将n项存储在内存[1]中。有关更多信息,请参见$ sort 运算符和内存

[1]allowDiskUsetruen项超过聚合内存限制时,优化仍然适用。

$ limit $ limit 合并

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

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

然后,第二个$limit阶段可以合并到第一个$limit阶段,并导致单个$limit阶段,其中限制量10是两个初始限制10010中的最小值。

{ $limit: 10 }

$ skip $ skip 合并

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

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

然后,第二个$skip阶段可以合并到第一个$skip阶段,并导致单个$skip阶段,其中跳过量7是两个初始限制52的总和。

{ $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 合并

3.2 版中的新功能。

$unwind紧随另一个$lookup$unwind$lookupas字段上操作时,优化程序可以将$unwind合并到$lookup阶段。这样可以避免创建较大的中间文档。

例如,管道包含以下序列:

{
  $lookup: {
    from: "otherCollection",
    as: "resultingArray",
    localField: "x",
    foreignField: "y"
  }
},
{ $unwind: "$resultingArray"}

优化程序可以将$unwind阶段合并为$lookup阶段。如果使用explain选项运行聚合,则explain输出将显示合并阶段:

{
  $lookup: {
    from: "otherCollection",
    as: "resultingArray",
    localField: "x",
    foreignField: "y",
    unwinding: { preserveNullAndEmptyArrays: false }
  }
}

Examples

以下示例是一些可以同时利用序列重新排序和合并的序列。通常,在任何序列重新排序优化之后*会发生合并。

$ sort $ skip $ limit 序列

管道包含$sort后跟$skip后跟$limit的序列:

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

首先,优化器执行$ skip $ limit 序列优化将序列转换为以下形式:

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

$ skip $ limit 序列优化通过重新排序增加了$limit的数量。有关详细信息,请参见$ skip $ limit 序列优化

现在,重新排序的序列在$limit之前紧跟着$sort,并且流水线可以合并两个阶段以减少排序操作期间的内存使用量。有关更多信息,请参见$ sort $ limit 合并

$ limit $ skip $ limit $ skip 序列

管道包含交替的$limit$skip阶段的序列:

{ $limit: 100 },
{ $skip: 5 },
{ $limit: 10 },
{ $skip: 2 }

$ skip $ limit 序列优化反转{ $skip: 5 }{ $limit: 10 }阶段的位置并增加限制量:

{ $limit: 100 },
{ $limit: 15},
{ $skip: 5 },
{ $skip: 2 }

然后,优化器将两个$limit阶段合并为一个$limit阶段,并将两个$skip阶段合并为一个$skip阶段。结果序列如下:

{ $limit: 15 },
{ $skip: 7 }

有关详情,请参见$ limit $ limit 合并$跳过$跳过合并