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

Agrupar valores e contagens distintas para cada propriedade em uma consulta


Existem diferentes abordagens dependendo da versão disponível, mas todas elas basicamente se dividem em transformar seus campos de documento em documentos separados em uma "matriz" e depois "desenrolar" essa matriz com $unwind e fazendo sucessivos $group estágios para acumular os totais de saída e as matrizes.

MongoDB 3.4.4 e superior


As versões mais recentes têm operadores especiais como $arrayToObject e $objectToArray que pode tornar a transferência para o "array" inicial do documento de origem mais dinâmica do que em versões 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"
    }
  }}
])

Então, usando $objectToArray você transforma o documento inicial em uma matriz de suas chaves e valores como "k" e "v" chaves na matriz de objetos resultante. Aplicamos $filter aqui para selecionar por "chave". Aqui usando $in com uma lista de chaves que queremos, mas isso poderia ser usado de forma mais dinâmica como uma lista de chaves para "excluir" onde fosse mais curta. Está apenas usando operadores lógicos para avaliar a condição.

O estágio final aqui usa $replaceRoot e como toda a nossa manipulação e "agrupamento" ainda mantém esse "k" e "v" formulário, então usamos $arrayToObject aqui para promover nosso "array de objetos" no resultado para as "chaves" do documento de nível superior na saída.

MongoDB 3.6 $mergeObjects


Como um detalhe extra aqui, o MongoDB 3.6 inclui $mergeObjects que pode ser usado como um "acumulador " em um $group pipeline também, substituindo assim o $push e fazendo o $replaceRoot final simplesmente deslocando o "data" key para a "raiz" do documento retornado:
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"  } }
])

Isso não é muito diferente do que está sendo demonstrado em geral, mas simplesmente demonstra como $mergeObjects pode ser usado desta forma e pode ser útil nos casos em que a chave de agrupamento fosse algo diferente e não queríamos aquela "fusão" final com o espaço raiz do objeto.

Observe que o $arrayToObject ainda é necessário transformar o "valor" novamente no nome da "chave", mas apenas o fazemos durante a acumulação e não após o agrupamento, pois a nova acumulação permite a "fusão" de chaves.

MongoDB 3.2


Tomando de volta uma versão ou mesmo se você tiver um MongoDB 3.4.x menor que a versão 3.4.4, ainda podemos usar muito disso, mas lidamos com a criação do array de maneira mais estática, também como lidar com a "transformação" final na saída de maneira diferente devido aos operadores de agregação que não temos:
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),{})
)

Isso é exatamente a mesma coisa, exceto que em vez de ter uma transformação dinâmica do documento no array, nós na verdade atribuímos "explicitamente" cada membro do array com o mesmo "k" e "v" notação. Realmente, apenas mantendo esses nomes-chave para convenção neste momento, pois nenhum dos operadores de agregação aqui depende disso.

Além disso, em vez de usar $replaceRoot , fazemos exatamente a mesma coisa que a implementação do estágio de pipeline anterior estava fazendo lá, mas no código do cliente. Todos os drivers do MongoDB têm alguma implementação de cursor.map() para habilitar "transformações de cursor". Aqui, com o shell, usamos as funções básicas de JavaScript de Array.map() e Array.reduce() para pegar essa saída e promover novamente o conteúdo da matriz para serem as chaves do documento de nível superior retornado.

MongoDB 2.6


E voltando ao MongoDB 2.6 para cobrir as versões intermediárias, a única coisa que muda aqui é o uso de $map e um $literal para entrada com a declaração de array:
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),{})
)

Como a ideia básica aqui é "iterar" uma matriz fornecida de nomes de campo, a atribuição real de valores vem "aninhando" o $cond declarações. Para três resultados possíveis, isso significa apenas um único aninhamento para "ramificar" para cada resultado.

O MongoDB moderno da versão 3.4 tem $switch o que torna essa ramificação mais simples, mas isso demonstra que a lógica sempre foi possível e o $cond O operador existe desde que a estrutura de agregação foi introduzida no MongoDB 2.2.

Novamente, a mesma transformação no resultado do cursor se aplica, pois não há nada de novo lá e a maioria das linguagens de programação tem a capacidade de fazer isso por anos, se não desde o início.

É claro que o processo básico pode ser feito até o MongoDB 2.2, mas apenas aplicando a criação do array e $unwind de uma maneira diferente. Mas ninguém deveria estar executando qualquer MongoDB abaixo do 2.8 neste momento, e o suporte oficial mesmo do 3.0 está se esgotando rapidamente.

Saída


Para visualização, a saída de todos os pipelines demonstrados aqui tem o seguinte formato antes da última "transformação" ser feita:
/* 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
                }
            ]
        }
    ]
}

E então pelo $replaceRoot ou a transformação do cursor como demonstrado, o resultado se torna:
/* 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
        }
    ]
}

Portanto, embora possamos colocar alguns operadores novos e sofisticados no pipeline de agregação onde temos disponíveis, o caso de uso mais comum é nessas "transformações de fim de pipeline", caso em que podemos simplesmente fazer a mesma transformação em cada documento em os resultados do cursor retornaram.