MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Agregação do mongoDB:soma com base em nomes de array


Há muito nisso, especialmente se você é relativamente novo no uso de agregar , mas pode ser feito. Vou explicar as etapas após a listagem:
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
        ]}}
    }}
])

Resumo


Isso pressupõe que os "jogadores" em cada matriz de "vitória" e "perda" são todos únicos para começar. Isso parecia razoável para o que parecia ser modelado aqui:

  1. Desenrole ambas as matrizes. Isso cria duplicatas, mas elas serão removidas posteriormente.

  2. Ao projetar, há algum uso do $cond operador (um ternário) para obter alguns valores de string literais. E o último uso é especial, porque um array está sendo adicionado. Então, depois de projetar essa matriz, ela será desenrolada novamente. Mais duplicatas, mas esse é o ponto. Uma "vitória", um registro de "derrota" para cada um.

  3. Mais projeção com o $cond operador e o uso do $eq operador também. Desta vez, estamos fundindo os dois campos em um. Portanto, usando isso, quando o "tipo" do campo corresponder ao valor em "pontuação", esse "campo-chave" será usado para o valor do campo "resultado". Resultado é que os dois campos diferentes de "ganho" e "perda" agora compartilham o mesmo nome, identificado por "tipo".

  4. Livrar-se das duplicatas dentro de cada documento. Simplesmente agrupando pelo documento _id e os campos "resultado" como chaves. Agora deve haver os mesmos registros de "ganho" e "perda" que havia no documento original, apenas de uma forma diferente à medida que são removidos das matrizes.

  5. Por fim, agrupe todos os documentos para obter os totais por "jogador". Mais uso de $cond e $eq mas desta vez para determinar se o documento atual é uma "vitória" ou uma "perda". Então, onde isso corresponde, retornamos 1 e, onde false, retornamos 0. Esses valores são passados ​​para $soma para obter as contagens totais de "vitórias" e "derrotas".

E isso explica como chegar ao resultado.

Saiba mais sobre os operadores de agregação da documentação. Alguns dos usos "engraçados" de $cond nessa listagem deve poder ser substituído por um $ literal operador. Mas isso não estará disponível até que a versão 2.6 e superior seja lançada.

Caso "simplificado" para MongoDB 2.6 e superior


Claro que há um novo operadores de conjunto em qual é o próximo lançamento no momento da escrita, o que ajudará a simplificar um pouco:
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
        ]}}
    }}

])

Mas "simplificado" é discutível. Para mim, isso apenas "parece" como se estivesse "fazendo barulho" e fazendo mais trabalho. Certamente é mais tradicional, pois simplesmente depende de $ setUnião para mesclar os resultados da matriz.

Mas esse "trabalho" seria anulado alterando um pouco seu esquema, conforme mostrado aqui:
{
    "_id" : ObjectId("531ea2b1fcc997d5cc5cbbc9"),
    "win": [
        {
            "player" : "Player2",
            "type" : "win"
        },
        {
            "player" : "Player4",
            "type" : "win"
        }
    ],
    "loss" : [
        {
            "player" : "Player6",
            "type" : "loss"
        },
        {
            "player" : "Player5",
            "type" : "loss"
        },
    ]
}

E isso elimina a necessidade de projetar o conteúdo do array adicionando o atributo "type" como temos feito, e reduz a consulta e o trabalho realizado:
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 claro apenas alterando seu esquema da seguinte forma:
{
    "_id" : ObjectId("531ea2b1fcc997d5cc5cbbc9"),
    "results" : [
        {
            "player" : "Player6",
            "type" : "loss"
        },
        {
            "player" : "Player5",
            "type" : "loss"
        },
        {
            "player" : "Player2",
            "type" : "win"
        },
        {
            "player" : "Player4",
            "type" : "win"
        }
    ]
}

Isso torna as coisas muito fácil. E isso poderia ser feito em versões anteriores a 2.6. Então você pode fazer isso agora mesmo:
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
        ]}}
    }}

])

Então, para mim, se fosse minha aplicação, eu gostaria do esquema no último formulário mostrado acima, em vez do que você tem. Todo o trabalho feito nas operações de agregação fornecidas (com exceção da última instrução) visa pegar o formulário de esquema existente e manipulá-lo neste form, então é fácil executar a instrução de agregação simples como mostrado acima.

Como cada jogador é "marcado" com o atributo "ganha/derrota", você sempre pode acessar discretamente seus "ganhadores/perdedores" de qualquer maneira.

Como uma coisa final. Sua data é uma corda. Eu não gosto disso.

Pode ter havido uma razão para fazê-lo, mas eu não vejo isso. Se você precisar agrupar por dia isso é fácil de fazer em agregação apenas usando uma data BSON adequada. Você também poderá trabalhar facilmente com outros intervalos de tempo.

Então, se você corrigiu a data e a tornou data_inicial e substituiu "duration" por end_time , então você pode manter algo que você pode obter a "duração" por matemática simples + Você obtém muitos extras benefícios ao tê-los como um valor de data.

Então, isso pode lhe dar alguma coisa para pensar em seu esquema.

Para quem estiver interessado, aqui está um código que usei para gerar um conjunto de dados de trabalho:
// 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);
}