sql >> Databáze >  >> NoSQL >> MongoDB

Aggregate $lookup Celková velikost dokumentů v odpovídajícím kanálu překračuje maximální velikost dokumentu

Jak bylo uvedeno dříve v komentáři, k chybě dochází, protože při provádění $lookup které ve výchozím nastavení vytváří cílové "pole" v rámci nadřazeného dokumentu z výsledků cizí kolekce, celková velikost dokumentů vybraných pro toto pole způsobí, že nadřazený objekt překročí limit 16 MB BSON.

Počítadlo pro toto je zpracování pomocí $unwind který bezprostředně následuje za $lookup fáze potrubí. To ve skutečnosti mění chování $lookup v tom, že namísto vytváření pole v nadřazeném prvku jsou výsledky místo toho „kopií“ každého rodiče pro každý odpovídající dokument.

Skoro jako při běžném používání $unwind s tou výjimkou, že namísto zpracování jako "samostatné" fáze potrubí se unwinding akce je ve skutečnosti přidána do $lookup samotný provoz potrubí. V ideálním případě také postupujte podle $unwind s $match podmínkou, která také vytvoří matching argument, který má být také přidán do $lookup . Ve skutečnosti to můžete vidět v explain výstup pro potrubí.

Toto téma je ve skutečnosti pokryto (stručně) v části Optimalizace agregačního potrubí v základní dokumentaci:

$lookup + $unwind Coalescence

Novinka ve verzi 3.2.

Když $unwind bezprostředně následuje po dalším $lookup a $unwind působí na poli jako $lookup, optimalizátor může sloučit $unwind do fáze $lookup. Vyhnete se tak vytváření velkých mezilehlých dokumentů.

Nejlépe se to demonstruje na výpisu, který vystavuje server stresu vytvářením „souvisejících“ dokumentů, které by překročily limit 16 MB BSON. Provedeno co nejkratší dobu, aby došlo k porušení a obcházení limitu 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();
  }

})();

Po vložení některých počátečních dat se zápis pokusí spustit souhrn obsahující pouze $lookup který selže s následující chybou:

{ MongoError:Celková velikost dokumentů v kanálu shody okrajů { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } překračuje maximální velikost dokumentu

Což v podstatě znamená, že limit BSON byl při načítání překročen.

Naproti tomu další pokus přidá $unwind a $match etapy potrubí

Výstup Vysvětlení :

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

A tento výsledek samozřejmě uspěje, protože protože výsledky již nejsou umísťovány do nadřazeného dokumentu, nelze překročit limit BSON.

To se opravdu děje jako výsledek přidání $unwind pouze, ale $match je například přidáno, aby se ukázalo, že je to také přidáno do $lookup fázi a že celkovým efektem je "omezení" výsledků vrácených efektivním způsobem, protože vše se děje v tomto $lookup operace a ve skutečnosti nejsou vráceny žádné jiné výsledky než ty, které se shodují.

Konstruováním tímto způsobem se můžete dotazovat na „referenční data“, která by překročila limit BSON, a poté, chcete-li, $group výsledky zpět do formátu pole, jakmile byly efektivně filtrovány „skrytým dotazem“, který ve skutečnosti provádí $lookup .

MongoDB 3.6 a vyšší – další pro „LEFT JOIN“

Jak poznamenává veškerý obsah výše, limit BSON je "tvrdý" limit, který nemůžete překročit, a to je obecně důvod, proč $unwind je nezbytný jako přechodný krok. Existuje však omezení, že „LEFT JOIN“ se stane „INNER JOIN“ na základě $unwind kde nemůže zachovat obsah. Také dokonce preserveNulAndEmptyArrays by negovalo "slučování" a stále by zůstalo nedotčené pole, což by způsobilo stejný problém s limitem BSON.

MongoDB 3.6 přidává novou syntaxi do $lookup který umožňuje použití výrazu "podpotrubí" místo "místního" a "cizího" klíče. Takže namísto použití možnosti „slučování“, jak bylo ukázáno, pokud vytvořené pole také neporuší limit, je možné vložit podmínky do tohoto potrubí, které vrátí pole „neporušené“ a možná bez žádných shod, jak by bylo indikativní. z "LEVÉHO PŘIPOJENÍ".

Nový výraz by pak byl:

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

Ve skutečnosti by to bylo v podstatě to, co MongoDB dělá "pod pokličkou" s předchozí syntaxí od 3.6 používá $expr „interně“ za účelem sestavení prohlášení. Rozdíl je samozřejmě v tom, že zde není žádné "unwinding" možnost přítomná ve způsobu $lookup skutečně bude vykonán.

Pokud se v důsledku "pipeline" ve skutečnosti nevytvoří žádné dokumenty výraz, pak bude cílové pole v hlavním dokumentu ve skutečnosti prázdné, stejně jako "LEFT JOIN" ve skutečnosti dělá a bylo by normálním chováním $lookup bez dalších možností.

Výstupní pole však NESMÍ způsobit, že dokument, ve kterém se vytváří, překročí limit BSON . Je tedy skutečně na vás, abyste zajistili, že jakýkoli „odpovídající“ obsah podle podmínek zůstane pod tímto limitem nebo bude stejná chyba přetrvávat, pokud samozřejmě nepoužijete $unwind k provedení „INNER JOIN“.



  1. Websocket odpojen Připojení volání se nezdařilo

  2. NoSQL (MongoDB) vs Lucene (nebo Solr) jako vaše databáze

  3. Pokud je Redis single Threaded, jak může být tak rychlý?

  4. Testování jednotek s MongoDB