Come affermato in precedenza nel commento, l'errore si verifica perché durante l'esecuzione di $lookup
che per impostazione predefinita produce un "array" di destinazione all'interno del documento padre dai risultati della raccolta esterna, la dimensione totale dei documenti selezionati per tale array fa sì che il genitore superi il limite BSON di 16 MB.
Il contatore per questo è elaborare con un $unwind
che segue immediatamente il $lookup
fase della pipeline. Questo in realtà altera il comportamento di $lookup
in modo tale che invece di produrre un array nel genitore, i risultati siano invece una "copia" di ciascun genitore per ogni documento abbinato.
Praticamente proprio come il normale utilizzo di $unwind
, con l'eccezione che invece di elaborare come una fase di pipeline "separata", lo unwinding
l'azione viene effettivamente aggiunta a $lookup
operazione stessa del gasdotto. Idealmente segui anche il $unwind
con un $match
condizione, che crea anche una matching
argomento da aggiungere anche a $lookup
. Puoi effettivamente vederlo in explain
output per la pipeline.
L'argomento è effettivamente trattato (brevemente) in una sezione dell'ottimizzazione della pipeline di aggregazione nella documentazione principale:
$lookup + $unwind Coalescenza
Novità nella versione 3.2.
Quando un $unwind segue immediatamente un altro $lookup e $unwind opera sul campo as della $lookup, l'ottimizzatore può fondere $unwind nella fase di $lookup. Ciò evita di creare documenti intermedi di grandi dimensioni.
Dimostrato al meglio con un elenco che mette sotto stress il server creando documenti "correlati" che supererebbero il limite di 16 MB BSON. Fatto il più brevemente possibile sia per rompere che aggirare il 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();
}
})();
Dopo aver inserito alcuni dati iniziali, l'elenco tenterà di eseguire un aggregato costituito semplicemente da $lookup
che fallirà con il seguente errore:
{ MongoError:la dimensione totale dei documenti nella pipeline di corrispondenza dei bordi { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } supera la dimensione massima del documento
Il che sostanzialmente ti dice che il limite BSON è stato superato durante il recupero.
Al contrario, il prossimo tentativo aggiunge il $unwind
e $match
fasi della pipeline
L'output Spiega :
{
"$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 questo risultato ovviamente ha esito positivo, perché poiché i risultati non vengono più inseriti nel documento principale, il limite BSON non può essere superato.
Questo accade davvero solo come risultato dell'aggiunta di $unwind
solo, ma il $match
viene aggiunto ad esempio per mostrare che questo è anche aggiunto nel $lookup
fase e che l'effetto complessivo è quello di "limitare" i risultati restituiti in modo efficace, poiché è tutto fatto in quel $lookup
operazione e non vengono effettivamente restituiti altri risultati diversi da quelli corrispondenti.
Costruendo in questo modo puoi interrogare "dati referenziati" che supererebbero il limite BSON e quindi se vuoi $group
i risultati tornano in un formato array, una volta che sono stati effettivamente filtrati dalla "query nascosta" che viene effettivamente eseguita da $lookup
.
MongoDB 3.6 e versioni successive - Aggiuntivo per "LEFT JOIN"
Come tutti i contenuti sopra riportati, il limite BSON è un "difficile" limite che non puoi superare e questo è generalmente il motivo per cui il $unwind
è necessario come passaggio intermedio. C'è però il limite che il "LEFT JOIN" diventi un "INNER JOIN" in virtù del $unwind
dove non può preservare il contenuto. Anche preserveNulAndEmptyArrays
annullerebbe la "coalescenza" e lascerebbe comunque l'array intatto, causando lo stesso problema di limite BSON.
MongoDB 3.6 aggiunge una nuova sintassi a $lookup
che consente di utilizzare un'espressione "sub-pipeline" al posto delle chiavi "locale" e "estranea". Quindi invece di utilizzare l'opzione "coalescenza" come mostrato, fintanto che l'array prodotto non supera anche il limite è possibile porre condizioni in quella pipeline che restituisce l'array "intatto", e possibilmente senza corrispondenze come sarebbe indicativo di un "UNIONE A SINISTRA".
La nuova espressione sarebbe quindi:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
In effetti, questo sarebbe fondamentalmente ciò che MongoDB sta facendo "sotto le coperte" con la sintassi precedente dalla 3.6 usa $expr
"internamente" per costruire l'affermazione. La differenza ovviamente è che non c'è "unwinding"
opzione presente in come il $lookup
viene effettivamente eseguito.
Se nessun documento viene effettivamente prodotto come risultato della "pipeline"
espressione, quindi l'array di destinazione all'interno del documento master sarà in effetti vuoto, proprio come fa effettivamente un "LEFT JOIN" e sarebbe il comportamento normale di $lookup
senza altre opzioni.
Tuttavia, l'array di output NON DEVE far sì che il documento in cui viene creato superi il limite BSON . Quindi sta davvero a te assicurarti che qualsiasi contenuto "corrispondente" alle condizioni rimanga al di sotto di questo limite o che lo stesso errore persista, a meno che ovviamente non usi effettivamente $unwind
per effettuare la "INNER JOIN".