MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Aggregazione mongoDB:somma basata sui nomi degli array

C'è molto in questo, soprattutto se sei relativamente nuovo nell'uso di aggregare , ma può essere fatto. Spiegherò le fasi dopo l'elenco:

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

Riepilogo

Questo presuppone che i "giocatori" in ogni matrice "vittoria" e "perdita" siano tutti unici per cominciare. Sembrava ragionevole per ciò che sembrava essere modellato qui:

  1. Srotolare entrambi gli array. Questo crea duplicati ma verranno rimossi in seguito.

  2. Durante la proiezione viene utilizzato in qualche modo $cond operatore (un ternario) per ottenere alcuni valori di stringa letterali. E l'ultimo utilizzo è speciale, perché viene aggiunto un array. Quindi, dopo aver proiettato, l'array verrà svolto di nuovo. Più duplicati, ma questo è il punto. Una "vittoria", un record di "sconfitte" per ciascuno.

  3. Più proiezione con $cond operatore e l'uso di $eq anche operatore. Questa volta ci stiamo fondendo i due campi in uno. Quindi, usando questo, quando il "tipo" del campo corrisponde al valore in "punteggio", allora quel "campo chiave" viene utilizzato per il valore del campo "risultato". Il risultato è che i due diversi campi "vittoria" e "perdita" ora condividono lo stesso nome, identificato dal "tipo".

  4. Sbarazzarsi dei duplicati all'interno di ogni documento. Semplicemente raggruppando per documento _id e i campi "risultato" come chiavi. Ora dovrebbero esserci gli stessi record di "vincita" e "perdita" presenti nel documento originale, solo in una forma diversa poiché vengono rimossi dagli array.

  5. Infine raggruppa tutti i documenti per ottenere i totali per "giocatore". Maggiore utilizzo di $cond e $eq ma questa volta per determinare se il documento attuale è una "vittoria" o una "perdita". Quindi, dove questo corrisponde, restituiamo 1 e dove false restituiamo 0. Questi valori vengono passati a $somma per ottenere i conteggi totali per "vincite" e "perdite".

E questo spiega come arrivare al risultato.

Ulteriori informazioni sugli operatori di aggregazione dalla documentazione. Alcuni degli usi "divertenti" per $cond in quell'elenco dovrebbe poter essere sostituito con un $ letterale operatore. Ma non sarà disponibile fino al rilascio della versione 2.6 e successive.

Case "semplificate" per MongoDB 2.6 e versioni successive

Ovviamente c'è un nuovo set operator in quella che è la prossima versione al momento della scrittura, che aiuterà a semplificare in qualche modo questo:

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

])

Ma "semplificato" è discutibile. Per me, questo "sembra" come se stesse "sbuffando" e facendo più lavoro. Certamente è più tradizionale, poiché si basa semplicemente su $ setUnion per unire i risultati dell'array.

Ma quel "lavoro" verrebbe annullato modificando leggermente il tuo schema, come mostrato qui:

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

E questo elimina la necessità di proiettare il contenuto dell'array aggiungendo l'attributo "type" come abbiamo fatto e riduce la query e il lavoro svolto:

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

])

E ovviamente semplicemente cambiando il tuo schema come segue:

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

Questo rende le cose molto facile. E questo potrebbe essere fatto nelle versioni precedenti alla 2.6. Quindi potresti farlo subito:

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

])

Quindi per me, se fosse la mia applicazione, vorrei lo schema nell'ultimo modulo mostrato sopra piuttosto che quello che hai. Tutto il lavoro svolto nelle operazioni di aggregazione fornite (ad eccezione dell'ultima affermazione) è volto a prendere la forma dello schema esistente e a manipolarla in questo form, quindi è facile eseguire la semplice istruzione di aggregazione come mostrato sopra.

Poiché ogni giocatore è "etichettato" con l'attributo "vincita/perdita", puoi sempre accedere in modo discreto ai tuoi "vincitori/perdenti" in qualsiasi modo.

Come ultima cosa. La tua data è una stringa. Non mi piace.

Potrebbe esserci stato un motivo per farlo, ma non lo vedo. Se devi raggruppare per giorno questo è facile da fare in aggregazione semplicemente usando una data BSON corretta. Potrai anche lavorare facilmente con altri intervalli di tempo.

Quindi, se hai corretto la data e l'hai resa la data_di_inizio e sostituito "duration" con end_time , quindi puoi conservare qualcosa di cui puoi ottenere la "durata" con semplici calcoli + Ottieni un sacco di extra vantaggi avendo questi come valore di data invece.

In modo che possa darti qualche spunto di riflessione sul tuo schema.

Per chi fosse interessato, ecco del codice che ho usato per generare un set di dati funzionante:

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