MongoDB聚合管道的优化及限制

00:00/00:00

今天是父亲节,祝天下的父亲节日快乐!送一首超级英雄给老爸~您永远都是我心目中的超级英雄。本周内容:MongoDB聚合管道的优化,聚合管道操作有一个优化阶段,用于重塑管道(重排序以及合并)以提高性能。此外,MongoDB中管道的使用还需要受到结果大小以及内存的限制。接下来,请听我一一道来。

映射的优化

聚合管道可以判断是否只需要文档中的字段子集来获得结果。如果是的话,管道将只使用这些需要的字段,以减少管道中传输的数据量。

管道的序列优化

$sort + $match 序列优化

当我们需要在$sort后面使用$match时,$match将会移动到$sort之前以最小化需要排序的对象数。例如,管道由下面几个阶段组成:

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

在优化阶段,优化器将会将顺序转化为:

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

$skip + $limit 序列优化

当我们需要在$skip后面使用$limit时,$limit将会移动到$skip之前。重新排序的话,$limit的值将会加上$skip的量。

例如,如果管道包含以下几个阶段:

  1. { $skip: 10 },
  2. { $limit: 5 }

在优化阶段,优化器将会将顺序转化为:

  1. { $limit: 15 },
  2. { $skip: 10 }

对于$sort+$limit的同时使用可以有更多的优化,例如,$sort+$limit+$limit序列。

在分片集合上进行聚合操作时,该优化将会减少从每个分片中返回的结果数。

$redact+$match序列优化

在可能的情况下,当管道在$redact阶段之后马上跟着$match阶段的时候,聚合有时候可以在$redact阶段之前增加$match阶段的一个部分。如果增加的$match阶段在一个管道的开头部分,聚合就可以使用索引以及查询集合来限制进入到管道的文档数目。
例如,如果管道由下面几个阶段组成:

  1. { $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
  2. { $match: { year: 2014, category: { $ne: "Z" } } }

优化器可以在$redact阶段之前增加相同的$match阶段:

  1. { $match: { year: 2014 } },
  2. { $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
  3. { $match: { year: 2014, category: { $ne: "Z" } } }

$project+$skip或者$limit序列优化

从3.2版本开始
当我们需要在$project阶段之后使用$skip或者$limit时,$skil$limit将会移动到$project之前。例如,如果管道由以下阶段组成:

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

在优化的阶段,优化器将会将顺序转化为:

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

该优化可以对$sort+$limit的合并使用进行更多的优化,例如$sort+$limit顺序。

管道合并优化

在可能的情况下,优化阶段会将一个管道阶段合并到前驱任务中。一般说来,合并存在于任何序列重排序优化之后

$sort+$limit合并

$sort操作之后马上跟着一个$limit的时候,优化器可以将$limit合并到$sort中。在处理的过程中,该操作允许排序操作只维护前n个结果,其中n是特定的限制数目。MongoDB只需要在内存中存储n条记录。

注意:该优化只会在allowDiskUsetrue,并且n条记录超过聚合内存限制时才会应用。

$limit+$limit合并

$limit操作之后马上跟着另一个$limit操作时,这两个阶段可以合并为一个单一的$limit,其中,返回的数目为两个原始返回数目中较小的那一个。例如,一个管道包含下列顺序:

  1. { $limit: 100 },
  2. { $limit: 10 }

然后第二个$limit阶段就可以合并到第一个$limit阶段,形成一个$limit阶段,其中返回记录数为10,是原始返回数10010中较小的那一个。

$skip+$skip合并

$skip操作之后马上跟着另一个$skip操作时,这两个阶段可以合并为一个单一的$skip,其中,返回的数目为两个原始返回数目之和。例如,一个管道包含下列顺序:

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

然后第二个$skip阶段就可以合并到第一个$skip阶段,形成一个$skip阶段,其中返回记录数为7,是原始返回数52之和。

  1. { $skip: 7 }

$match+$match合并

$match操作之后马上跟着另一个$match操作时,这两个阶段可以合并为一个单一的$match,由一个$and操作符组合这两个条件。例如,一个管道包含下列顺序:

  1. { $match: { year: 2014 } },
  2. { $match: { status: "A" } }

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

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

$lookup+$unwind合并

从3.2版本开始
$unwind阶段的前面是一个$lookup操作的时候,$unwind操作符在$lookup的字段上进行操作时,优化器可以合并$unwind阶段到$lookup阶段。这样就避免了创建大量的中间文档。

例如,一个管道包含下面的序列:

  1. {
  2. $lookup: {
  3. from: "otherCollection",
  4. as: "resultingArray",
  5. localField: "x",
  6. foreignField: "y"
  7. }
  8. },
  9. { $unwind: "$resultingArray"}

优化器可以合并$unwind阶段到$lookup阶段。如果我们使用explain选项运行聚合的话,explain输出将会显示合并的阶段:

  1. {
  2. $lookup: {
  3. from: "otherCollection",
  4. as: "resultingArray",
  5. localField: "x",
  6. foreignField: "y",
  7. unwinding: { preserveNullAndEmptyArrays: false }
  8. }
  9. }

示例

下面的例子是一些可以利用序列重排序和合并的顺序。一般说来,合并存在于所有的重排序优化之后

$sort+$skip+$limit顺序

一个管道包含$sort+$skip+$limit序列:

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

首先,优化器执行$skip+$limit顺序优化增加$limit的数目。
重排序的序列目前在$sort之后紧跟的时$limit操作,而管道可以合并这两个阶段来减少排序操作过程中的内存使用。

$limit+$skip+$skip+$limit序列

一个管道包含交替的$limit$skip阶段:

  1. { $limit: 100 },
  2. { $skip: 5 },
  3. { $limit: 10 },
  4. { $skip: 2 }

$skip+ $limit序列优化保留了{$skip:5}{$limit:10}阶段,并且增加了返回数目:

  1. { $limit: 100 },
  2. { $limit: 15},
  3. { $skip: 5 },
  4. { $skip: 2 }

然后,优化器合并这两个$limit阶段到一个单一的$limit阶段,而两个$skip阶段合并为一个单一的$skip阶段,结果序列如下:

  1. { $limit: 15 },
  2. { $skip: 7 }

 

聚合管道的限制

使用aggregate命令的聚合操作有以下限制:

结果大小限制

从2.6版本中开始修改
从MongoDB 2.6版本开始。aggregate命令可以返回一个游标或者在集合中存储结果。在返回一个游标或者将结果存储在集合中时,结果集中的每个文旦满足BSON文档大小的限制,目前为16MB。如果任何一个单一文档大小超过了BSON文档大小限制,该命令将会返回一个错误。这个限制只适用于返回的文档,而在管道处理的过程中,文档大小有可能会超过这个大小。从2.6版本开始,db.collection.aggregate()方法默认返回一个游标。

如果我们不指定游标选项或者将结果存储在一个集合中,aggregate命令返回一个单一的BSON文旦,包含结果集中的一个字段。这样的话,如果返回结果的总大小超过了BSON文档大小的限制,该命令将会返回一个错误。

aggregate命令的早起版本中,只可以返回一个包含结果集的BSON文档。如果结果集的总大小超过了BSON文档限制,将会报错。

内存限制

从2.6版本中开始修改
管道阶段有100MB内存大小的限制。如果某个阶段超过了这个限制,MongoDB将会报错。为了能够处理更大的数据库,我们可以使用allowDiskUse这个选项来允许聚合管道阶段来将数据写入到临时文件。

从3.4版本中开始修改
$graphLookup阶段必须限制在100MB内存以内。即使是在aggregate()操作中指定了allowDiskUser:true的选项,$graphLookup阶段也会忽略这个选项。如果aggregate()操作中有别的阶段,allowDiskUser:true选项对这些其他阶段有效。

打赏

mickey

记录生活,写给几十年后的自己。