【问题标题】:Aggregate $lookup Total size of documents in matching pipeline exceeds maximum document size聚合 $lookup 匹配管道中文档的总大小超过最大文档大小
【发布时间】:2018-01-25 06:13:31
【问题描述】:

我有一个非常简单的$lookup 聚合查询,如下所示:

{'$lookup':
 {'from': 'edge',
  'localField': 'gid',
  'foreignField': 'to',
  'as': 'from'}}

当我在有足够文档的匹配项上运行此程序时,我收到以下错误:

Command failed with error 4568: 'Total size of documents in edge
matching { $match: { $and: [ { from: { $eq: "geneDatabase:hugo" }
}, {} ] } } exceeds maximum document size' on server

所有限制文档数量的尝试都失败了。 allowDiskUse: true 什么都不做。发送 cursor in 没有任何作用。将$limit 添加到聚合中也会失败。

这怎么可能?

然后我再次看到错误。 $match$and$eq 是从哪里来的?幕后的聚合管道是否将 $lookup 调用转移到另一个聚合,它自己运行,我无法提供限制或使用游标??

这是怎么回事?

【问题讨论】:

  • $lookup 之后直接添加$unwind。这实际上改变了$lookup 的行为方式。还要注意实际使用的是哪个 MongoDB 版本,并指定发出的整个命令。我知道您似乎在说“这只是 $lookup”,但在上下文中实际看到这一点会有所帮助。

标签: mongodb aggregation-framework


【解决方案1】:

我对流动的 Node.js 查询有同样的问题,因为“赎回”集合有超过 400,000 个数据。我正在使用 Mongo DB 服务器 4.2 和 Node JS 驱动程序 3.5.3。

db.collection('businesses').aggregate(
    { 
        $lookup: { from: 'redemptions', localField: "_id", foreignField: "business._id", as: "redemptions" }
    },      
    {
        $project: {
            _id: 1,
            name: 1,            
            email: 1,               
            "totalredemptions" : {$size:"$redemptions"}
        }
    }

我对查询进行了如下修改,使其运行速度超快。

db.collection('businesses').aggregate(query,
{
    $lookup:
    {
        from: 'redemptions',
        let: { "businessId": "$_id" },
        pipeline: [
            { $match: { $expr: { $eq: ["$business._id", "$$businessId"] } } },
            { $group: { _id: "$_id", totalCount: { $sum: 1 } } },
            { $project: { "_id": 0, "totalCount": 1 } }
        ],
        as: "redemptions"
    }, 
    {
        $project: {
            _id: 1,
            name: 1,            
            email: 1,               
            "totalredemptions" : {$size:"$redemptions"}
        }
    }
}

【讨论】:

    【解决方案2】:

    正如前面在评论中所说,发生错误是因为在执行$lookup 时,默认情况下会根据外部集合的结果在父文档中生成一个目标“数组”,为该数组选择的文档的总大小会导致父级超过16MB BSON Limit.

    此计数器将使用紧跟在$lookup 管道阶段之后的$unwind 进行处理。这实际上改变了$lookup 的行为,因此结果不是在父级中生成一个数组,而是每个匹配的文档的每个父级的“副本”。

    $unwind 的常规用法非常相似,只是unwinding 操作实际上添加到$lookup 管道操作本身,而不是作为“单独的”管道阶段处理。理想情况下,您还可以在$unwind 之后使用$match 条件,这也将创建一个matching 参数以添加到$lookup。您实际上可以在管道的explain 输出中看到这一点。

    核心文档中的Aggregation Pipeline Optimization 部分实际上(简要地)介绍了该主题:

    $lookup + $unwind 合并

    3.2 版中的新功能。

    当 $unwind 紧跟在另一个 $lookup 之后,并且 $unwind 在 $lookup 的 as 字段上运行时,优化器可以将 $unwind 合并到 $lookup 阶段。这样可以避免创建大型中间文档。

    最好通过创建超过 16MB BSON 限制的“相关”文档来使服务器承受压力的清单进行演示。尽可能简短地打破和解决 BSON 限制:

    const MongoClient = require('mongodb').MongoClient;
    
    const uri = 'mongodb://localhost/test';
    
    function data(data) {
      console.log(JSON.stringify(data, undefined, 2))
    }
    
    (async function() {
    
      let db;
    
      try {
        db = await MongoClient.connect(uri);
    
        console.log('Cleaning....');
        // Clean data
        await Promise.all(
          ["source","edge"].map(c => db.collection(c).remove() )
        );
    
        console.log('Inserting...')
    
        await db.collection('edge').insertMany(
          Array(1000).fill(1).map((e,i) => ({ _id: i+1, gid: 1 }))
        );
        await db.collection('source').insert({ _id: 1 })
    
        console.log('Fattening up....');
        await db.collection('edge').updateMany(
          {},
          { $set: { data: "x".repeat(100000) } }
        );
    
        // The full pipeline. Failing test uses only the $lookup stage
        let pipeline = [
          { $lookup: {
            from: 'edge',
            localField: '_id',
            foreignField: 'gid',
            as: 'results'
          }},
          { $unwind: '$results' },
          { $match: { 'results._id': { $gte: 1, $lte: 5 } } },
          { $project: { 'results.data': 0 } },
          { $group: { _id: '$_id', results: { $push: '$results' } } }
        ];
    
        // List and iterate each test case
        let tests = [
          'Failing.. Size exceeded...',
          'Working.. Applied $unwind...',
          'Explain output...'
        ];
    
        for (let [idx, test] of Object.entries(tests)) {
          console.log(test);
    
          try {
            let currpipe = (( +idx === 0 ) ? pipeline.slice(0,1) : pipeline),
                options = (( +idx === tests.length-1 ) ? { explain: true } : {});
    
            await new Promise((end,error) => {
              let cursor = db.collection('source').aggregate(currpipe,options);
              for ( let [key, value] of Object.entries({ error, end, data }) )
                cursor.on(key,value);
            });
          } catch(e) {
            console.error(e);
          }
    
        }
    
      } catch(e) {
        console.error(e);
      } finally {
        db.close();
      }
    
    })();
    

    插入一些初始数据后,列表将尝试运行仅包含 $lookup 的聚合,这将失败并出现以下错误:

    { MongoError: 边缘匹配管道中文档的总大小 { $match: { $and : [ { gid: { $eq: 1 } }, {} ] } } 超过最大文档大小

    这基本上告诉您检索时超出了 BSON 限制。

    相比之下,下一次尝试添加了$unwind$match 管道阶段

    解释输出

      {
        "$lookup": {
          "from": "edge",
          "as": "results",
          "localField": "_id",
          "foreignField": "gid",
          "unwinding": {                        // $unwind now is unwinding
            "preserveNullAndEmptyArrays": false
          },
          "matching": {                         // $match now is matching
            "$and": [                           // and actually executed against 
              {                                 // the foreign collection
                "_id": {
                  "$gte": 1
                }
              },
              {
                "_id": {
                  "$lte": 5
                }
              }
            ]
          }
        }
      },
      // $unwind and $match stages removed
      {
        "$project": {
          "results": {
            "data": false
          }
        }
      },
      {
        "$group": {
          "_id": "$_id",
          "results": {
            "$push": "$results"
          }
        }
      }
    

    这个结果当然是成功的,因为结果不再被放入父文档中,所以不能超过 BSON 限制。

    这实际上只是由于添加了$unwind 而发生的,但是添加了$match 以表明这是添加到$lookup 阶段并且整体效果是以有效的方式“限制”返回的结果,因为这一切都在 $lookup 操作中完成,除了匹配的结果之外没有其他结果实际返回。

    通过以这种方式构建,您可以查询超出 BSON 限制的“引用数据”,然后如果您希望 $group 将结果返回为数组格式,一旦它们已被“隐藏查询”有效过滤这实际上是由$lookup 执行的。


    MongoDB 3.6 及更高版本 - “LEFT JOIN”的附加功能

    正如上述所有内容所指出的,BSON 限制是一个您不能违反的“硬”限制,这通常是为什么$unwind 作为临时步骤是必要的。然而,由于$unwind 无法保留内容,“LEFT JOIN”成为“INNER JOIN”的限制。甚至preserveNulAndEmptyArrays 也会否定“合并”并仍然保留完整的数组,从而导致相同的 BSON 限制问题。

    MongoDB 3.6 向$lookup 添加了新语法,允许使用“子管道”表达式代替“本地”和“外来”键。因此,只要生成的数组不违反限制,就可以在返回数组“完整”的管道中放置条件,而不是使用演示的“合并”选项,并且可能没有匹配项作为指示的“左连接”。

    新的表达式将是:

    { "$lookup": {
      "from": "edge",
      "let": { "gid": "$gid" },
      "pipeline": [
        { "$match": {
          "_id": { "$gte": 1, "$lte": 5 },
          "$expr": { "$eq": [ "$$gid", "$to" ] }
        }}          
      ],
      "as": "from"
    }}
    

    事实上,这基本上就是 MongoDB 正在做的“在幕后”,使用以前的语法,因为 3.6 使用 $expr“内部”来构造语句。当然,不同之处在于$lookup 的实际执行方式中没有"unwinding" 选项。

    如果"pipeline" 表达式实际上没有生成任何文档,那么主文档中的目标数组实际上将是空的,就像“LEFT JOIN”实际上所做的那样并且将是@ 的正常行为987654346@ 没有任何其他选项。

    但是,输出数组不得导致创建它的文档超过 BSON 限制。因此,您必须确保条件下的任何“匹配”内容保持在此限制以下,否则相同的错误将持续存在,当然,除非您实际使用 $unwind 来实现“INNER JOIN”。

    【讨论】:

    • 天哪,你一次为我解决了这么多事情。我希望我意识到$lookup 是这样组合的!我也对在查找方面缺少任何类型的$match 感到绝望。似乎很重要的一点,感谢您透露这一点!
    • @prismofeverything 就我个人而言,我对上面链接中“聚合管道优化”下列出的各种内容印象不深。恕我直言,不应该存在这样的过程,您应该能够“直接”指定选项。他们也“应该”记录在$lookup 操作员文档本身。但不幸的是,目前需要指定单独的管道阶段并让“服务器”进行“优化”。恕我直言,这是“意图”而不是“优化”,“意图”应该是您可以直接指定的选项。
    • @NeilLunn 你给出了一个非常好的解释,让我清楚地了解了这个场景。但是在我的情况下,我不能在查询中使用 $unwind,因为我在 $lookup 之后对文档执行过滤,所以我需要它以数组格式。你能建议任何其他方法来解决这个问题吗??
    猜你喜欢
    • 2021-03-19
    • 1970-01-01
    • 2020-11-19
    • 2015-06-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-15
    • 1970-01-01
    相关资源
    最近更新 更多