Som nævnt tidligere i kommentaren opstår fejlen, fordi når du udfører $lookup
som som standard producerer et mål "array" i det overordnede dokument ud fra resultaterne af den udenlandske indsamling, får den samlede størrelse af dokumenter, der er valgt for det array, det overordnede til at overskride 16 MB BSON-grænsen.
Tælleren for dette er at behandle med en $unwind
som umiddelbart følger $lookup
pipeline fase. Dette ændrer faktisk adfærden for $lookup
på en sådan måde, at resultaterne i stedet for at producere en matrix i det overordnede, i stedet er en "kopi" af hver forælder for hvert matchede dokument.
Stort set ligesom almindelig brug af $unwind
, med den undtagelse, at unwinding
i stedet for at behandle som et "separat" pipelinetrin handling er faktisk tilføjet til $lookup
selve rørledningsdriften. Ideelt set følger du også $unwind
med en $match
betingelse, som også skaber en matching
argument skal også tilføjes til $lookup
. Du kan faktisk se dette i explain
output for rørledningen.
Emnet er faktisk dækket (kort) i et afsnit af Aggregation Pipeline Optimization i kernedokumentationen:
$lookup + $unwind Coalescence
Ny i version 3.2.
Når et $unwind umiddelbart følger efter et andet $opslag, og $unwind opererer på as-feltet i $opslag, kan optimeringsværktøjet samle $unwind ind i $opslagsstadiet. Dette undgår at skabe store mellemliggende dokumenter.
Bedst demonstreret med en liste, der sætter serveren under stress ved at skabe "relaterede" dokumenter, der ville overskride 16 MB BSON-grænsen. Udført så kort som muligt for både at bryde og omgå BSON-grænsen:
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();
}
})();
Efter at have indsat nogle indledende data, vil fortegnelsen forsøge at køre et aggregat, der kun består af $lookup
som vil mislykkes med følgende fejl:
{ MongoError:Samlet størrelse af dokumenter i kantmatchende pipeline { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } overstiger den maksimale dokumentstørrelse
Hvilket grundlæggende fortæller dig, at BSON-grænsen blev overskredet ved hentning.
I modsætning hertil tilføjer næste forsøg $unwind
og $match
pipeline stadier
Forklar-output :
{
"$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"
}
}
}
Og det resultat lykkes selvfølgelig, for da resultaterne ikke længere placeres i det overordnede dokument, kan BSON-grænsen ikke overskrides.
Dette sker egentlig bare som et resultat af tilføjelse af $unwind
kun, men $match
tilføjes f.eks. for at vise, at dette også er tilføjet i $lookup
fase, og at den overordnede effekt er at "begrænse" de resultater, der returneres på en effektiv måde, da det hele foregår i den $lookup
operation og ingen andre resultater end de matchende returneres faktisk.
Ved at konstruere på denne måde kan du forespørge efter "referencedata", der ville overskride BSON-grænsen, og derefter om du vil have $group
resultaterne tilbage i et array-format, når de er blevet effektivt filtreret af den "skjulte forespørgsel", der rent faktisk udføres af $lookup
.
MongoDB 3.6 og nyere - Yderligere til "LEFT JOIN"
Som alt indholdet ovenfor bemærker, er BSON-grænsen en "hård" grænse, som du ikke kan overtræde, og det er generelt derfor $unwind
er nødvendigt som et midlertidigt skridt. Der er dog den begrænsning, at "LEFT JOIN" bliver en "INNER JOIN" i kraft af $unwind
hvor den ikke kan bevare indholdet. Også selv preserveNulAndEmptyArrays
ville ophæve "sammensmeltningen" og stadig forlade det intakte array, hvilket forårsager det samme BSON Limit-problem.
MongoDB 3.6 tilføjer ny syntaks til $lookup
der gør det muligt at bruge et "sub-pipeline" udtryk i stedet for de "lokale" og "fremmede" nøgler. Så i stedet for at bruge "sammensmeltnings"-muligheden som vist, så længe det producerede array ikke også overskrider grænsen, er det muligt at sætte betingelser i den pipeline, som returnerer arrayet "intakt", og muligvis uden match, hvilket ville være vejledende af en "LEFT JOIN".
Det nye udtryk ville så være:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
Faktisk ville dette dybest set være, hvad MongoDB laver "under dynen" med den tidligere syntaks siden 3.6 bruger $expr
"internt" for at konstruere udsagnet. Forskellen er selvfølgelig, at der ikke er nogen "unwinding"
mulighed til stede i hvordan $lookup
faktisk bliver henrettet.
Hvis der faktisk ikke produceres dokumenter som følge af "pipeline"
udtryk, så vil målarrayet i masterdokumentet faktisk være tomt, ligesom en "LEFT JOIN" faktisk gør og ville være den normale adfærd for $lookup
uden andre muligheder.
Output-arrayet til MÅ IKKE forårsage, at dokumentet, hvor det oprettes, overskrider BSON-grænsen . Så det er virkelig op til dig at sikre, at alt "matchende" indhold efter betingelserne forbliver under denne grænse, ellers vil den samme fejl fortsætte, medmindre du selvfølgelig rent faktisk bruger $unwind
for at udføre "INNER JOIN".