sql >> Base de Datos >  >> NoSQL >> MongoDB

Agregar $búsqueda El tamaño total de los documentos en la canalización coincidente excede el tamaño máximo del documento

Como se indicó anteriormente en el comentario, el error ocurre porque al realizar $lookup que por defecto produce una "matriz" de destino dentro del documento principal a partir de los resultados de la colección externa, el tamaño total de los documentos seleccionados para esa matriz hace que el principal exceda el límite de BSON de 16 MB.

El contador para esto es procesar con un $unwind que sigue inmediatamente al $lookup etapa de tubería. Esto realmente altera el comportamiento de $lookup de tal manera que en lugar de producir una matriz en el padre, los resultados son una "copia" de cada padre para cada documento coincidente.

Prácticamente como el uso regular de $unwind , con la excepción de que en lugar de procesarse como una etapa de canalización "separada", el unwinding la acción se agrega realmente a $lookup operación del oleoducto en sí. Lo ideal es que también sigas el $unwind con un $match condición, que también crea una matching argumento que también se agregará a $lookup . De hecho, puedes ver esto en explain salida para la canalización.

En realidad, el tema se cubre (brevemente) en una sección de Optimización de canalización de agregación en la documentación principal:

$lookup + $unwind Coalescencia

Nuevo en la versión 3.2.

Cuando $unwind sigue inmediatamente a otra $lookup, y $unwind opera en el campo as de $lookup, el optimizador puede fusionar $unwind en la etapa de $lookup. Esto evita crear grandes documentos intermedios.

La mejor demostración es con una lista que pone al servidor bajo estrés al crear documentos "relacionados" que excederían el límite de BSON de 16 MB. Hecho lo más breve posible para romper y evitar el límite de 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();
  }

})();

Después de insertar algunos datos iniciales, la lista intentará ejecutar un agregado que consista simplemente en $lookup que fallará con el siguiente error:

{ MongoError:el tamaño total de los documentos en la canalización de coincidencia de bordes { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } excede el tamaño máximo del documento

Lo que básicamente le dice que se excedió el límite de BSON en la recuperación.

Por el contrario, el siguiente intento agrega el $unwind y $match etapas de canalización

El resultado de la explicación :

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

Y ese resultado, por supuesto, tiene éxito, porque como los resultados ya no se colocan en el documento principal, no se puede exceder el límite de BSON.

Esto realmente sucede como resultado de agregar $unwind solamente, pero el $match se agrega, por ejemplo, para mostrar que esto es también agregado en el $lookup etapa y que el efecto general es "limitar" los resultados devueltos de una manera efectiva, ya que todo se hace en ese $lookup operación y no se devuelven otros resultados que no sean los coincidentes.

Al construir de esta manera, puede consultar "datos de referencia" que excederían el límite de BSON y luego, si desea $group los resultados regresan a un formato de matriz, una vez que han sido filtrados de manera efectiva por la "consulta oculta" que en realidad está realizando $lookup .

MongoDB 3.6 y superior:adicional para "LEFT JOIN"

Como señala todo el contenido anterior, el límite de BSON es un "duro" límite que no puede infringir y esta es generalmente la razón por la cual $unwind es necesario como un paso intermedio. Sin embargo, existe la limitación de que "LEFT JOIN" se convierte en "INNER JOIN" en virtud de $unwind donde no puede preservar el contenido. También incluso preserveNulAndEmptyArrays negaría la "coalescencia" y aún dejaría la matriz intacta, causando el mismo problema de límite de BSON.

MongoDB 3.6 agrega una nueva sintaxis a $lookup que permite utilizar una expresión de "subcanalización" en lugar de las claves "local" y "foránea". Entonces, en lugar de usar la opción "coalescencia" como se demostró, siempre que la matriz producida no infrinja el límite, es posible poner condiciones en esa tubería que devuelve la matriz "intacta", y posiblemente sin coincidencias como sería indicativo de una "UNIÓN IZQUIERDA".

La nueva expresión sería entonces:

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

De hecho, esto sería básicamente lo que está haciendo MongoDB "debajo de las sábanas" con la sintaxis anterior desde 3.6 usa $expr "internamente" para construir la declaración. La diferencia, por supuesto, es que no hay "unwinding" opción presente en cómo $lookup en realidad se ejecuta.

Si no se produce ningún documento como resultado del "pipeline" expresión, entonces la matriz de destino dentro del documento maestro de hecho estará vacía, tal como lo hace realmente "LEFT JOIN" y sería el comportamiento normal de $lookup sin ninguna otra opción.

Sin embargo, la matriz de salida a NO DEBE hacer que el documento en el que se crea supere el límite de BSON . Por lo tanto, realmente depende de usted asegurarse de que cualquier contenido que "coincida" con las condiciones se mantenga por debajo de este límite o persistirá el mismo error, a menos, por supuesto, que realmente use $unwind para efectuar la "UNIÓN INTERNA".