sql >> Database >  >> NoSQL >> MongoDB

mongoDB Aggregation:som op basis van arraynamen

Hier zit veel in, vooral als je relatief nieuw bent in het gebruik van aggregeren , maar het kan klaar zijn. Ik zal de fasen na de lijst uitleggen:

db.collection.aggregate([

    // 1. Unwind both arrays
    {"$unwind": "$win"},
    {"$unwind": "$loss"},

    // 2. Cast each field with a type and the array on the end
    {"$project":{ 
        "win.player": "$win.player",
        "win.type": {"$cond":[1,"win",0]},
        "loss.player": "$loss.player", 
        "loss.type": {"$cond": [1,"loss",0]}, 
        "score": {"$cond":[1,["win", "loss"],0]} 
    }},

    // Unwind the "score" array
    {"$unwind": "$score"},

    // 3. Reshape to "result" based on the value of "score"
    {"$project": { 
        "result.player": {"$cond": [
            {"$eq": ["$win.type","$score"]},
            "$win.player", 
            "$loss.player"
        ] },
        "result.type": {"$cond": [
            {"$eq":["$win.type", "$score"]},
            "$win.type",
            "$loss.type"
        ]}
    }},

    // 4. Get all unique result within each document 
    {"$group": { "_id": { "_id":"$_id", "result": "$result" } }},

    // 5. Sum wins and losses across documents
    {"$group": { 
        "_id": "$_id.result.player", 
        "wins": {"$sum": {"$cond": [
            {"$eq":["$_id.result.type","win"]},1,0
        ]}}, 
        "losses": {"$sum":{"$cond": [
            {"$eq":["$_id.result.type","loss"]},1,0
        ]}}
    }}
])

Samenvatting

Dit veronderstelt wel dat de "spelers" in elke "win" en "loss" array om te beginnen allemaal uniek zijn. Dat leek redelijk voor wat hier gemodelleerd leek te zijn:

  1. Ontspan beide arrays. Dit creëert duplicaten, maar deze worden later verwijderd.

  2. Bij het projecteren is er enig gebruik van de $cond operator (een ternair) om enkele letterlijke tekenreekswaarden te krijgen. En het laatste gebruik is speciaal, omdat en array wordt toegevoegd. Dus na het projecteren wordt die array weer afgewikkeld. Meer duplicaten, maar dat is het punt. Eén "win", één "verlies" record voor elk.

  3. Meer projectie met de $cond operator en het gebruik van de $eq exploitant ook. Deze keer gaan we samenvoegen de twee velden in één. Dus als u dit gebruikt, wanneer het "type" van het veld overeenkomt met de waarde in "score", dan wordt dat "sleutelveld" gebruikt voor de veldwaarde "resultaat". Het resultaat is dat de twee verschillende "win"- en "verlies"-velden nu dezelfde naam hebben, geïdentificeerd door "type".

  4. Het wegwerken van de duplicaten in elk document. Eenvoudig groeperen op het document _id en de "resultaat" velden als sleutels. Nu zouden er dezelfde "win"- en "verlies"-records moeten zijn als in het originele document, alleen in een andere vorm omdat ze uit de arrays worden verwijderd.

  5. Groepeer tenslotte alle documenten om de totalen per "speler" te krijgen. Meer gebruik van $cond en $eq maar deze keer om te bepalen of het huidige document een "winst" of een "verlies" is. Dus waar dit overeenkomt, retourneren we 1 en waar false retourneren we 0. Die waarden worden doorgegeven aan $som om de totale tellingen voor "winsten" en "verliezen" te krijgen.

En dat verklaart hoe je tot het resultaat kunt komen.

Meer informatie over de aggregatie-operators uit de documentatie. Enkele van de "grappige" toepassingen voor $cond in die lijst moet kunnen worden vervangen door een $ letterlijk exploitant. Maar dat zal pas beschikbaar zijn als versie 2.6 en hoger wordt vrijgegeven.

"Vereenvoudigde" case voor MongoDB 2.6 en hoger

Natuurlijk is er een nieuwe set operators in wat de aanstaande release is op het moment van schrijven, wat dit enigszins zal helpen vereenvoudigen:

db.collection.aggregate([
    { "$unwind": "$win" },
    { "$project": {
        "win.player": "$win.player",
        "win.type": { "$literal": "win" },
        "loss": 1,
    }},
    { "$group": {
        "_id" : {
            "_id": "$_id",
            "loss": "$loss"
        },
        "win": { "$push": "$win" }
    }},
    { "$unwind": "$_id.loss" },
    { "$project": {
        "loss.player": "$_id.loss.player",
        "loss.type": { "$literal": "loss" },
        "win": 1,
    }},
    { "$group": {
        "_id" : {
            "_id": "$_id._id",
            "win": "$win"
        },
        "loss": { "$push": "$loss" }
    }},
    { "$project": {
        "_id": "$_id._id",
        "results": { "$setUnion": [ "$_id.win", "$loss" ] }
    }},
    { "$unwind": "$results" },
    { "$group": { 
        "_id": "$results.player", 
        "wins": {"$sum": {"$cond": [
            {"$eq":["$results.type","win"]},1,0
        ]}}, 
        "losses": {"$sum":{"$cond": [
            {"$eq":["$results.type","loss"]},1,0
        ]}}
    }}

])

Maar "vereenvoudigd" is discutabel. Voor mij "voelt" dat gewoon alsof het "rondhangt" en meer werk doet. Het is zeker traditioneler, omdat het gewoon vertrouwt op $ setUnion samenvoegen de array resultaten.

Maar dat "werk" zou teniet worden gedaan door je schema een beetje te veranderen, zoals hier getoond:

{
    "_id" : ObjectId("531ea2b1fcc997d5cc5cbbc9"),
    "win": [
        {
            "player" : "Player2",
            "type" : "win"
        },
        {
            "player" : "Player4",
            "type" : "win"
        }
    ],
    "loss" : [
        {
            "player" : "Player6",
            "type" : "loss"
        },
        {
            "player" : "Player5",
            "type" : "loss"
        },
    ]
}

En dit elimineert de noodzaak om de array-inhoud te projecteren door het "type" attribuut toe te voegen zoals we hebben gedaan, en vermindert de query en het werk gedaan:

db.collection.aggregate([
    { "$project": {
        "results": { "$setUnion": [ "$win", "$loss" ] }
    }},
    { "$unwind": "$results" },
    { "$group": { 
        "_id": "$results.player", 
        "wins": {"$sum": {"$cond": [
            {"$eq":["$results.type","win"]},1,0
        ]}}, 
        "losses": {"$sum":{"$cond": [
            {"$eq":["$results.type","loss"]},1,0
        ]}}
    }}

])

En natuurlijk gewoon je schema als volgt wijzigen:

{
    "_id" : ObjectId("531ea2b1fcc997d5cc5cbbc9"),
    "results" : [
        {
            "player" : "Player6",
            "type" : "loss"
        },
        {
            "player" : "Player5",
            "type" : "loss"
        },
        {
            "player" : "Player2",
            "type" : "win"
        },
        {
            "player" : "Player4",
            "type" : "win"
        }
    ]
}

Dat maakt dingen heel eenvoudig. En dit kan worden gedaan in versies vóór 2.6. Dus je zou het nu meteen kunnen doen:

db.collection.aggregate([
    { "$unwind": "$results" },
    { "$group": { 
        "_id": "$results.player", 
        "wins": {"$sum": {"$cond": [
            {"$eq":["$results.type","win"]},1,0
        ]}}, 
        "losses": {"$sum":{"$cond": [
            {"$eq":["$results.type","loss"]},1,0
        ]}}
    }}

])

Dus voor mij, als het mijn toepassing was, zou ik het schema in de laatste vorm hierboven willen hebben in plaats van wat je hebt. Al het werk dat wordt gedaan in de geleverde aggregatiebewerkingen (met uitzondering van de laatste instructie) is erop gericht de bestaande schemavorm over te nemen en deze te manipuleren in dit formulier, dus dan is het eenvoudig om de eenvoudige aggregatieverklaring uit te voeren zoals hierboven weergegeven.

Aangezien elke speler is "getagd" met het "win/loss"-attribuut, kun je hoe dan ook altijd discreet toegang krijgen tot je "winnaars/verliezers".

Als laatste ding. Je datum is een touwtje. Dat vind ik niet leuk.

Er kan een reden zijn geweest om dit te doen, maar ik zie het niet. Als u moet groeperen op dag dat is gemakkelijk te aggregeren door gewoon een goede BSON-datum te gebruiken. U kunt dan ook gemakkelijk met andere tijdsintervallen werken.

Dus als je de datum hebt vastgesteld en er de startdatum van hebt gemaakt, , en "duur" vervangen door end_time , dan mag je iets houden waarvan je de "duur" kunt krijgen door eenvoudige wiskunde + Je krijgt veel extra voordelen door deze in plaats daarvan als datumwaarde te gebruiken.

Dus dat kan je wat stof tot nadenken geven over je schema.

Voor degenen die geïnteresseerd zijn, hier is wat code die ik heb gebruikt om een ​​werkende set gegevens te genereren:

// Ye-olde array shuffle
function shuffle(array) {
    var m = array.length, t, i;

    while (m) {

        i = Math.floor(Math.random() * m--);

        t = array[m];
        array[m] = array[i];
        array[i] = t;

    }

    return array;
}


for ( var l=0; l<10000; l++ ) {

    var players = ["Player1","Player2","Player3","Player4"];

    var playlist = shuffle(players);
    for ( var x=0; x<playlist.length; x++ ) { 
        var obj = {  
            player: playlist[x], 
            score: Math.floor(Math.random() * (100000 - 50 + 1)) +50
        }; 

        playlist[x] = obj;
    }

    var rec = { 
        duration: Math.floor(Math.random() * (50000 - 15000 +1)) +15000,
        date: new Date(),
         win: playlist.slice(0,2),
        loss: playlist.slice(2) 
    };  

    db.game.insert(rec);
}


  1. MongoDB/NoSQL:geschiedenis van documentwijzigingen bijhouden

  2. Kan ik twee kolommen uniek voor elkaar maken? of samengestelde primaire sleutels in redis gebruiken?

  3. Mongod-fout:98 Kan bestand niet vergrendelen:/data/db/mongod.lock Bron tijdelijk niet beschikbaar. Is er al een mongod-instantie actief?

  4. zeilen-mongo auth fout in zeilen 0.10