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

Agrupe y cuente en un rango inicial y final

El algoritmo para esto es básicamente "iterar" valores entre el intervalo de los dos valores. MongoDB tiene un par de formas de lidiar con esto, siendo lo que siempre ha estado presente con mapReduce() y con nuevas características disponibles para el aggregate() método.

Ampliaré su selección para mostrar deliberadamente un mes superpuesto ya que sus ejemplos no tenían uno. Esto dará como resultado que los valores "HGV" aparezcan en "tres" meses de producción.

{
        "_id" : 1,
        "startDate" : ISODate("2017-01-01T00:00:00Z"),
        "endDate" : ISODate("2017-02-25T00:00:00Z"),
        "type" : "CAR"
}
{
        "_id" : 2,
        "startDate" : ISODate("2017-02-17T00:00:00Z"),
        "endDate" : ISODate("2017-03-22T00:00:00Z"),
        "type" : "HGV"
}
{
        "_id" : 3,
        "startDate" : ISODate("2017-02-17T00:00:00Z"),
        "endDate" : ISODate("2017-04-22T00:00:00Z"),
        "type" : "HGV"
}

Agregado:requiere MongoDB 3.4

db.cars.aggregate([
  { "$addFields": {
    "range": {
      "$reduce": {
        "input": { "$map": {
          "input": { "$range": [ 
            { "$trunc": { 
              "$divide": [ 
                { "$subtract": [ "$startDate", new Date(0) ] },
                1000
              ]
            }},
            { "$trunc": {
              "$divide": [
                { "$subtract": [ "$endDate", new Date(0) ] },
                1000
              ]
            }},
            60 * 60 * 24
          ]},
          "as": "el",
          "in": {
            "$let": {
              "vars": {
                "date": {
                  "$add": [ 
                    { "$multiply": [ "$$el", 1000 ] },
                    new Date(0)
                  ]
                },
                "month": {
                }
              },
              "in": {
                "$add": [
                  { "$multiply": [ { "$year": "$$date" }, 100 ] },
                  { "$month": "$$date" }
                ]
              }
            }
          }
        }},
        "initialValue": [],
        "in": {
          "$cond": {
            "if": { "$in": [ "$$this", "$$value" ] },
            "then": "$$value",
            "else": { "$concatArrays": [ "$$value", ["$$this"] ] }
          }
        }
      }
    }
  }},
  { "$unwind": "$range" },
  { "$group": {
    "_id": {
      "type": "$type",
      "month": "$range"
    },
    "count": { "$sum": 1 }
  }},
  { "$sort": { "_id": 1 } },
  { "$group": {
    "_id": "$_id.type",
    "monthCounts": { 
      "$push": { "month": "$_id.month", "count": "$count" }
    }
  }}
])

La clave para que esto funcione es $range operador que toma valores para un "inicio" y "fin", así como un "intervalo" para aplicar. El resultado es una matriz de valores tomados desde el "inicio" e incrementados hasta alcanzar el "final".

Usamos esto con startDate y endDate para generar las posibles fechas entre esos valores. Notarás que necesitamos hacer algunos cálculos aquí ya que el $range solo toma un número entero de 32 bits, pero podemos quitar los milisegundos de los valores de la marca de tiempo, así que está bien.

Como queremos "meses", las operaciones aplicadas extraen los valores de mes y año del rango generado. De hecho, generamos el rango como los "días" intermedios, ya que los "meses" son difíciles de manejar en matemáticas. El siguiente $reduce la operación toma solo los "meses distintos" del intervalo de fechas.

Por lo tanto, el resultado de la primera etapa de canalización de agregación es un nuevo campo en el documento que es una "matriz" de todos los meses distintos cubiertos entre startDate y endDate . Esto proporciona un "iterador" para el resto de la operación.

Por "iterador" quiero decir que cuando aplicamos $unwind obtenemos una copia del documento original para cada mes distinto cubierto en el intervalo. Esto permite los siguientes dos $group etapas para aplicar primero una agrupación a la clave común de "mes" y "tipo" para "totalizar" los recuentos a través de $sum y luego $group hace que la clave sea solo el "tipo" y coloca los resultados en una matriz a través de $push .

Esto da el resultado de los datos anteriores:

{
        "_id" : "HGV",
        "monthCounts" : [
                {
                        "month" : 201702,
                        "count" : 2
                },
                {
                        "month" : 201703,
                        "count" : 2
                },
                {
                        "month" : 201704,
                        "count" : 1
                }
        ]
}
{
        "_id" : "CAR",
        "monthCounts" : [
                {
                        "month" : 201701,
                        "count" : 1
                },
                {
                        "month" : 201702,
                        "count" : 1
                }
        ]
}

Tenga en cuenta que la cobertura de "meses" solo está presente cuando hay datos reales. Si bien es posible producir valores cero en un rango, requiere un poco de discusión para hacerlo y no es muy práctico. Si desea valores cero, es mejor agregarlo en el procesamiento posterior en el cliente una vez que se hayan recuperado los resultados.

Si realmente tiene su corazón puesto en los valores cero, entonces debe consultar por separado $min y $max valores, y pasarlos a la "fuerza bruta" de la canalización para generar las copias para cada valor de rango posible proporcionado.

Entonces, esta vez, el "rango" se hace externamente a todos los documentos, y luego usa un $cond declaración en el acumulador para ver si los datos actuales están dentro del rango agrupado producido. Además, dado que la generación es "externa", realmente no necesitamos el operador MongoDB 3.4 de $range , por lo que esto también se puede aplicar a versiones anteriores:

// Get min and max separately 
var ranges = db.cars.aggregate(
 { "$group": {
   "_id": null,
   "startRange": { "$min": "$startDate" },
   "endRange": { "$max": "$endDate" }
 }}
).toArray()[0]

// Make the range array externally from all possible values
var range = [];
for ( var d = new Date(ranges.startRange.valueOf()); d <= ranges.endRange; d.setUTCMonth(d.getUTCMonth()+1)) {
  var v = ( d.getUTCFullYear() * 100 ) + d.getUTCMonth()+1;
  range.push(v);
}

// Run conditional aggregation
db.cars.aggregate([
  { "$addFields": { "range": range } },
  { "$unwind": "$range" },
  { "$group": {
    "_id": {
      "type": "$type",
      "month": "$range"
    },
    "count": { 
      "$sum": {
        "$cond": {
          "if": {
            "$and": [
              { "$gte": [
                "$range",
                { "$add": [
                  { "$multiply": [ { "$year": "$startDate" }, 100 ] },
                  { "$month": "$startDate" }
                ]}
              ]},
              { "$lte": [
                "$range",
                { "$add": [
                  { "$multiply": [ { "$year": "$endDate" }, 100 ] },
                  { "$month": "$endDate" }
                ]}
              ]}
            ]
          },
          "then": 1,
          "else": 0
        }
      }
    }
  }},
  { "$sort": { "_id": 1 } },
  { "$group": {
    "_id": "$_id.type",
    "monthCounts": { 
      "$push": { "month": "$_id.month", "count": "$count" }
    }
  }}
])

Lo que produce rellenos cero consistentes para todos los meses posibles en todas las agrupaciones:

{
        "_id" : "HGV",
        "monthCounts" : [
                {
                        "month" : 201701,
                        "count" : 0
                },
                {
                        "month" : 201702,
                        "count" : 2
                },
                {
                        "month" : 201703,
                        "count" : 2
                },
                {
                        "month" : 201704,
                        "count" : 1
                }
        ]
}
{
        "_id" : "CAR",
        "monthCounts" : [
                {
                        "month" : 201701,
                        "count" : 1
                },
                {
                        "month" : 201702,
                        "count" : 1
                },
                {
                        "month" : 201703,
                        "count" : 0
                },
                {
                        "month" : 201704,
                        "count" : 0
                }
        ]
}

MapaReducir

Todas las versiones de MongoDB admiten mapReduce, y el caso simple del "iterador" como se mencionó anteriormente es manejado por un for bucle en el mapeador. Podemos obtener resultados generados hasta el primer $group desde arriba simplemente haciendo:

db.cars.mapReduce(
  function () {
    for ( var d = this.startDate; d <= this.endDate;
      d.setUTCMonth(d.getUTCMonth()+1) )
    { 
      var m = new Date(0);
      m.setUTCFullYear(d.getUTCFullYear());
      m.setUTCMonth(d.getUTCMonth());
      emit({ id: this.type, date: m},1);
    }
  },
  function(key,values) {
    return Array.sum(values);
  },
  { "out": { "inline": 1 } }
)

Que produce:

{
        "_id" : {
                "id" : "CAR",
                "date" : ISODate("2017-01-01T00:00:00Z")
        },
        "value" : 1
},
{
        "_id" : {
                "id" : "CAR",
                "date" : ISODate("2017-02-01T00:00:00Z")
        },
        "value" : 1
},
{
        "_id" : {
                "id" : "HGV",
                "date" : ISODate("2017-02-01T00:00:00Z")
        },
        "value" : 2
},
{
        "_id" : {
                "id" : "HGV",
                "date" : ISODate("2017-03-01T00:00:00Z")
        },
        "value" : 2
},
{
        "_id" : {
                "id" : "HGV",
                "date" : ISODate("2017-04-01T00:00:00Z")
        },
        "value" : 1
}

Por lo tanto, no tiene la segunda agrupación para componer matrices, pero producimos la misma salida agregada básica.