sql >> Base de Datos >  >> NoSQL >> MongoDB

Agregación mongoDB:suma basada en nombres de matriz

Hay mucho en esto, especialmente si es relativamente nuevo en el uso de agregado , pero puede hacerse Explicaré las etapas después del listado:

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

Resumen

Esto supone que los "jugadores" en cada conjunto de "ganancias" y "pérdidas" son todos únicos para empezar. Eso parecía razonable para lo que parecía estar modelado aquí:

  1. Desenrolle ambas matrices. Esto crea duplicados, pero se eliminarán más tarde.

  2. Al proyectar, se usa el $cond operador (un ternario) para obtener algunos valores de cadena literales. Y el último uso es especial, porque se agrega una matriz. Entonces, después de proyectar, esa matriz se desenrollará nuevamente. Más duplicados, pero ese es el punto. Un registro de "ganancias", uno de "pérdidas" para cada uno.

  3. Más proyección con $cond operador y el uso de $eq operador también. Esta vez nos estamos fusionando los dos campos en uno. Entonces, al usar esto, cuando el "tipo" del campo coincide con el valor en "puntuación", ese "campo clave" se usa para el valor del campo "resultado". El resultado es que los dos campos diferentes de "ganancia" y "pérdida" ahora comparten el mismo nombre, identificado por "tipo".

  4. Deshacerse de los duplicados dentro de cada documento. Simplemente agrupando por el documento _id y los campos de "resultado" como claves. Ahora debería haber los mismos registros de "ganancias" y "pérdidas" que había en el documento original, solo que en una forma diferente a medida que se eliminan de las matrices.

  5. Finalmente, agrupe todos los documentos para obtener los totales por "jugador". Más uso de $cond y $eq pero esta vez para determinar si el documento actual es una "ganancia" o una "pérdida". Entonces, donde esto coincida, devolvemos 1 y donde sea falso, devolvemos 0. Esos valores se pasan a $suma para obtener los recuentos totales de "ganancias" y "pérdidas".

Y eso explica cómo llegar al resultado.

Más información sobre los operadores de agregación de la documentación. Algunos de los usos "divertidos" de $cond en esa lista debería poder reemplazarse con un $ literal operador. Pero eso no estará disponible hasta que se lance la versión 2.6 y posteriores.

Caso "simplificado" para MongoDB 2.6 y superior

Por supuesto, hay un nuevo establecer operadores en cuál es el próximo lanzamiento al momento de escribir, lo que ayudará a simplificar esto un poco:

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

])

Pero "simplificado" es discutible. Para mí, eso simplemente "se siente" como si estuviera "dando vueltas" y haciendo más trabajo. Ciertamente es más tradicional, ya que simplemente se basa en $ establecerUnión para fusionar los resultados de la matriz.

Pero ese "trabajo" se anularía cambiando un poco su esquema, como se muestra aquí:

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

Y esto elimina la necesidad de proyectar el contenido de la matriz agregando el atributo "tipo" como lo hemos estado haciendo, y reduce la consulta y el trabajo 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
        ]}}
    }}

])

Y, por supuesto, simplemente cambiando su esquema de la siguiente manera:

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

Eso hace las cosas muy fácil. Y esto se podía hacer en versiones anteriores a la 2.6. Así que podrías hacerlo ahora mismo:

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

])

Entonces, para mí, si fuera mi aplicación, querría el esquema en la última forma que se muestra arriba en lugar de lo que tiene. Todo el trabajo realizado en las operaciones de agregación proporcionadas (con la excepción de la última declaración) tiene como objetivo tomar la forma de esquema existente y manipularlo en este formulario, por lo que es fácil ejecutar la declaración de agregación simple como se muestra arriba.

Como cada jugador está "etiquetado" con el atributo "ganador/perdido", siempre puedes acceder discretamente a tus "ganadores/perdedores" de todos modos.

Como cosa final. Tu fecha es una cadena. No me gusta eso.

Puede haber una razón para hacerlo, pero no la veo. Si necesita agrupar por día eso es fácil de hacer en agregación simplemente usando una fecha BSON adecuada. También podrá trabajar fácilmente con otros intervalos de tiempo.

Entonces, si fijó la fecha y la convirtió en start_date y reemplazó "duration" con end_time , luego puedes quedarte con algo de lo que puedes obtener la "duración" mediante operaciones matemáticas simples + Obtienes mucho extra se beneficia al tener estos como un valor de fecha en su lugar.

Eso puede darle algo de qué pensar en su esquema.

Para aquellos que estén interesados, aquí hay un código que usé para generar un conjunto de datos de trabajo:

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