MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Aggrega $ ricerca La dimensione totale dei documenti nella pipeline corrispondente supera la dimensione massima del documento

Come affermato in precedenza nel commento, l'errore si verifica perché durante l'esecuzione di $lookup che per impostazione predefinita produce un "array" di destinazione all'interno del documento padre dai risultati della raccolta esterna, la dimensione totale dei documenti selezionati per tale array fa sì che il genitore superi il limite BSON di 16 MB.

Il contatore per questo è elaborare con un $unwind che segue immediatamente il $lookup fase della pipeline. Questo in realtà altera il comportamento di $lookup in modo tale che invece di produrre un array nel genitore, i risultati siano invece una "copia" di ciascun genitore per ogni documento abbinato.

Praticamente proprio come il normale utilizzo di $unwind , con l'eccezione che invece di elaborare come una fase di pipeline "separata", lo unwinding l'azione viene effettivamente aggiunta a $lookup operazione stessa del gasdotto. Idealmente segui anche il $unwind con un $match condizione, che crea anche una matching argomento da aggiungere anche a $lookup . Puoi effettivamente vederlo in explain output per la pipeline.

L'argomento è effettivamente trattato (brevemente) in una sezione dell'ottimizzazione della pipeline di aggregazione nella documentazione principale:

$lookup + $unwind Coalescenza

Novità nella versione 3.2.

Quando un $unwind segue immediatamente un altro $lookup e $unwind opera sul campo as della $lookup, l'ottimizzatore può fondere $unwind nella fase di $lookup. Ciò evita di creare documenti intermedi di grandi dimensioni.

Dimostrato al meglio con un elenco che mette sotto stress il server creando documenti "correlati" che supererebbero il limite di 16 MB BSON. Fatto il più brevemente possibile sia per rompere che aggirare il limite 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();
  }

})();

Dopo aver inserito alcuni dati iniziali, l'elenco tenterà di eseguire un aggregato costituito semplicemente da $lookup che fallirà con il seguente errore:

{ MongoError:la dimensione totale dei documenti nella pipeline di corrispondenza dei bordi { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } supera la dimensione massima del documento

Il che sostanzialmente ti dice che il limite BSON è stato superato durante il recupero.

Al contrario, il prossimo tentativo aggiunge il $unwind e $match fasi della pipeline

L'output Spiega :

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

E questo risultato ovviamente ha esito positivo, perché poiché i risultati non vengono più inseriti nel documento principale, il limite BSON non può essere superato.

Questo accade davvero solo come risultato dell'aggiunta di $unwind solo, ma il $match viene aggiunto ad esempio per mostrare che questo è anche aggiunto nel $lookup fase e che l'effetto complessivo è quello di "limitare" i risultati restituiti in modo efficace, poiché è tutto fatto in quel $lookup operazione e non vengono effettivamente restituiti altri risultati diversi da quelli corrispondenti.

Costruendo in questo modo puoi interrogare "dati referenziati" che supererebbero il limite BSON e quindi se vuoi $group i risultati tornano in un formato array, una volta che sono stati effettivamente filtrati dalla "query nascosta" che viene effettivamente eseguita da $lookup .

MongoDB 3.6 e versioni successive - Aggiuntivo per "LEFT JOIN"

Come tutti i contenuti sopra riportati, il limite BSON è un "difficile" limite che non puoi superare e questo è generalmente il motivo per cui il $unwind è necessario come passaggio intermedio. C'è però il limite che il "LEFT JOIN" diventi un "INNER JOIN" in virtù del $unwind dove non può preservare il contenuto. Anche preserveNulAndEmptyArrays annullerebbe la "coalescenza" e lascerebbe comunque l'array intatto, causando lo stesso problema di limite BSON.

MongoDB 3.6 aggiunge una nuova sintassi a $lookup che consente di utilizzare un'espressione "sub-pipeline" al posto delle chiavi "locale" e "estranea". Quindi invece di utilizzare l'opzione "coalescenza" come mostrato, fintanto che l'array prodotto non supera anche il limite è possibile porre condizioni in quella pipeline che restituisce l'array "intatto", e possibilmente senza corrispondenze come sarebbe indicativo di un "UNIONE A SINISTRA".

La nuova espressione sarebbe quindi:

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

In effetti, questo sarebbe fondamentalmente ciò che MongoDB sta facendo "sotto le coperte" con la sintassi precedente dalla 3.6 usa $expr "internamente" per costruire l'affermazione. La differenza ovviamente è che non c'è "unwinding" opzione presente in come il $lookup viene effettivamente eseguito.

Se nessun documento viene effettivamente prodotto come risultato della "pipeline" espressione, quindi l'array di destinazione all'interno del documento master sarà in effetti vuoto, proprio come fa effettivamente un "LEFT JOIN" e sarebbe il comportamento normale di $lookup senza altre opzioni.

Tuttavia, l'array di output NON DEVE far sì che il documento in cui viene creato superi il limite BSON . Quindi sta davvero a te assicurarti che qualsiasi contenuto "corrispondente" alle condizioni rimanga al di sotto di questo limite o che lo stesso errore persista, a meno che ovviamente non usi effettivamente $unwind per effettuare la "INNER JOIN".