Como dito anteriormente no comentário, o erro ocorre porque ao executar o
$lookup
que por padrão produz uma "matriz" de destino no documento pai a partir dos resultados da coleção estrangeira, o tamanho total dos documentos selecionados para essa matriz faz com que o pai exceda o limite de 16 MB BSON. O contador para isso é processar com um
$unwind
que segue imediatamente o $lookup
estágio de tubulação. Isso realmente altera o comportamento de $lookup
de tal forma que, em vez de produzir uma matriz no pai, os resultados são uma "cópia" de cada pai para cada documento correspondido. Quase como o uso regular de
$unwind
, com a exceção de que, em vez de processar como um estágio de pipeline "separado", o unwinding
ação é realmente adicionada ao $lookup
própria operação do gasoduto. Idealmente, você também segue o $unwind
com um $match
condição, que também cria uma matching
argumento a ser adicionado ao $lookup
. Você pode realmente ver isso no explain
saída para o pipeline. Na verdade, o tópico é abordado (brevemente) em uma seção de Otimização de pipeline de agregação na documentação principal:
$lookup + $unwind Coalescence
Novo na versão 3.2.
Quando um $unwind segue imediatamente outro $lookup, e o $unwind opera no campo as do $lookup, o otimizador pode unir o $unwind no estágio $lookup. Isso evita a criação de grandes documentos intermediários.
Melhor demonstrado com uma listagem que coloca o servidor sob estresse criando documentos "relacionados" que excederiam o limite BSON de 16 MB. Feito o mais breve possível para quebrar e contornar o limite 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();
}
})();
Após inserir alguns dados iniciais, a listagem tentará executar uma agregação consistindo meramente de
$lookup
que irá falhar com o seguinte erro:
{ MongoError:tamanho total de documentos no pipeline de correspondência de borda { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } excede o tamanho máximo do documento
O que basicamente está dizendo que o limite de BSON foi excedido na recuperação.
Por outro lado, a próxima tentativa adiciona o
$unwind
e $match
estágios do pipeline A saída Explicar :
{
"$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 esse resultado, claro, é bem-sucedido, porque como os resultados não estão mais sendo colocados no documento pai, o limite BSON não pode ser excedido.
Isso realmente acontece como resultado da adição de
$unwind
apenas, mas o $match
é adicionado, por exemplo, para mostrar que isso é também adicionado ao $lookup
stage e que o efeito geral é "limitar" os resultados retornados de forma efetiva, já que tudo é feito nesse $lookup
operação e nenhum outro resultado diferente daqueles correspondentes são realmente retornados. Ao construir dessa maneira, você pode consultar "dados referenciados" que excederiam o limite BSON e, em seguida, se você quiser
$group
os resultados de volta em um formato de matriz, uma vez que tenham sido efetivamente filtrados pela "consulta oculta" que está sendo realizada por $lookup
. MongoDB 3.6 e superior - Adicional para "LEFT JOIN"
Como todo o conteúdo acima observa, o Limite BSON é um "difícil" limite que você não pode violar e geralmente é por isso que o
$unwind
é necessário como um passo provisório. No entanto, existe a limitação de que o "LEFT JOIN" se torna um "INNER JOIN" em virtude do $unwind
onde não pode preservar o conteúdo. Também até mesmo preserveNulAndEmptyArrays
negaria a "coalescência" e ainda deixaria a matriz intacta, causando o mesmo problema de limite de BSON. MongoDB 3.6 adiciona nova sintaxe ao
$lookup
que permite que uma expressão "sub-pipeline" seja usada no lugar das chaves "local" e "estrangeira". Então, em vez de usar a opção "coalescence" como demonstrado, desde que o array produzido também não ultrapasse o limite, é possível colocar condições nesse pipeline que retornam o array "intacto", e possivelmente sem correspondências como seria indicativo de um "LEFT JOIN". A nova expressão seria então:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
Na verdade, isso seria basicamente o que o MongoDB está fazendo "nos bastidores" com a sintaxe anterior desde 3.6 usa
$expr
"internamente" para construir o enunciado. A diferença, claro, é que não há "unwinding"
opção presente em como o $lookup
realmente é executado. Se nenhum documento for realmente produzido como resultado do
"pipeline"
expressão, então a matriz de destino dentro do documento mestre estará de fato vazia, assim como um "LEFT JOIN" realmente faz e seria o comportamento normal de $lookup
sem outras opções. No entanto, a matriz de saída para NÃO DEVE fazer com que o documento em que está sendo criado exceda o limite BSON . Portanto, cabe a você garantir que qualquer conteúdo "correspondente" pelas condições permaneça abaixo desse limite ou o mesmo erro persistirá, a menos que você realmente use
$unwind
para efetuar o "INNER JOIN".