MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Agrégat $lookup La taille totale des documents dans le pipeline correspondant dépasse la taille maximale du document

Comme indiqué précédemment dans le commentaire, l'erreur se produit car lors de l'exécution de la $lookup qui produit par défaut 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 fait que le parent dépasse la limite BSON de 16 Mo.

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 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.

À peu près comme l'utilisation régulière de $unwind , à l'exception qu'au lieu de traiter comme une étape de pipeline "séparée", le unwinding l'action est en fait ajoutée au $lookup l'exploitation du pipeline lui-même. Idéalement, vous suivez également le $unwind avec un $match condition, qui crée également une matching argument à ajouter également au $lookup . Vous pouvez en fait le voir dans le explain sortie pour le pipeline.

Le sujet est en fait couvert (brièvement) dans une section de l'optimisation du pipeline d'agrégation dans la documentation principale :

$lookup + $unwind Coalescence

Nouveauté de la version 3.2.

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

Mieux illustré avec une liste qui met le serveur sous pression en créant des documents "connexes" qui dépasseraient la limite de 16 Mo BSON. Effectué aussi brièvement que possible pour casser 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é quelques données initiales, la liste tentera d'exécuter un agrégat composé simplement de $lookup qui échouera avec l'erreur suivante :

{ MongoError :La taille totale des documents dans le pipeline de correspondance des bords { $match :{ $and :[ { 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 Expliquer :

  {
    "$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 simplement à la suite de l'ajout de $unwind seulement, mais le $match est ajouté par exemple pour montrer que c'est aussi ajouté dans le $lookup étape et que l'effet global est de "limiter" les résultats renvoyés de manière efficace, puisque tout est fait dans cette $lookup opération et aucun autre résultat autre que ceux correspondants n'est réellement renvoyé.

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é effectivement filtrés par la "requête cachée" qui est en fait exécuté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 un "dur" limite que vous ne pouvez pas franchir et c'est généralement pourquoi le $unwind est nécessaire comme étape intermédiaire. Il y a cependant la limitation que le "LEFT JOIN" devient un "INNER JOIN" en vertu du $unwind où il ne peut pas conserver le contenu. Aussi même preserveNulAndEmptyArrays annulerait la "coalescence" et laisserait toujours le tableau intact, causant 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 "locale" et "étrangère". 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 renvoie le tableau "intact", et éventuellement sans correspondance comme 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 essentiellement ce que MongoDB fait "sous les couvertures" avec la syntaxe précédente puisque la 3.6 utilise $expr "interne" pour construire l'énoncé. La différence est bien sûr qu'il n'y a pas de "unwinding" option présente dans la façon dont le $lookup est effectivement exécuté.

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

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