sql >> Databasteknik >  >> NoSQL >> MongoDB

mongoDB Aggregation:summa baserad på arraynamn

Det ligger mycket i detta, speciellt om du är relativt ny på att använda sammanslagna , men det kan ske. Jag ska förklara stegen efter listningen:

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

Sammanfattning

Detta tar antagandet att "spelarna" i varje "vinst" och "förlust"-array alla är unika till att börja med. Det verkade rimligt för det som verkade vara modellerat här:

  1. Varva ner båda arrayerna. Detta skapar dubbletter men de kommer att tas bort senare.

  2. Vid projicering finns det viss användning av $cond operator (en ternär) för att få några bokstavliga strängvärden. Och den sista användningen är speciell, eftersom en array läggs till. Så efter projicering kommer den arrayen att lindas upp igen. Fler dubbletter, men det är meningen. En "vinst", ett "förlust"-rekord för varje.

  3. Mer projektion med $cond operatör och användningen av $eq operatör också. Den här gången slår vi samman de två fälten till ett. Så med detta, när "typ" av fältet matchar värdet i "poäng" så används det "nyckelfältet" för "resultat"-fältvärdet. Resultatet är att de två olika fälten "vinst" och "förlust" delar nu samma namn, identifierade med "typ".

  4. Att bli av med dubbletterna i varje dokument. Helt enkelt gruppera efter dokumentet _id och "resultat"-fälten som nycklar. Nu borde det finnas samma "vinst" och "förlust"-poster som det fanns i originaldokumentet, bara i en annan form eftersom de tas bort från arrayerna.

  5. Gruppera slutligen över alla dokument för att få summan per "spelare". Mer användning av $cond och $eq men denna gång för att avgöra om det aktuella dokumentet är en "vinst" eller en "förlust". Så där detta matchar returnerar vi 1 och där false returnerar vi 0. Dessa värden skickas till $summa för att få det totala antalet "vinster" och "förluster".

Och det förklarar hur man kommer till resultatet.

Läs mer om aggregationsoperatorerna från dokumentationen. Några av de "roliga" användningsområdena för $cond i den noteringen ska kunna ersättas med en $ bokstavligt operatör. Men det kommer inte att vara tillgängligt förrän version 2.6 och senare släpps.

"Förenklat" fall för MongoDB 2.6 och uppåt

Naturligtvis finns det en ny uppsättning operatorer i vad är den kommande releasen i skrivande stund, vilket kommer att bidra till att förenkla detta något:

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

])

Men "förenklat" är diskutabelt. För mig "känns" det bara som att det "tuffar runt" och jobbar mer. Det är verkligen mer traditionellt, eftersom det helt enkelt förlitar sig på $ setUnion för att sammanfoga arrayresultaten.

Men det "arbetet" skulle omintetgöras genom att ändra ditt schema lite, som visas här:

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

Och detta tar bort behovet av att projicera arrayinnehållet genom att lägga till "type"-attributet som vi har gjort, och minskar frågan och det utförda arbetet:

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

])

Och naturligtvis bara ändra ditt schema enligt följande:

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

Det gör saker mycket lätt. Och detta kan göras i versioner före 2.6. Så du kan göra det nu:

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

])

Så för mig, om det var min ansökan, skulle jag vilja ha schemat i den sista formen som visas ovan snarare än vad du har. Allt arbete som utförs i de medföljande aggregeringsoperationerna (med undantag för det sista uttalandet) syftar till att ta den befintliga schemaformen och manipulera den till denna form, så då är det lätt att köra den enkla aggregeringssatsen som visas ovan.

Eftersom varje spelare är "taggad" med "vinst/förlust"-attributet kan du alltid diskret komma åt dina "vinnare/lösare" hur som helst.

Som en sista sak. Ditt datum är ett snöre. Jag gillar inte det.

Det kan ha funnits en anledning till det men jag ser det inte. Om du behöver gruppera efter dag det är lätt att göra i aggregering bara genom att använda ett korrekt BSON-datum. Du kommer även då att enkelt kunna arbeta med andra tidsintervall.

Så om du fixade datumet och gjorde det till startdatum , och ersatte "duration" med end_time , då får du behålla något som du kan få "varaktighet" från genom enkel matematik + Du får massor av extra fördelar genom att ha dessa som datumvärde istället.

Så det kan ge dig en tankeställare om ditt schema.

För de som är intresserade, här är lite kod som jag använde för att generera en fungerande uppsättning data:

// 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. Kryptera/Dekryptera egendom medan du skriver/läser till c# mongo db

  2. Stänger du av MongoDB-databasanslutningen på rätt sätt från C# 2.1-drivrutinen?

  3. Redis benchmarking för hget- och hset-kommandon

  4. Mongo DB $ eller fråga i PHP