MongoDB
 sql >> база данни >  >> NoSQL >> MongoDB

Aggregate $lookup Общият размер на документите в съответстващия конвейер надвишава максималния размер на документа

Както беше посочено по-рано в коментар, грешката възниква, защото при извършване на $lookup който по подразбиране създава целеви „масив“ в родителския документ от резултатите от чуждата колекция, общият размер на документите, избрани за този масив, кара родителя да надвиши ограничението от 16MB BSON.

Броячът за това е да се обработва с $unwind който непосредствено следва $lookup етап на тръбопровода. Това всъщност променя поведението на $lookup в така, че вместо да създават масив в родителя, резултатите са вместо това "копие" на всеки родител за всеки съпоставен документ.

Почти като редовното използване на $unwind , с изключението, че вместо да се обработва като "отделен" етап на конвейера, unwinding действието всъщност се добавя към $lookup самата работа на тръбопровода. В идеалния случай трябва да следвате и $unwind с $match условие, което също създава matching аргумент също да бъде добавен към $lookup . Всъщност можете да видите това в explain изход за тръбопровода.

Темата всъщност е разгледана (накратко) в раздел за оптимизация на агрегационния конвейер в основната документация:

$lookup + $unwind Coalescence

Ново във версия 3.2.

Когато $unwind непосредствено следва друго $lookup и $unwind оперира с полето as на $lookup, оптимизаторът може да обедини $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 тръбопроводни етапи

Изходът Explain :

  {
    "$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 е необходимо като междинна стъпка. Съществува обаче ограничението, че „LEFT JOIN“ става „INNER JOIN“ по силата на $unwind където не може да запази съдържанието. Също така дори preserveNulAndEmptyArrays ще отрече „сливането“ и пак ще остави непокътнатия масив, причинявайки същия проблем с BSON Limit.

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"
}}

Всъщност това би било основно това, което MongoDB прави „под завивките“ с предишния синтаксис от 3.6 използва $expr "вътрешно", за да се конструира изявлението. Разликата, разбира се, е, че няма "unwinding" опция присъства в как $lookup всъщност се изпълнява.

Ако действително не се произвеждат документи в резултат на "pipeline" израз, тогава целевият масив в основния документ всъщност ще бъде празен, точно както всъщност прави "LEFT JOIN" и би било нормалното поведение на $lookup без никакви други опции.

Въпреки това изходният масив на НЕ ТРЯБВА да кара документа, където се създава, да надвишава BSON лимита . Така че наистина зависи от вас да гарантирате, че всяко "съвпадащо" съдържание от условията остава под това ограничение или същата грешка ще продължи, освен ако, разбира се, всъщност не използвате $unwind за да осъществите „INNER JOIN“.



  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Разделете низ в масив от поднизове или знаци в MongoDB

  2. Използвайте Mongosniff, за да изясните какво чува и казва вашият MongoDB

  3. връщане на документ с най-нов поддокумент само в mongodb агрегат

  4. objasni() в Mongodb:разлики между nscanned и nscannedObjects

  5. Как да конвертирам pymongo.cursor.Cursor в dict?