Nowoczesne
Od MongoDB 3.6 istnieje "nowatorskie" podejście do tego, używając $lookup
do wykonania "samołączenia" w taki sam sposób, jak oryginalne przetwarzanie kursora pokazane poniżej.
Ponieważ w tej wersji możesz określić "pipeline"
argument do $lookup
jako źródło „dołączenia”, oznacza to zasadniczo, że możesz użyć $match
i $limit
zebrać i "ograniczyć" wpisy w tablicy:
db.messages.aggregate([
{ "$group": { "_id": "$conversation_ID" } },
{ "$lookup": {
"from": "messages",
"let": { "conversation": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$conversation_ID", "$$conversation" ] } }},
{ "$limit": 10 },
{ "$project": { "_id": 1 } }
],
"as": "msgs"
}}
])
Możesz opcjonalnie dodać dodatkową projekcję po $lookup
aby elementy tablicy były po prostu wartościami, a nie dokumentami z _id
klucz, ale podstawowy wynik można uzyskać, wykonując powyższe czynności.
Nadal istnieje zaległy SERWER-9277, który faktycznie żąda „limitu do wypchnięcia” bezpośrednio, ale przy użyciu $lookup
w ten sposób jest realną alternatywą w międzyczasie.
UWAGA :Istnieje również $slice
który został wprowadzony po napisaniu oryginalnej odpowiedzi i wymieniony przez „wyjątkowy problem JIRA” w oryginalnej treści. Chociaż możesz uzyskać ten sam wynik z małymi zestawami wyników, nadal wiąże się to z „wpychaniem wszystkiego” do tablicy, a następnie ograniczaniem końcowego wyniku tablicy do pożądanej długości.
To jest główna różnica i dlaczego generalnie nie jest praktyczne $slice
dla dużych wyników. Ale oczywiście może być używany naprzemiennie w przypadkach, w których tak jest.
Istnieje kilka dodatkowych szczegółów na temat wartości grup mongodb według wielu pól dotyczących alternatywnego użycia.
Oryginał
Jak wspomniano wcześniej, nie jest to niemożliwe, ale z pewnością jest to straszny problem.
Właściwie, jeśli głównym problemem jest to, że wynikowe tablice będą wyjątkowo duże, najlepszym rozwiązaniem jest przesłanie dla każdego odrębnego „identyfikatora_rozmowy” jako osobne zapytanie, a następnie połączenie wyników. W samej składni MongoDB 2.6, która może wymagać pewnych poprawek w zależności od tego, jaka jest implementacja języka:
var results = [];
db.messages.aggregate([
{ "$group": {
"_id": "$conversation_ID"
}}
]).forEach(function(doc) {
db.messages.aggregate([
{ "$match": { "conversation_ID": doc._id } },
{ "$limit": 10 },
{ "$group": {
"_id": "$conversation_ID",
"msgs": { "$push": "$_id" }
}}
]).forEach(function(res) {
results.push( res );
});
});
Ale wszystko zależy od tego, czy starasz się tego uniknąć. Przejdźmy do prawdziwej odpowiedzi:
Pierwszym problemem jest to, że nie istnieje funkcja „ograniczająca” liczbę elementów, które są „wpychane” do tablicy. Jest to z pewnością coś, co chcielibyśmy, ale ta funkcjonalność nie istnieje.
Drugi problem polega na tym, że nawet podczas umieszczania wszystkich elementów w tablicy nie można użyć $slice
lub dowolny podobny operator w potoku agregacji. Nie ma więc sposobu na uzyskanie tylko „top 10” wyników z wytworzonej tablicy za pomocą prostej operacji.
Ale w rzeczywistości możesz stworzyć zestaw operacji, aby skutecznie „wyciąć” granice grupowania. Jest to dość skomplikowane i na przykład tutaj zmniejszę elementy tablicy „pokrojone” tylko do „sześciu”. Głównym powodem jest zademonstrowanie procesu i pokazanie, jak to zrobić bez destrukcji za pomocą tablic, które nie zawierają sumy, na którą chcesz „pociąć”.
Biorąc pod uwagę próbkę dokumentów:
{ "_id" : 1, "conversation_ID" : 123 }
{ "_id" : 2, "conversation_ID" : 123 }
{ "_id" : 3, "conversation_ID" : 123 }
{ "_id" : 4, "conversation_ID" : 123 }
{ "_id" : 5, "conversation_ID" : 123 }
{ "_id" : 6, "conversation_ID" : 123 }
{ "_id" : 7, "conversation_ID" : 123 }
{ "_id" : 8, "conversation_ID" : 123 }
{ "_id" : 9, "conversation_ID" : 123 }
{ "_id" : 10, "conversation_ID" : 123 }
{ "_id" : 11, "conversation_ID" : 123 }
{ "_id" : 12, "conversation_ID" : 456 }
{ "_id" : 13, "conversation_ID" : 456 }
{ "_id" : 14, "conversation_ID" : 456 }
{ "_id" : 15, "conversation_ID" : 456 }
{ "_id" : 16, "conversation_ID" : 456 }
Widać tam, że grupując według warunków otrzymasz jedną tablicę z dziesięcioma elementami, a drugą z „pięciu”. To, co chcesz tutaj zrobić, zredukuj oba do pierwszych „sześciu” bez „niszczenia” tablicy, która będzie pasować tylko do „pięciu” elementów.
Oraz następujące zapytanie:
db.messages.aggregate([
{ "$group": {
"_id": "$conversation_ID",
"first": { "$first": "$_id" },
"msgs": { "$push": "$_id" },
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"seen": { "$eq": [ "$first", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"seen": { "$eq": [ "$second", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"seen": { "$eq": [ "$third", "$msgs" ] },
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"seen": { "$eq": [ "$forth", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$forth" },
"fifth": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"fifth": 1,
"seen": { "$eq": [ "$fifth", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$forth" },
"fifth": { "$first": "$fifth" },
"sixth": { "$first": "$msgs" },
}},
{ "$project": {
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"fifth": 1,
"sixth": 1,
"pos": { "$const": [ 1,2,3,4,5,6 ] }
}},
{ "$unwind": "$pos" },
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [
{ "$eq": [ "$pos", 1 ] },
"$first",
{ "$cond": [
{ "$eq": [ "$pos", 2 ] },
"$second",
{ "$cond": [
{ "$eq": [ "$pos", 3 ] },
"$third",
{ "$cond": [
{ "$eq": [ "$pos", 4 ] },
"$forth",
{ "$cond": [
{ "$eq": [ "$pos", 5 ] },
"$fifth",
{ "$cond": [
{ "$eq": [ "$pos", 6 ] },
"$sixth",
false
]}
]}
]}
]}
]}
]
}
}
}},
{ "$unwind": "$msgs" },
{ "$match": { "msgs": { "$ne": false } }},
{ "$group": {
"_id": "$_id",
"msgs": { "$push": "$msgs" }
}}
])
Otrzymasz najlepsze wyniki w tablicy, do sześciu wpisów:
{ "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] }
{ "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] }
Jak widać tutaj, mnóstwo zabawy.
Po początkowym zgrupowaniu chcesz w zasadzie „wyskoczyć” $first
wartość ze stosu dla wyników tablicy. Aby uprościć ten proces, robimy to w początkowej operacji. Tak więc proces staje się:
$unwind
tablica- Porównaj z wartościami już widzianymi za pomocą
$eq
dopasowanie równości $sort
wyniki do "pływania"false
niewidoczne wartości do góry (to nadal zachowuje kolejność)$group
z powrotem i "pop"$first
niewidoczna wartość jako następny członek na stosie. Używa to również$cond
operator do zamiany "widzianych" wartości w stosie tablic nafalse
aby pomóc w ocenie.
Ostatnia akcja z $cond
jest po to, aby upewnić się, że przyszłe iteracje nie będą tylko dodawały w kółko ostatniej wartości tablicy, gdy liczba „wycinków” jest większa niż liczba członków tablicy.
Cały ten proces należy powtórzyć dla tylu elementów, ile chcesz „pokroić”. Ponieważ znaleźliśmy już „pierwszy” element w początkowym grupowaniu, oznacza to n-1
iteracje dla pożądanego wyniku wykrojenia.
Ostatnie kroki są tak naprawdę tylko opcjonalną ilustracją konwersji wszystkiego z powrotem na tablice dla wyniku, jak pokazano w końcu. Więc tak naprawdę tylko warunkowe wypychanie elementów lub false
z powrotem według ich pasującej pozycji i wreszcie "odfiltrowanie" wszystkich false
wartości, więc tablice końcowe mają odpowiednio „sześć” i „pięć”.
Nie ma więc standardowego operatora, który by to obejmował, i nie można po prostu „ograniczyć” wypychania do 5 lub 10 lub jakichkolwiek innych elementów w tablicy. Ale jeśli naprawdę musisz to zrobić, to jest to najlepsze podejście.
Możesz ewentualnie podejść do tego za pomocą mapReduce i całkowicie porzucić strukturę agregacji. Podejście, które bym przyjął (w rozsądnych granicach) polegałoby na efektywnym umieszczeniu mapy haszującej w pamięci na serwerze i akumulowaniu do niej tablic, przy jednoczesnym użyciu wycinka JavaScript do "ograniczenia" wyników:
db.messages.mapReduce(
function () {
if ( !stash.hasOwnProperty(this.conversation_ID) ) {
stash[this.conversation_ID] = [];
}
if ( stash[this.conversation_ID.length < maxLen ) {
stash[this.conversation_ID].push( this._id );
emit( this.conversation_ID, 1 );
}
},
function(key,values) {
return 1; // really just want to keep the keys
},
{
"scope": { "stash": {}, "maxLen": 10 },
"finalize": function(key,value) {
return { "msgs": stash[key] };
},
"out": { "inline": 1 }
}
)
Tak więc po prostu buduje się obiekt "w pamięci" pasujący do emitowanych "kluczy" z tablicą nigdy nie przekraczającą maksymalnego rozmiaru, który chcesz pobrać z wyników. Dodatkowo nie przeszkadza to nawet w „emitowaniu” przedmiotu po osiągnięciu maksymalnego stosu.
W rzeczywistości część zmniejszania nie robi nic innego, jak po prostu zmniejsza się do „klucza” i pojedynczej wartości. Tak więc na wypadek, gdyby nasz reduktor nie został wywołany, co byłoby prawdą, gdyby dla klucza istniała tylko 1 wartość, funkcja finalize zajmuje się mapowaniem kluczy „ukryj” do końcowego wyniku.
Skuteczność tego zależy od rozmiaru danych wyjściowych, a ocena JavaScript z pewnością nie jest szybka, ale prawdopodobnie szybsza niż przetwarzanie dużych tablic w potoku.
Zagłosuj na kwestie JIRA, aby faktycznie mieć operator „plaster”, a nawet „limit” dla „$push” i „$addToSet”, które byłyby przydatne. Osobiście mam nadzieję, że przynajmniej niektóre modyfikacje mogą zostać wprowadzone do $map
operatora, aby ujawnić wartość „bieżącego indeksu” podczas przetwarzania. To skutecznie umożliwiłoby „krojenie” i inne operacje.
Naprawdę chciałbyś to zakodować, aby "wygenerować" wszystkie wymagane iteracje. Jeśli odpowiedź na to pytanie dostanie wystarczająco dużo miłości i/lub innego czasu do załatwienia, to mogę dodać trochę kodu, aby zademonstrować, jak to zrobić. To już dość długa odpowiedź.
Kod do generowania potoku:
var key = "$conversation_ID";
var val = "$_id";
var maxLen = 10;
var stack = [];
var pipe = [];
var fproj = { "$project": { "pos": { "$const": [] } } };
for ( var x = 1; x <= maxLen; x++ ) {
fproj["$project"][""+x] = 1;
fproj["$project"]["pos"]["$const"].push( x );
var rec = {
"$cond": [ { "$eq": [ "$pos", x ] }, "$"+x ]
};
if ( stack.length == 0 ) {
rec["$cond"].push( false );
} else {
lval = stack.pop();
rec["$cond"].push( lval );
}
stack.push( rec );
if ( x == 1) {
pipe.push({ "$group": {
"_id": key,
"1": { "$first": val },
"msgs": { "$push": val }
}});
} else {
pipe.push({ "$unwind": "$msgs" });
var proj = {
"$project": {
"msgs": 1
}
};
proj["$project"]["seen"] = { "$eq": [ "$"+(x-1), "$msgs" ] };
var grp = {
"$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
}
}
};
for ( n=x; n >= 1; n-- ) {
if ( n != x )
proj["$project"][""+n] = 1;
grp["$group"][""+n] = ( n == x ) ? { "$first": "$msgs" } : { "$first": "$"+n };
}
pipe.push( proj );
pipe.push({ "$sort": { "seen": 1 } });
pipe.push(grp);
}
}
pipe.push(fproj);
pipe.push({ "$unwind": "$pos" });
pipe.push({
"$group": {
"_id": "$_id",
"msgs": { "$push": stack[0] }
}
});
pipe.push({ "$unwind": "$msgs" });
pipe.push({ "$match": { "msgs": { "$ne": false } }});
pipe.push({
"$group": {
"_id": "$_id",
"msgs": { "$push": "$msgs" }
}
});
To buduje podstawowe podejście iteracyjne aż do maxLen
z krokami z $unwind
do $group
. Zawarte w nim są również szczegóły dotyczące wymaganych projekcji końcowych i „zagnieżdżonej” instrukcji warunkowej. Ostatnie to w zasadzie podejście przyjęte w odpowiedzi na to pytanie:
Czy klauzula $in MongoDB gwarantuje kolejność?