MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

Agregacja Mongodb $grupa, ogranicz długość tablicy

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 na false 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ść?



  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. MongoDB $podłoga

  2. zapisz adres IP w mongoDB

  3. Jak usunąć element z podwójnie zagnieżdżonej tablicy w dokumencie MongoDB.

  4. Aktualizacja do ClusterControl Enterprise Edition

  5. Jak zwiększyć wydajność operacji aktualizacji w Mongo?