MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

mongoDB Aggregation :somme basée sur les noms de tableaux

Il y a beaucoup de choses là-dedans, surtout si vous êtes relativement novice dans l'utilisation de agrégé , mais il peut être terminé. J'expliquerai les étapes après le listing :

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

Résumé

Cela suppose que les "joueurs" dans chaque tableau "gagner" et "perdre" sont tous uniques pour commencer. Cela semblait raisonnable pour ce qui semblait être modélisé ici :

  1. Déroulez les deux tableaux. Cela crée des doublons mais ils seront supprimés plus tard.

  2. Lors de la projection, il y a une utilisation du $cond opérateur (un ternaire) afin d'obtenir des valeurs de chaîne littérales. Et la dernière utilisation est spéciale, car un tableau est ajouté. Donc, après la projection, ce tableau va être déroulé à nouveau. Plus de doublons, mais c'est le point. Un "gagner", un "défaite" record pour chacun.

  3. Plus de projection avec le $cond opérateur et l'utilisation du $eq opérateur également. Cette fois, nous fusionnons les deux domaines en un seul. Donc, en utilisant ceci, lorsque le "type" du champ correspond à la valeur dans "score", alors ce "champ clé" est utilisé pour la valeur du champ "résultat". Le résultat est que les deux champs "gagner" et "perdre" partagent désormais le même nom, identifié par "type".

  4. Se débarrasser des doublons dans chaque document. Regrouper simplement par le document _id et les champs "résultat" comme clés. Maintenant, il devrait y avoir les mêmes enregistrements "gain" et "perte" que dans le document d'origine, mais sous une forme différente car ils sont supprimés des tableaux.

  5. Enfin, regroupez tous les documents pour obtenir les totaux par "joueur". Plus d'utilisation de $cond et $eq mais cette fois pour déterminer si le document actuel est un "gagner" ou une "perte". Ainsi, lorsque cela correspond, nous renvoyons 1 et lorsque false, nous renvoyons 0. Ces valeurs sont transmises à $somme afin d'obtenir le nombre total de "victoires" et de "pertes".

Et cela explique comment arriver au résultat.

En savoir plus sur les opérateurs d'agrégation à partir de la documentation. Certaines des utilisations "amusantes" de $cond dans cette liste devrait pouvoir être remplacé par un $ littéral opérateur. Mais cela ne sera pas disponible avant la sortie de la version 2.6 et ultérieure.

Cas "simplifié" pour MongoDB 2.6 et supérieur

Bien sûr, il existe un nouveau opérateurs d'ensemble dans quelle est la prochaine version au moment de la rédaction, ce qui contribuera à simplifier quelque peu :

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

])

Mais "simplifié" est discutable. Pour moi, c'est juste "l'impression" que c'est "se promener" et faire plus de travail. Il est certainement plus traditionnel, car il repose simplement sur $ setUnion pour fusionner les résultats du tableau.

Mais ce "travail" serait annulé en modifiant légèrement votre schéma, comme indiqué ici :

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

Et cela supprime la nécessité de projeter le contenu du tableau en ajoutant l'attribut "type" comme nous l'avons fait, et réduit la requête et le travail effectué :

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

])

Et bien sûr, changez simplement votre schéma comme suit :

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

Cela rend les choses très facile. Et cela pourrait être fait dans les versions antérieures à 2.6. Vous pouvez donc le faire dès maintenant :

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

])

Donc pour moi, si c'était mon application, je voudrais le schéma sous la dernière forme montrée ci-dessus plutôt que ce que vous avez. Tout le travail effectué dans les opérations d'agrégation fournies (à l'exception de la dernière instruction) vise à prendre la forme de schéma existante et à la manipuler dans ceci formulaire, il est donc facile d'exécuter l'instruction d'agrégation simple comme indiqué ci-dessus.

Comme chaque joueur est "marqué" avec l'attribut "gagnant/perdant", vous pouvez toujours accéder discrètement à vos "gagnants/perdants" de toute façon.

Comme une dernière chose. Votre rencontre est une chaîne. Je n'aime pas ça.

Il y avait peut-être une raison à cela mais je ne la vois pas. Si vous devez regrouper par jour c'est facile à faire en agrégation simplement en utilisant une date BSON appropriée. Vous pourrez alors aussi travailler facilement avec d'autres intervalles de temps.

Donc, si vous avez fixé la date et en avez fait la start_date , et remplacé "durée" par end_time , alors vous pouvez garder quelque chose dont vous pouvez obtenir la "durée" par de simples calculs + Vous obtenez beaucoup de supplément avantages en les ayant comme valeur de date à la place.

Cela peut donc vous donner matière à réflexion sur votre schéma.

Pour ceux que cela intéresse, voici un code que j'ai utilisé pour générer un ensemble de données de travail :

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