聚合$ lookup匹配管道中文档的总大小超过最大文档大小

pri*_*ing 14 mongodb aggregation-framework

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

{'$lookup':
 {'from': 'edge',
  'localField': 'gid',
  'foreignField': 'to',
  'as': 'from'}}
Run Code Online (Sandbox Code Playgroud)

当我在匹配足够的文档时运行它时,我收到以下错误:

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

所有限制文件数量的尝试都失败了.allowDiskUse: true什么也没做.发送输入cursor无效.添加$limit到聚合中也会失败.

怎么会这样?

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

这里发生了什么?

Nei*_*unn 29

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

对此的计数器是$unwind$lookup管道阶段之后立即处理.这实际上改变了$lookup这样的行为,即不是在父节点中生成数组,而是每个匹配的文档的每个父节点的"副本".

几乎就像常规使用一样$unwind,除了作为"单独"管道阶段处理之外,该unwinding操作实际上已添加到$lookup管道操作本身.理想情况下,你也遵循$unwind一个$match条件,它也创建一个matching参数也可以添加到$lookup.您实际上可以在explain管道的输出中看到这一点.

该主题实际上在核心文档的聚合管道优化部分中(简要地)介绍:

$ lookup + $ unwind Coalescence

版本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();
  }

})();
Run Code Online (Sandbox Code Playgroud)

插入一些初始数据后,列表将尝试运行仅由$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"
      }
    }
  }
Run Code Online (Sandbox Code Playgroud)

当然结果是成功的,因为结果不再被放入父文档中,所以不能超过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,允许使用"子管道"表达式来代替"本地"和"外部"键.因此,不是使用所示的"coalescence"选项,只要生成的数组也没有超出限制,就可以在该管道中放置条件,使得数组"完整",并且可能没有匹配,因为这将是指示一个"LEFT JOIN".

那么新的表达式将是:

{ "$lookup": {
  "from": "edge",
  "let": { "gid": "$gid" },
  "pipeline": [
    { "$match": {
      "_id": { "$gte": 1, "$lte": 5 },
      "$expr": { "$eq": [ "$$gid", "$to" ] }
    }}          
  ],
  "as": "from"
}}
Run Code Online (Sandbox Code Playgroud)

事实上,这基本上是MongoDB 使用之前的语法"在幕后"进行的操作,因为3.6使用$expr"内部"来构造语句.当然,区别在于实际执行方式没有"unwinding"选项$lookup.

如果"pipeline"表达式不会实际生成任何文档,那么主文档中的目标数组实际上将为空,就像"LEFT JOIN"实际上一样,并且是$lookup没有任何其他选项的正常行为.

但是输出数组必须不会导致创建它的文档超过BSON限制.因此,您应该确保条件下的任何"匹配"内容保持在此限制之下,否则相同的错误将持续存在,除非您实际上用于$unwind实现"内部联接".

  • @prismofeverything就个人而言,我对上面链接中"聚合管道优化"下列出的各种事物印象不深.恕我直言,不应该存在这样的过程,你应该能够"直接"指定选项.并且它们也应"记录在`$ lookup`操作员文档本身上.但不幸的是,目前需要指定单独的管道阶段并让"服务器"进行"优化".恕我直言,这是"意图"而不是"优化",而"意图"应该是您可以直接指定的选项. (3认同)
  • 噢,伙计,你刚刚为我解决了很多事情.我希望我意识到`$ lookup`就这样组合了!我对与查询相关的任何"$ match"缺乏绝望.看起来非常重要,感谢您揭示这一点! (2认同)

pri*_*san 5

我在执行 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"}
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

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"}
        }
    }
}
Run Code Online (Sandbox Code Playgroud)