聚合管道操作有一个优化阶段,它试图重塑管道以提高性能。查看优化器如何转换特定的聚合管道,请将 explain 选项包含在 db.collection.aggregate() 方法中。
注意:优化可能会在不同版本之间发生变化
投影优化即优化我们应用到管道的数据,减少应用到管道的数据量,一定程度上可以优化性能。
在开始优化之前,你先确定你的聚合管道是否只需要文档中一部分字段就可以获得结果。如果是这样,那么你可以只将需要的字段应用到管道,从而减少通过管道的数据量。这个和优化SQL类似,例如:
select * from users; # 下面性能将更好 select username, password from users;
对于包含一个投影阶段($project 或 $unset 或 $addFields 或 $set)和一个 $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 字段在 $addField 阶段计算,但不依赖于 $project 阶段。优化器为这些字段上的过滤器创建了一个新的 $match 阶段,并将其置于 $project 阶段之前
$match 过滤器 { name: "Joe Schmoe" } 不使用在 $project 或 $addFields 阶段中计算的任何值,因此它在两个投影阶段之前都被移动到一个新的 $match 阶段。
注意:优化之后,过滤器{name: "Joe Schmoe"}在管道开始处处于$match阶段。这还有一个好处,即允许聚合在最初查询集合时在name字段上使用索引。
当您有一个带有 $sort 的序列,然后是 $match 时,$match 将在 $sort 之前移动,以最小化要排序的对象的数量。例如,如果管道由以下阶段组成:
{ $sort: { age : -1 } }, { $match: { status: 'A' } }
在优化阶段,优化器将序列转换为:
{ $match: { status: 'A' } }, { $sort: { age : -1 } }
注意:在执行排序 $sort 之前过滤数据,减少排序的数据量。数据量越少,排序肯定越快。
如果可能的话,当管道的 $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" } } }
新版本3.2
当您有一个带有 $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 时,如果没有中间阶段修改文档的数量(例如:$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个项。
带 $skip 序列优化:
如果在 $sort 阶段和 $limit 阶段之间存在 $skip 阶段,MongoDB 将 $limit 合并到 $sort 阶段,并将 $skip 的值添加到 $limit。
当一个 $limit 后立即跟随另一个 $limit 时,这两个阶段可以合并成一个单一的 $limit,其中限制数量是两个初始限制数量中较小的。例如,管道包含以下序列:
{ $limit: 100 }, { $limit: 10 }
然后,第二个 $limit 阶段可以合并到第一个 $limit 阶段,并导致只有一个 $limit 阶段,其中限制数量10是两个初始限制数量100和10的最小值。
{ $limit: 10 }
当一个 $skip 后立即跟随另一个 $skip 时,这两个阶段可以合并成一个单独的 $skip,其中跳过数量是两个初始跳过数量的总和。例如,管道包含以下序列:
{ $skip: 5 }, { $skip: 2 }
然后,第二个 $skip 阶段可以合并成第一个 $skip 阶段,并导致只有一个 $skip 阶段,其中 跳过数量 7 是两个初始跳过量 5 和 2 的和。
{ $skip: 7 }
当 $match 后立即跟随另一个 $match 时,这两个阶段可以合并为一个 $match,并将条件与 $and 合并。例如,管道包含以下序列:
{ $match: { year: 2014 } }, { $match: { status: "A" } }
然后,第二个 $match 阶段可以合并到第一个 $match 阶段,并导致只有一个 $match 阶段。
{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }
新版本3.2
当 $unwind 后立即跟随另一个 $lookup,并且 $unwind 在 $lookup 的 as 字段上操作时,优化器可以将 $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 } } }
管道包含 $sort 的序列,后面是 $skip,然后是 $limit:
{ $sort: { age : -1 } }, { $skip: 10 }, { $limit: 5 }
优化器执行 $sort+$limit 合并将序列转换为以下内容:
{ "$sort" : { "sortKey" : { "age" : -1 }, "limit" : NumberLong(15) } }, { "$skip" : NumberLong(10) }
MongoDB 通过重新排序增加 $limit 限制数量。