sql >> Database >  >> NoSQL >> MongoDB

Mongodb-aggregatie $groep, beperk de lengte van de array

Modern

Vanaf MongoDB 3.6 is er een "nieuwe" benadering hiervoor door gebruik te maken van $lookup om een ​​"self join" uit te voeren op vrijwel dezelfde manier als de originele cursorverwerking die hieronder wordt getoond.

Aangezien je in deze release een "pipeline" kunt specificeren argument voor $lookup als bron voor de "join", betekent dit in wezen dat je $match . kunt gebruiken en $limit om de gegevens voor de array te verzamelen en te "beperken":

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"
  }}
])

U kunt optioneel extra projectie toevoegen na de $lookup om de array-items eenvoudig de waarden te maken in plaats van documenten met een _id sleutel, maar het basisresultaat is er door simpelweg het bovenstaande te doen.

Er is nog steeds de uitstekende SERVER-9277 die in feite rechtstreeks een "limiet om te pushen" aanvraagt, maar met behulp van $lookup op deze manier is in de tussentijd een levensvatbaar alternatief.

OPMERKING :Er is ook $slice die werd geïntroduceerd na het schrijven van het oorspronkelijke antwoord en genoemd door "uitstekend JIRA-probleem" in de originele inhoud. Hoewel je hetzelfde resultaat kunt krijgen met kleine resultaatsets, moet je nog steeds "alles in de array duwen" en later de uiteindelijke array-uitvoer beperken tot de gewenste lengte.

Dus dat is het belangrijkste onderscheid en waarom het over het algemeen niet praktisch is om $slice voor grote resultaten. Maar kan natuurlijk afwisselend worden gebruikt in gevallen waar het is.

Er zijn nog een paar details over mongodb-groepswaarden door meerdere velden over alternatief gebruik.

Origineel

Zoals eerder vermeld, is dit niet onmogelijk maar zeker een vreselijk probleem.

Als uw grootste zorg is dat uw resulterende arrays uitzonderlijk groot zullen zijn, dan kunt u het beste voor elke afzonderlijke "conversation_ID" een afzonderlijke query indienen en vervolgens uw resultaten combineren. In zeer MongoDB 2.6-syntaxis die misschien wat moet worden aangepast, afhankelijk van wat uw taalimplementatie eigenlijk is:

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 );
    });
});

Maar het hangt er allemaal van af of je dat probeert te vermijden. Dus op naar het echte antwoord:

Het eerste probleem hier is dat er geen functie is om het aantal items dat in een array wordt "geduwd" te "beperken". Het is zeker iets dat we zouden willen, maar de functionaliteit bestaat momenteel niet.

Het tweede probleem is dat zelfs wanneer u alle items in een array pusht, u $slice niet kunt gebruiken , of een vergelijkbare operator in de aggregatiepijplijn. Er is dus geen huidige manier om met een simpele handeling alleen de "top 10" resultaten van een geproduceerde array te krijgen.

Maar u kunt in feite een reeks bewerkingen produceren om effectief op uw groeperingsgrenzen te "snijden". Het is behoorlijk ingewikkeld, en hier zal ik bijvoorbeeld de array-elementen "sliced" reduceren tot alleen "zes". De belangrijkste reden hier is om het proces te demonstreren en te laten zien hoe je dit kunt doen zonder destructief te zijn met arrays die niet het totaal bevatten waarnaar je wilt "slicen".

Gegeven een voorbeeld van documenten:

{ "_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 }

U kunt daar zien dat u bij het groeperen op uw voorwaarden een array krijgt met tien elementen en een andere met "vijf". Wat u hier wilt doen, verminder beide tot de top "zes" zonder de array te "vernietigen" die alleen overeenkomt met "vijf" elementen.

En de volgende vraag:

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" }
    }}
])

U krijgt de beste resultaten in de array, maximaal zes items:

{ "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] }
{ "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] }

Zoals je hier kunt zien, veel plezier.

Nadat je in eerste instantie hebt gegroepeerd, wil je in feite de $first waarde uit de stapel voor de arrayresultaten. Om dit proces een beetje te vereenvoudigen, doen we dit eigenlijk in de eerste operatie. Dus het proces wordt:

  • $unwind de reeks
  • Vergelijk met de waarden die al gezien zijn met een $eq gelijkheidswedstrijd
  • $sort de resultaten "zweven" false ongeziene waarden naar de top (dit behoudt nog steeds de volgorde)
  • $group weer terug en "pop" de $first ongeziene waarde als het volgende lid op de stapel. Dit gebruikt ook de $cond operator om "gezien" waarden in de array-stack te vervangen door false om te helpen bij de evaluatie.

De laatste actie met $cond is er om ervoor te zorgen dat toekomstige iteraties niet alleen de laatste waarde van de array steeds opnieuw toevoegen waar het aantal "slices" groter is dan de arrayleden.

Dat hele proces moet worden herhaald voor zoveel items als u wilt "snijden". Aangezien we het "eerste" item in de initiële groepering al hebben gevonden, betekent dit n-1 iteraties voor het gewenste slice-resultaat.

De laatste stappen zijn eigenlijk slechts een optionele illustratie van het converteren van alles terug naar arrays voor het resultaat zoals uiteindelijk wordt getoond. Dus eigenlijk gewoon voorwaardelijk items pushen of false terug op hun overeenkomende positie en tenslotte "filteren" alle false waarden zodat de eindarrays respectievelijk "zes" en "vijf" leden hebben.

Er is dus geen standaardoperator om dit aan te passen, en je kunt de push niet zomaar "beperken" tot 5 of 10 of wat dan ook in de array. Maar als je het echt moet doen, dan is dit je beste aanpak.

U kunt dit mogelijk benaderen met mapReduce en het aggregatieraamwerk helemaal verlaten. De benadering die ik zou nemen (binnen redelijke limieten) zou zijn om effectief een in-memory hash-map op de server te hebben en daar arrays op te verzamelen, terwijl ik JavaScript-segment gebruik om de resultaten te "beperken":

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 }
    }
)

Dus dat bouwt in feite het "in-memory" -object op dat overeenkomt met de uitgezonden "sleutels" met een array die nooit de maximale grootte overschrijdt die u uit uw resultaten wilt halen. Bovendien neemt dit niet eens de moeite om het item te "uitzenden" wanneer de maximale stapel is bereikt.

Het reduceergedeelte doet eigenlijk niets anders dan in wezen gewoon reduceren tot "sleutel" en een enkele waarde. Dus voor het geval dat onze reducer niet wordt aangeroepen, zoals het geval zou zijn als er maar 1 waarde voor een sleutel bestond, zorgt de finalize-functie ervoor dat de "stash"-sleutels worden toegewezen aan de uiteindelijke uitvoer.

De effectiviteit hiervan hangt af van de grootte van de uitvoer, en JavaScript-evaluatie is zeker niet snel, maar mogelijk sneller dan het verwerken van grote arrays in een pijplijn.

Stem op de JIRA-problemen om daadwerkelijk een "slice" -operator of zelfs een "limiet" op "$push" en "$addToSet" te hebben, wat beide handig zou zijn. Ik hoop persoonlijk dat er op zijn minst enige wijziging kan worden aangebracht aan de $map operator om de waarde van de "huidige index" weer te geven tijdens de verwerking. Dat zou effectief "snijden" en andere bewerkingen mogelijk maken.

Je zou dit echt willen coderen om alle vereiste iteraties te "genereren". Als het antwoord hier genoeg liefde en/of andere tijd in afwachting krijgt die ik in tuits heb, dan kan ik wat code toevoegen om te demonstreren hoe dit te doen. Het is al een redelijk lange reactie.

Code om pijplijn te genereren:

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" }
    }
}); 

Dat bouwt de iteratieve basisbenadering op tot maxLen met de stappen van $unwind naar $group . Ook zijn er details ingebed over de vereiste definitieve projecties en de "geneste" voorwaardelijke verklaring. De laatste is eigenlijk de benadering van deze vraag:

Garandeert de $in-clausule van MongoDB de bestelling?



  1. Het systeem is niet opgestart met systemd als init-systeem (PID 1). Kan niet werken

  2. Honderden tellers tegelijk verhogen, redis of mongodb?

  3. Maak verbinding met host mongodb vanuit docker-container

  4. Verwijderen door _id in MongoDB-console