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

¿Consulta agregada de Mongodb, o demasiado compleja?

Aunque debería haber quedado más claro en su pregunta, su muestra de salida de la fuente sugiere que está buscando:

  • Recuento total de mensajes por "uid"
  • Recuento distinto de valores en "to"
  • Recuento distinto de valores en "desde"
  • Resumen de conteos por "hora" para cada "uid"

Todo esto es posible en una sola declaración de agregación, y solo se necesita una gestión cuidadosa de las distintas listas y luego cierta manipulación para mapear los resultados de cada hora en un período de 24 horas.

El mejor enfoque aquí es ayudado por operadores introducidos en 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 de MongoDB 3.2, debe involucrarse un poco más para mapear el contenido de la matriz para todas las horas del día:

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 desglosarlo, ambos enfoques aquí siguen los mismos pasos básicos, con la única diferencia real que ocurre en el mapeo de "horas" para el período de 24 horas.

En la primera agregación $group etapa, el objetivo es obtener resultados por hora presente en los datos y para cada valor "uid". El operador de agregación de fecha simple de $hour ayuda a obtener este valor como parte de la clave de agrupación.

El $addToSet las operaciones son una especie de "mini-grupo" en sí mismas, y esto permite mantener los "conjuntos distintos" para cada uno de los valores "hasta" y "desde" mientras esencialmente se sigue agrupando por hora.

El siguiente $group es más "organizacional", ya que los "recuentos" registrados para cada hora se mantienen en una matriz mientras se acumulan todos los datos para agruparlos por "uid". Básicamente, esto le brinda todos los "datos" que realmente necesita para el resultado, pero, por supuesto, el $addToSet las operaciones aquí son simplemente agregar "matrices dentro de matrices" de los distintos conjuntos determinados por hora.

Para obtener estos valores como listas verdaderamente distintas por cada "uid" y solo, es necesario deconstruir cada matriz usando $unwind y finalmente agrupar nuevamente como "conjuntos" distintos. El mismo $addToSet compacta esto, y el $first las operaciones simplemente toman los "primeros" valores de los otros campos, que ya son todos iguales para los datos de destino "por uid". Estamos contentos con ellos, así que déjalos como están.

Las etapas finales aquí son esencialmente de naturaleza "cosmética" y se pueden lograr igualmente en el código del lado del cliente. Dado que no hay datos presentes para cada intervalo de una sola hora, es necesario asignarlos a una matriz de valores que representen cada hora. Los dos enfoques aquí varían según las capacidades de los operadores disponibles entre versiones.

En la versión MongoDB 3.2, hay $filter y $arrayElemAt operadores que le permiten crear la lógica para "transponer" una fuente de entrada de todas las posiciones de índice posibles (24 horas) a los valores que ya están determinados para los recuentos de esas horas en los datos disponibles. Esto es básicamente una "búsqueda directa" de valores ya registrados para cada hora disponible para ver si existe, donde se transpone el conteo a la matriz completa. Donde no está presente, un valor predeterminado de 0 se usa en su lugar.

Sin esos operadores, hacer esta "coincidencia" significa esencialmente desnormalizar ambas matrices (los datos grabados y las 24 posiciones completas) para comparar y transponer. Esto es lo que sucede en el segundo enfoque con una simple comparación de los valores del "índice" para ver si hubo un resultado para esa hora. El $max El operador aquí se usa principalmente debido a los dos $unwind declaraciones, donde cada valor registrado de los datos de origen se reproducirá para cada posición de índice posible. Esto se "compacta" hasta los valores deseados por "hora índice".

En ese último enfoque, se vuelve importante $sort en la agrupación _id valor. Esto se debe a que contiene la posición de "índice", y eso será necesario cuando vuelva a mover este contenido a una matriz que espera que se ordene. Que es, por supuesto, el $group final etapa aquí donde las posiciones ordenadas se colocan en una matriz con $push .

Volviendo a las "listas distintas", el $size El operador se utiliza en todos los casos para determinar la "longitud" y, por lo tanto, el "recuento" de valores distintos en las listas de "hasta" y "desde". Esta es la única restricción real en MongoDB 2.6 al menos, pero de lo contrario se puede reemplazar simplemente "desenrollando" cada matriz individualmente y luego agrupando nuevamente en el _id ya presente para contar las entradas de la matriz en cada conjunto. Es un proceso básico, pero como deberías ver el $size operador es la mejor opción aquí para el rendimiento general.

Como nota final, los datos de su conclusión están un poco fuera de lugar, ya que posiblemente la entrada con "ddd" en "desde" también tenía la intención de ser la misma en "hasta", pero en su lugar se registra como "bbb". Esto cambia el recuento distinto de la tercera agrupación "uid" para "a" hacia abajo en una entrada. Pero, por supuesto, los resultados lógicos dados los datos de origen son 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 La fuente también tiene un error tipográfico con el delimitador interpuesto con : en lugar de una coma justo después de la marca de tiempo en todas las líneas.