Jak wspomniano wcześniej w komentarzu, błąd występuje, ponieważ podczas wykonywania $lookup
która domyślnie tworzy docelową "tablicę" w dokumencie nadrzędnym z wyników kolekcji zagranicznej, całkowity rozmiar dokumentów wybranych dla tej tablicy powoduje, że rodzic przekracza 16 MB limitu BSON.
Licznikiem tego jest przetwarzanie za pomocą $unwind
który bezpośrednio następuje po $lookup
etap rurociągu. To faktycznie zmienia zachowanie $lookup
w taki sposób, że zamiast tworzyć tablicę w rodzicu, wyniki są zamiast tego "kopią" każdego rodzica dla każdego dopasowanego dokumentu.
Prawie tak, jak zwykłe użycie $unwind
, z wyjątkiem tego, że zamiast przetwarzania jako „oddzielny” etap potoku, unwinding
akcja jest faktycznie dodawana do $lookup
samą obsługę rurociągu. Idealnie byłoby również podążać za $unwind
z $match
warunek, który również tworzy matching
argument do dodania również do $lookup
. Możesz to zobaczyć w explain
wyjście dla potoku.
Temat jest w rzeczywistości omówiony (krótko) w sekcji Optymalizacja rurociągu agregacji w podstawowej dokumentacji:
$lookup + $odpręż Koalescencja
Nowość w wersji 3.2.
Kiedy $odwijanie następuje natychmiast po kolejnym $wyglądaniu, a $odwijanie działa na polu jako $wyszukiwania, optymalizator może połączyć $odwijanie z etapem $wyszukiwania. Pozwala to uniknąć tworzenia dużych dokumentów pośrednich.
Najlepiej zademonstrować listę, która obciąża serwer, tworząc „powiązane” dokumenty, które przekraczają limit 16 MB BSON. Zrób tak krótko, jak to możliwe, aby zarówno złamać, jak i obejść limit 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();
}
})();
Po wstawieniu niektórych danych początkowych, listing spróbuje uruchomić agregat składający się jedynie z $lookup
który zakończy się niepowodzeniem z następującym błędem:
{ MongoError:Całkowity rozmiar dokumentów w potoku dopasowania krawędzi { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } przekracza maksymalny rozmiar dokumentu
Co w zasadzie oznacza, że limit BSON został przekroczony podczas pobierania.
Natomiast następna próba dodaje $unwind
i $match
etapy rurociągu
Wyjście Wyjaśnij :
{
"$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"
}
}
}
I ten wynik oczywiście się powiedzie, ponieważ ponieważ wyniki nie są już umieszczane w dokumencie nadrzędnym, nie można przekroczyć limitu BSON.
Tak naprawdę dzieje się to w wyniku dodania $unwind
tylko, ale $match
jest dodawany na przykład, aby pokazać, że jest to również dodane do $lookup
etapie i że ogólnym efektem jest „ograniczenie” zwracanych wyników w skuteczny sposób, ponieważ wszystko odbywa się w tym $lookup
operacja i żadne inne wyniki poza pasującymi nie są w rzeczywistości zwracane.
Konstruując w ten sposób możesz zapytać o "dane referencyjne", które przekroczyłyby limit BSON, a następnie, jeśli chcesz, $group
wyniki z powrotem do formatu tablicy, gdy zostaną skutecznie przefiltrowane przez „ukryte zapytanie”, które jest faktycznie wykonywane przez $lookup
.
MongoDB 3.6 i nowsze — dodatkowe dla „LEFT JOIN”
Jak zauważają wszystkie powyższe treści, limit BSON jest "twardy" limit, którego nie można przekroczyć i dlatego generalnie $unwind
jest konieczne jako etap przejściowy. Istnieje jednak ograniczenie polegające na tym, że „LEFT JOIN” staje się „INNER JOIN” na mocy $unwind
gdzie nie może zachować treści. Nawet preserveNulAndEmptyArrays
zanegowałby „koalescencję” i nadal pozostawiłby nienaruszoną tablicę, powodując ten sam problem z limitem BSON.
MongoDB 3.6 dodaje nową składnię do $lookup
co pozwala na użycie wyrażenia „podpotoku” zamiast kluczy „lokalnego” i „obcego”. Więc zamiast używać opcji „koalescencji”, jak pokazano, o ile wyprodukowana tablica nie przekracza również limitu, można umieścić warunki w tym potoku, który zwróci tablicę „nienaruszoną” i prawdopodobnie bez dopasowań, co byłoby orientacyjne „LEWE DOŁĄCZENIE”.
Nowe wyrażenie byłoby wtedy:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
W rzeczywistości jest to w zasadzie to, co MongoDB robi „pod przykrywką” z poprzednią składnią od wersji 3.6 używa $expr
„wewnętrznie” w celu skonstruowania instrukcji. Różnica oczywiście polega na tym, że nie ma "unwinding"
opcja obecna w sposobie $lookup
faktycznie zostanie wykonany.
Jeśli żadne dokumenty nie są faktycznie tworzone w wyniku "pipeline"
wyrażenie, wtedy tablica docelowa w dokumencie głównym będzie w rzeczywistości pusta, tak jak robi to „LEFT JOIN” i byłoby normalnym zachowaniem $lookup
bez żadnych innych opcji.
Jednak tablica wyjściowa NIE MOŻE powodować, że dokument, w którym jest tworzony, przekracza limit BSON . Tak więc to naprawdę zależy od ciebie, aby upewnić się, że jakakolwiek "pasująca" zawartość według warunków pozostanie w tym limicie lub ten sam błąd będzie się powtarzał, chyba że faktycznie użyjesz $unwind
aby wykonać „WEWNĘTRZNE POŁĄCZENIE”.