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

Agrupar valores y recuentos distintos para cada propiedad en una consulta

Existen diferentes enfoques dependiendo de la versión disponible, pero todos se dividen esencialmente para transformar los campos de su documento en documentos separados en una "matriz", luego "desenrollar" esa matriz con $unwind y haciendo sucesivos $group etapas para acumular los totales de salida y las matrices.

MongoDB 3.4.4 y superior

Las últimas versiones tienen operadores especiales como $arrayToObject y $objectToArray lo que puede hacer que la transferencia a la "matriz" inicial desde el documento de origen sea más dinámica que en versiones anteriores:

db.profile.aggregate([
  { "$project": { 
     "_id": 0,
     "data": { 
       "$filter": {
         "input": { "$objectToArray": "$$ROOT" },
         "cond": { "$in": [ "$$this.k", ["gender","caste","education"] ] }
       }   
     }
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": "$data",
    "total": { "$sum": 1 }  
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": "$data"
    }
  }}
])

Entonces usando $objectToArray conviertes el documento inicial en una matriz de sus claves y valores como "k" y "v" claves en la matriz resultante de objetos. Aplicamos $filter aquí para seleccionar por "clave". Aquí usando $in con una lista de claves que queremos, pero esto podría usarse de manera más dinámica como una lista de claves para "excluir" donde era más corto. Solo usa operadores lógicos para evaluar la condición.

La etapa final aquí usa $replaceRoot y dado que toda nuestra manipulación y "agrupación" en el medio aún mantiene ese "k" y "v" formulario, luego usamos $arrayToObject aquí para promocionar nuestra "matriz de objetos" como resultado a las "claves" del documento de nivel superior en la salida.

MongoDB 3.6 $mergeObjects

Como detalle adicional aquí, MongoDB 3.6 incluye $mergeObjects que se puede usar como un "acumulador " en un $group etapa de canalización también, reemplazando así el $push y haciendo el final $replaceRoot simplemente cambiando los "data" clave para la "raíz" del documento devuelto en su lugar:

db.profile.aggregate([
  { "$project": { 
     "_id": 0,
     "data": { 
       "$filter": {
         "input": { "$objectToArray": "$$ROOT" },
         "cond": { "$in": [ "$$this.k", ["gender","caste","education"] ] }
       }   
     }
  }},
  { "$unwind": "$data" },
  { "$group": { "_id": "$data", "total": { "$sum": 1 } }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": {
      "$mergeObjects": {
        "$arrayToObject": [
          [{ "k": "$_id", "v": "$v" }]
        ] 
      }
    }  
  }},
  { "$replaceRoot": { "newRoot": "$data"  } }
])

Esto no es tan diferente de lo que se demuestra en general, sino que simplemente demuestra cómo $mergeObjects se puede usar de esta manera y puede ser útil en casos en los que la clave de agrupación era algo diferente y no queríamos esa "fusión" final con el espacio raíz del objeto.

Tenga en cuenta que $arrayToObject Todavía se necesita transformar el "valor" nuevamente en el nombre de la "clave", pero lo hacemos durante la acumulación en lugar de después de la agrupación, ya que la nueva acumulación permite la "fusión" de claves.

MongoDB 3.2

Retomando una versión o incluso si tiene un MongoDB 3.4.x que es anterior a la versión 3.4.4, aún podemos usar gran parte de esto, pero en su lugar, también nos ocupamos de la creación de la matriz de una manera más estática. como manejar la "transformación" final en la salida de manera diferente debido a los operadores de agregación que no tenemos:

db.profile.aggregate([
  { "$project": {
    "data": [
      { "k": "gender", "v": "$gender" },
      { "k": "caste", "v": "$caste" },
      { "k": "education", "v": "$education" }
    ]
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": "$data",
    "total": { "$sum": 1 }  
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  /*
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": "$data"
    }
  }}
  */
]).map( d => 
  d.data.map( e => ({ [e.k]: e.v }) )
    .reduce((acc,curr) => Object.assign(acc,curr),{})
)

Esto es exactamente lo mismo, excepto que en lugar de tener una transformación dinámica del documento en la matriz, en realidad asignamos "explícitamente" a cada miembro de la matriz con el mismo "k" y "v" notación. Realmente solo mantengo esos nombres clave por convención en este punto, ya que ninguno de los operadores de agregación aquí depende de eso en absoluto.

También en lugar de usar $replaceRoot , hacemos exactamente lo mismo que la implementación de la etapa de canalización anterior, pero en el código del cliente. Todos los controladores MongoDB tienen alguna implementación de cursor.map() para habilitar las "transformaciones del cursor". Aquí, con el shell, usamos las funciones básicas de JavaScript de Array.map() y Array.reduce() para tomar esa salida y nuevamente promover el contenido de la matriz para que sean las claves del documento de nivel superior devuelto.

MongoDB 2.6

Y volviendo a MongoDB 2.6 para cubrir las versiones intermedias, lo único que cambia aquí es el uso de $map y un $literal para la entrada con la declaración de matriz:

db.profile.aggregate([
  { "$project": {
    "data": {
      "$map": {
        "input": { "$literal": ["gender","caste", "education"] },
        "as": "k",
        "in": {
          "k": "$$k",
          "v": {
            "$cond": {
              "if": { "$eq": [ "$$k", "gender" ] },
              "then": "$gender",
              "else": {
                "$cond": {
                  "if": { "$eq": [ "$$k", "caste" ] },
                  "then": "$caste",
                  "else": "$education"
                }
              }    
            }
          }    
        }
      }
    }
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": "$data",
    "total": { "$sum": 1 }  
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": {
      "$push": { "name": "$_id.v", "total": "$total" } 
    }  
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  /*
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": "$data"
    }
  }}
  */
])
.map( d => 
  d.data.map( e => ({ [e.k]: e.v }) )
    .reduce((acc,curr) => Object.assign(acc,curr),{})
)

Dado que la idea básica aquí es "iterar" una matriz proporcionada de los nombres de los campos, la asignación real de valores se produce al "anidar" el $cond declaraciones. Para tres resultados posibles, esto significa solo una sola anidación para "ramificarse" para cada resultado.

MongoDB moderno de 3.4 tiene $switch lo que simplifica esta bifurcación, pero esto demuestra que la lógica siempre fue posible y $cond El operador existe desde que se introdujo el marco de agregación en MongoDB 2.2.

Nuevamente, se aplica la misma transformación en el resultado del cursor ya que no hay nada nuevo allí y la mayoría de los lenguajes de programación tienen la capacidad de hacer esto durante años, si no desde el inicio.

Por supuesto, el proceso básico incluso se puede realizar desde MongoDB 2.2, pero simplemente aplicando la creación de matriz y $unwind de una manera diferente. Pero nadie debería estar ejecutando ningún MongoDB bajo 2.8 en este momento, y el soporte oficial incluso desde 3.0 se está agotando rápidamente.

Salida

Para la visualización, la salida de todas las canalizaciones demostradas aquí tiene el siguiente formato antes de que se realice la última "transformación":

/* 1 */
{
    "_id" : null,
    "data" : [ 
        {
            "k" : "gender",
            "v" : [ 
                {
                    "name" : "Male",
                    "total" : 3.0
                }, 
                {
                    "name" : "Female",
                    "total" : 2.0
                }
            ]
        }, 
        {
            "k" : "education",
            "v" : [ 
                {
                    "name" : "M.C.A",
                    "total" : 1.0
                }, 
                {
                    "name" : "B.E",
                    "total" : 3.0
                }, 
                {
                    "name" : "B.Com",
                    "total" : 1.0
                }
            ]
        }, 
        {
            "k" : "caste",
            "v" : [ 
                {
                    "name" : "Lingayath",
                    "total" : 3.0
                }, 
                {
                    "name" : "Vokkaliga",
                    "total" : 2.0
                }
            ]
        }
    ]
}

Y luego por $replaceRoot o la transformación del cursor como se demuestra, el resultado se convierte en:

/* 1 */
{
    "gender" : [ 
        {
            "name" : "Male",
            "total" : 3.0
        }, 
        {
            "name" : "Female",
            "total" : 2.0
        }
    ],
    "education" : [ 
        {
            "name" : "M.C.A",
            "total" : 1.0
        }, 
        {
            "name" : "B.E",
            "total" : 3.0
        }, 
        {
            "name" : "B.Com",
            "total" : 1.0
        }
    ],
    "caste" : [ 
        {
            "name" : "Lingayath",
            "total" : 3.0
        }, 
        {
            "name" : "Vokkaliga",
            "total" : 2.0
        }
    ]
}

Entonces, si bien podemos poner algunos operadores nuevos y sofisticados en la canalización de agregación donde los tenemos disponibles, el caso de uso más común es en estas "transformaciones de final de canalización", en cuyo caso también podemos simplemente hacer la misma transformación en cada documento en en su lugar, se devolvieron los resultados del cursor.