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

Consulta agregada do Mongodb ou muito complexa?


Embora devesse ter ficado mais claro em sua pergunta, sua amostra de saída da fonte sugere que você está procurando:
  • Contagem total de mensagens por "uid"
  • Contagem distinta de valores em "para"
  • Contagem distinta de valores em "de"
  • Resumo das contagens por "hora" para cada "uid"

Tudo isso é possível em uma única instrução de agregação, e é preciso apenas um gerenciamento cuidadoso das listas distintas e, em seguida, alguma manipulação para mapear os resultados para cada hora em um período de 24 horas.

A melhor abordagem aqui é auxiliada por operadores introduzidos no MongoDB 3.2:
db.collection.aggregate([
    // First group by hour within "uid" and keep distinct "to" and "from"
    { "$group": {
        "_id": {
            "uid": "$uid",
            "time": { "$hour": "$timestamp" }
        },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "count": { "$sum": 1 }
    }},

    // Roll-up to "uid" and keep each hour in an array
    { "$group": {
        "_id": "$_id.uid",
        "total": { "$sum": "$count" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { 
            "$push": {
                "index": "$_id.time",
                "count": "$count"
            }
        }
     }},

     // Getting distinct "to" and "from" requires a double unwind of arrays
     { "$unwind": "$to" },
     { "$unwind": "$to" },
     { "$unwind": "$from" },
     { "$unwind": "$from" },

     // And then adding back to sets for distinct
     { "$group": {
        "_id": "$_id",
        "total": { "$first": "$total" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { "$first": "$temp_hours" }
     }},

     // Map out for each hour and count size of distinct lists
     { "$project": {
        "count": "$total",
        "from_count": { "$size": "$from" },
        "to_count": { "$size": "$to" },
        "hours": {
            "$map": {
                "input": [
                     00,01,02,03,04,05,06,07,08,09,10,11,
                     12,13,14,15,16,17,18,19,20,21,22,23
                 ],
                 "as": "el",
                 "in": {
                      "$ifNull": [
                          { "$arrayElemAt": [
                              { "$map": {
                                  "input": { "$filter": {
                                     "input": "$temp_hours",
                                     "as": "tmp",
                                     "cond": {
                                         "$eq": [ "$$el", "$$tmp.index" ]
                                     }
                                  }},
                                 "as": "out",
                                 "in": "$$out.count"
                              }},
                              0
                          ]},
                          0
                      ]
                 }
            }
        }
     }},

     // Optionally sort in "uid" order
     { "$sort": { "_id": 1 } }
 ])

Antes do MongoDB 3.2, você precisa se envolver um pouco mais para mapear o conteúdo do array para todas as horas do dia:
db.collection.aggregate([

    // First group by hour within "uid" and keep distinct "to" and "from"
    { "$group": {
        "_id": {
            "uid": "$uid",
            "time": { "$hour": "$timestamp" }
        },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "count": { "$sum": 1 }
    }},

    // Roll-up to "uid" and keep each hour in an array
    { "$group": {
        "_id": "$_id.uid",
        "total": { "$sum": "$count" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { 
            "$push": {
                "index": "$_id.time",
                "count": "$count"
            }
        }
     }},

     // Getting distinct "to" and "from" requires a double unwind of arrays
     { "$unwind": "$to" },
     { "$unwind": "$to" },
     { "$unwind": "$from" },
     { "$unwind": "$from" },

     // And then adding back to sets for distinct, also adding the indexes array
     { "$group": {
        "_id": "$_id",
        "total": { "$first": "$total" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { "$first": "$temp_hours" },
        "indexes": { "$first": { "$literal": [
                     00,01,02,03,04,05,06,07,08,09,10,11,
                     12,13,14,15,16,17,18,19,20,21,22,23
        ] } }
     }},

     // Denormalize both arrays
     { "$unwind": "$temp_hours" },
     { "$unwind": "$indexes" },

     // Marry up the index entries and keep either the value or 0
     // Note you are normalizing the double unwind to distinct index
     { "$group": {
         "_id": {
             "_id": "$_id",
             "index": "$indexes"
         },
         "total": { "$first": "$total" }, 
         "from": { "$first": "$from" },
         "to": { "$first": "$to" },
         "count": {
             "$max": {
                 "$cond": [
                     { "$eq": [ "$indexes", "$temp_hours.index" ] },
                     "$temp_hours.count",
                     0
                 ]
             }
         }
     }},

     // Sort to keep index order - !!Important!!         
     { "$sort": { "_id": 1 } },

     // Put the hours into the array and get sizes for other results
     { "$group": {
         "_id": "$_id._id",
         "count": { "$first": "$total" },
         "from_count": { "$first": { "$size": "$from" } },
         "to_count": { "$first": { "$size": "$to" } },
         "hours": { "$push": "$count" }
     }},

     // Optionally sort in "uid" order
     { "$sort": { "_id": 1 } }
])

Para quebrar isso, ambas as abordagens seguem os mesmos passos básicos, com a única diferença real ocorrendo no mapeamento de "horas" para o período de 24 horas.

Na primeira agregação $group etapa, o objetivo é obter resultados por hora presente nos dados e para cada valor "uid". O operador de agregação de data simples de $hour ajuda a obter esse valor como parte da chave de agrupamento.

O $addToSet as operações são uma espécie de "mini-grupo" em si, e isso permite manter os "conjuntos distintos" para cada um dos valores "para" e "de" enquanto essencialmente ainda agrupa por hora.

O próximo $group é mais "organizacional", pois as "contagens" registradas para cada hora são mantidas em uma matriz enquanto rolam todos os dados para serem agrupados apenas por "uid". Isso basicamente fornece todos os "dados" que você realmente precisa para o resultado, mas é claro que o $addToSet as operações aqui estão apenas adicionando "matrizes dentro de matrizes" dos conjuntos distintos determinados por hora.

Para obter esses valores como listas verdadeiramente distintas por cada "uid" e somente, é necessário desconstruir cada array usando $unwind e, finalmente, agrupe de volta apenas os "conjuntos" distintos. O mesmo $addToSet compacta isso e o $first as operações apenas pegam os valores "primeiros" dos outros campos, que já são todos iguais para os dados "por uid" de destino. Estamos felizes com eles, então mantenha-os como estão.

Os estágios finais aqui são essencialmente "cosméticos" por natureza e podem ser alcançados igualmente no código do lado do cliente. Como não há dados presentes para cada intervalo de uma hora, eles precisam ser mapeados em uma matriz de valores representando cada hora. As duas abordagens aqui variam nas capacidades dos operadores disponíveis entre as versões.

Na versão MongoDB 3.2, existem $filter e $arrayElemAt operadores que efetivamente permitem criar a lógica para "transpor" uma fonte de entrada de todas as posições de índice possíveis ( 24 horas ) para os valores já determinados para as contagens dessas horas nos dados disponíveis. Esta é basicamente uma "pesquisa direta" de valores já registrados para cada hora disponível para ver se ela existe, onde faz a contagem ser transposta para o array completo. Onde não estiver presente, um valor padrão de 0 é usado no lugar.

Sem esses operadores, fazer esse "match up" significa essencialmente desnormalizar ambas as matrizes (os dados gravados e as 24 posições completas) para comparar e transpor. Isso é o que está acontecendo na segunda abordagem com uma simples comparação dos valores do "índice" para ver se houve resultado para aquela hora. O $max aqui é usado principalmente por causa dos dois $unwind instruções, onde cada valor registrado dos dados de origem será reproduzido para todas as posições de índice possíveis. Isso "compacta" apenas para os valores desejados por "hora de índice".

Nessa última abordagem, torna-se importante $sort no agrupamento _id valor. Isso ocorre porque ele contém a posição "índice", e isso será necessário ao mover esse conteúdo de volta para uma matriz que você espera que seja ordenada. Que é, obviamente, o $group final estágio aqui onde as posições ordenadas são colocadas em um array com $push .

De volta às "listas distintas", o $size O operador é usado em todos os casos para determinar o "comprimento" e, portanto, a "contagem" de valores distintos nas listas para "para" e "de". Esta é a única restrição real no MongoDB 2.6, pelo menos, mas pode ser substituída por simplesmente "desenrolar" cada array individualmente e, em seguida, agrupar novamente no _id já presente para contar as entradas do array em cada conjunto. É um processo básico, mas como você deve ver o $size operador é a melhor opção aqui para o desempenho geral.

Como nota final, seus dados de conclusão estão um pouco errados, pois possivelmente a entrada com "ddd" em "de" pretendia ser a mesma em "para", mas é registrada como "bbb". Isso altera a contagem distinta do terceiro agrupamento "uid" para "to" em uma entrada. Mas é claro que os resultados lógicos dados os dados de origem são sólidos:
{ "_id" : 1000000, "count" : 3, "from_count" : 2, "to_count" : 2, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0 ] }
{ "_id" : 2000000, "count" : 2, "from_count" : 1, "to_count" : 1, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0 ] }
{ "_id" : 3000000, "count" : 5, "from_count" : 5, "to_count" : 4, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0 ] }

N.B A fonte também tem um erro de digitação com o delimitador sendo interposto com : em vez de uma vírgula logo após o carimbo de data/hora em todas as linhas.