Wie bereits im Kommentar erwähnt, tritt der Fehler auf, weil beim Ausführen von $lookup
die standardmäßig ein Ziel-"Array" innerhalb des übergeordneten Dokuments aus den Ergebnissen der Fremdsammlung erzeugt, führt die Gesamtgröße der für dieses Array ausgewählten Dokumente dazu, dass das übergeordnete Dokument die 16-MB-BSON-Grenze überschreitet.
Der Zähler dafür ist mit einem $unwind
zu verarbeiten die unmittelbar auf $lookup
folgt Pipeline-Phase. Dies ändert tatsächlich das Verhalten von $lookup
in der Weise, dass anstatt ein Array im Elternteil zu erzeugen, die Ergebnisse stattdessen eine "Kopie" jedes Elternteils für jedes übereinstimmende Dokument sind.
So ziemlich wie die normale Verwendung von $unwind
, mit der Ausnahme, dass das unwinding
statt als "separate" Pipelinestufe verarbeitet wird Aktion wird tatsächlich zu $lookup
hinzugefügt Pipeline-Betrieb selbst. Idealerweise folgen Sie auch dem $unwind
mit einem $match
Bedingung, die auch einen matching
erzeugt Argument, das auch zu $lookup
hinzugefügt werden soll . Sie können dies tatsächlich im explain
sehen Ausgabe für die Pipeline.
Das Thema wird tatsächlich (kurz) in einem Abschnitt von Aggregation Pipeline Optimization in der Kerndokumentation behandelt:
$lookup + $unwind Koaleszenz
Neu in Version 3.2.
Wenn ein $unwind unmittelbar auf ein anderes $lookup folgt und das $unwind auf dem as-Feld des $lookup operiert, kann der Optimierer das $unwind in die $lookup-Stufe vereinigen. Dadurch wird die Erstellung großer Zwischendokumente vermieden.
Am besten demonstriert mit einem Listing, das den Server belastet, indem "verwandte" Dokumente erstellt werden, die die 16-MB-BSON-Grenze überschreiten würden. So kurz wie möglich ausgeführt, um das BSON-Limit zu brechen und zu umgehen:
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();
}
})();
Nach dem Einfügen einiger anfänglicher Daten versucht das Listing, ein Aggregat auszuführen, das lediglich aus $lookup
besteht was mit folgendem Fehler fehlschlagen wird:
{ MongoError:Gesamtgröße der Dokumente in der Edge-Matching-Pipeline { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } überschreitet die maximale Dokumentgröße
Was Ihnen im Grunde sagt, dass das BSON-Limit beim Abrufen überschritten wurde.
Im Gegensatz dazu fügt der nächste Versuch den $unwind
hinzu und $match
Pipeline-Stufen
Die Explain-Ausgabe :
{
"$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"
}
}
}
Und dieses Ergebnis gelingt natürlich, denn da die Ergebnisse nicht mehr in das übergeordnete Dokument eingefügt werden, kann die BSON-Grenze nicht überschritten werden.
Dies geschieht wirklich nur als Ergebnis des Hinzufügens von $unwind
nur, sondern das $match
wird zum Beispiel hinzugefügt, um zu zeigen, dass dies auch ist in $lookup
hinzugefügt Phase und dass der Gesamteffekt darin besteht, die zurückgegebenen Ergebnisse effektiv zu "begrenzen", da dies alles in diesem $lookup
erledigt wird Operation und es werden keine anderen Ergebnisse als die übereinstimmenden tatsächlich zurückgegeben.
Indem Sie auf diese Weise konstruieren, können Sie nach "referenzierten Daten" fragen, die das BSON-Limit überschreiten würden, und dann, wenn Sie $group
möchten die Ergebnisse zurück in ein Array-Format, sobald sie effektiv durch die "versteckte Abfrage" gefiltert wurden, die tatsächlich von $lookup
durchgeführt wird .
MongoDB 3.6 und höher – Zusätzlich für „LEFT JOIN“
Wie der gesamte obige Inhalt anmerkt, ist das BSON-Limit ein "hartes" Limit Grenze, die Sie nicht überschreiten können, und das ist im Allgemeinen der Grund für $unwind
ist als Zwischenschritt notwendig. Es gibt jedoch die Einschränkung, dass der "LEFT JOIN" aufgrund des $unwind
zu einem "INNER JOIN" wird wo es den Inhalt nicht bewahren kann. Auch preserveNulAndEmptyArrays
würde die "Koaleszenz" negieren und dennoch das intakte Array belassen, was das gleiche BSON-Limit-Problem verursacht.
MongoDB 3.6 fügt $lookup
eine neue Syntax hinzu Dies ermöglicht die Verwendung eines "Sub-Pipeline"-Ausdrucks anstelle der "lokalen" und "fremden" Schlüssel. Anstatt also die Option "Koaleszenz" wie gezeigt zu verwenden, ist es möglich, solange das erzeugte Array nicht auch die Grenze überschreitet, Bedingungen in diese Pipeline einzufügen, die das Array "intakt" und möglicherweise ohne Übereinstimmungen zurückgibt, wie dies ein Hinweis wäre eines "LEFT JOIN".
Der neue Ausdruck wäre dann:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
Tatsächlich wäre dies im Grunde das, was MongoDB unter der Decke tut mit der vorherigen Syntax seit 3.6 verwendet $expr
"intern", um die Aussage zu konstruieren. Der Unterschied besteht natürlich darin, dass es kein "unwinding"
gibt Option vorhanden, wie der $lookup
tatsächlich hingerichtet wird.
Wenn als Ergebnis der "pipeline"
tatsächlich keine Dokumente erstellt werden Ausdruck, dann ist das Zielarray innerhalb des Masterdokuments tatsächlich leer, genau wie ein "LEFT JOIN" es tatsächlich tut und das normale Verhalten von $lookup
wäre ohne weitere Optionen.
Das Ausgabe-Array darf jedoch NICHT dazu führen, dass das Dokument, in dem es erstellt wird, das BSON-Limit überschreitet . Es liegt also wirklich an Ihnen, dafür zu sorgen, dass alle "übereinstimmenden" Inhalte durch die Bedingungen unter dieser Grenze bleiben, oder derselbe Fehler bleibt bestehen, es sei denn, Sie verwenden tatsächlich $unwind
um den "INNER JOIN" zu bewirken.