web-dev-qa-db-fra.com

Recherche $ agrégée La taille totale des documents dans le pipeline correspondant dépasse la taille maximale du document

J'ai un assez simple $lookup requête d'agrégation comme suit:

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

Lorsque j'exécute cela sur une correspondance avec suffisamment de documents, j'obtiens l'erreur suivante:

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

Toutes les tentatives pour limiter le nombre de documents échouent. allowDiskUse: true ne fait rien. L'envoi d'un cursor in ne fait rien. Ajout d'un $limit dans l'agrégation échoue également.

Comment est-ce possible?

Ensuite, je vois à nouveau l'erreur. Où est-ce que $match et $and et $eq viens de? Le pipeline d'agrégation se déroule-t-il dans les coulisses du $lookup appel à une autre agrégation, une qui s'exécute d'elle-même avec laquelle je n'ai aucune capacité à fournir des limites ou à utiliser des curseurs avec ??

Qu'est-ce qui se passe ici?

16
prismofeverything

Comme indiqué précédemment dans le commentaire, l'erreur se produit car lors de l'exécution de la $lookup qui, par défaut, produit un "tableau" cible dans le document parent à partir des résultats de la collection étrangère, la taille totale des documents sélectionnés pour ce tableau entraîne le parent à dépasser 16 Mo BSON Limit.

Le compteur pour cela est de traiter avec un $unwind qui suit immédiatement le $lookup étape du pipeline. Cela modifie en fait le comportement de $lookup de telle sorte qu'au lieu de produire un tableau dans le parent, les résultats sont plutôt une "copie" de chaque parent pour chaque document correspondant.

Tout comme l’utilisation régulière de $unwind , à l'exception qu'au lieu de traiter comme une étape de pipeline "séparée", l'action unwinding est en fait ajoutée à $lookup opération de pipeline elle-même. Idéalement, vous suivez également les $unwind avec un $match , qui crée également un argument matching à ajouter également à la $lookup . Vous pouvez réellement voir cela dans la sortie explain du pipeline.

Le sujet est en fait couvert (brièvement) dans une section de Aggregation Pipeline Optimization dans la documentation principale:

$ lookup + $ unwind Coalescence

Nouveau dans la version 3.2.

Lorsqu'un déroulement $ suit immédiatement une autre recherche $ et que le déroulement $ opère sur le champ as de la recherche $, l'optimiseur peut fusionner le déroulement $ dans l'étape de recherche $. Cela évite de créer de gros documents intermédiaires.

Il est préférable de le démontrer avec une liste qui met le serveur sous tension en créant des documents "connexes" qui dépasseraient la limite BSON de 16 Mo. Fait aussi brièvement que possible pour briser et contourner la 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();
  }

})();

Après avoir inséré des données initiales, la liste tentera d'exécuter un agrégat simplement composé de $lookup qui échouera avec l'erreur suivante:

{MongoError: la taille totale des documents dans le pipeline de correspondance Edge {$ match: {$ et: [{gid: {$ eq: 1}}, {}]}} dépasse la taille maximale du document

Ce qui vous indique essentiellement que la limite BSON a été dépassée lors de la récupération.

En revanche, la prochaine tentative ajoute le $unwind et $match étapes du pipeline

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

Et ce résultat réussit bien sûr, car comme les résultats ne sont plus placés dans le document parent, la limite BSON ne peut pas être dépassée.

Cela se produit vraiment simplement suite à l'ajout de $unwind uniquement, mais le $match est ajouté par exemple pour montrer que c'est également ajouté dans le $lookup étape et que l'effet global est de "limiter" les résultats retournés de manière efficace, car tout est fait dans cette $lookup opération et aucun autre résultat que ceux correspondant n'est réellement retourné.

En construisant de cette manière, vous pouvez rechercher des "données référencées" qui dépasseraient la limite BSON, puis si vous le souhaitez $group les résultats dans un format de tableau, une fois qu'ils ont été efficacement filtrés par la "requête cachée" qui est réellement effectuée par $lookup .


MongoDB 3.6 et supérieur - Supplémentaire pour "LEFT JOIN"

Comme tout le contenu ci-dessus le note, la limite BSON est une limite "dure" que vous ne pouvez pas violer et c'est généralement la raison pour laquelle $unwind est nécessaire comme étape intermédiaire. Il y a cependant la limitation que le "LEFT JOIN" devient un "INNER JOIN" en vertu de $unwind où il ne peut pas conserver le contenu. De plus, même preserveNulAndEmptyArrays annulerait la "coalescence" et laisserait toujours le tableau intact, provoquant le même problème de limite BSON.

MongoDB 3.6 ajoute une nouvelle syntaxe à $lookup qui permet d'utiliser une expression "sous-pipeline" à la place des clés "locales" et "étrangères". Ainsi, au lieu d'utiliser l'option "coalescence" comme démontré, tant que le tableau produit ne dépasse pas également la limite, il est possible de mettre des conditions dans ce pipeline qui retourne le tableau "intact", et éventuellement sans correspondance comme cela serait indicatif d'un "LEFT JOIN".

La nouvelle expression serait alors:

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

En fait, ce serait fondamentalement ce que fait MongoDB "sous les couvertures" avec la syntaxe précédente puisque 3.6 utilise $expr "en interne" afin de construire l'instruction. La différence est bien sûr qu'il n'y a pas de "unwinding" option présente dans la façon dont $lookup est effectivement exécuté.

Si aucun document n'est réellement produit à la suite du "pipeline", le tableau cible dans le document maître sera en fait vide, tout comme le fait réellement un "LEFT JOIN" et serait le comportement normal de $lookup sans aucune autre option.

Cependant, le tableau de sortie à NE DOIT PAS faire en sorte que le document où il est créé dépasse la limite BSON . Donc, c'est vraiment à vous de vous assurer que tout contenu "correspondant" par les conditions reste sous cette limite ou la même erreur persistera, à moins bien sûr que vous n'utilisiez réellement $unwind pour effectuer la "INNER JOIN".

31
Neil Lunn