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