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

Agrupar e contar em um intervalo inicial e final


O algoritmo para isso é basicamente "iterar" valores entre o intervalo dos dois valores. O MongoDB tem algumas maneiras de lidar com isso, sendo o que sempre esteve presente com mapReduce() e com novos recursos disponíveis para o aggregate() método.

Vou expandir sua seleção para mostrar deliberadamente um mês sobreposto, já que seus exemplos não tinham um. Isso fará com que os valores "HGV" apareçam em "três" meses de produção.
{
        "_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 - Requer 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" }
    }
  }}
])

A chave para fazer isso funcionar é o $range operador que recebe valores para um "início" e um "fim" bem como um "intervalo" para aplicar. O resultado é uma matriz de valores retirados do "início" e incrementados até que o "fim" seja alcançado.

Usamos isso com startDate e endDate para gerar as datas possíveis entre esses valores. Você notará que precisamos fazer algumas contas aqui, pois o $range leva apenas um inteiro de 32 bits, mas podemos tirar os milissegundos dos valores de carimbo de data/hora para que esteja tudo bem.

Como queremos "meses", as operações aplicadas extraem os valores de mês e ano do intervalo gerado. Na verdade, geramos o intervalo como os "dias" intermediários, já que os "meses" são difíceis de lidar em matemática. O subsequente $reduce operação leva apenas os "meses distintos" do intervalo de datas.

O resultado, portanto, do primeiro estágio do pipeline de agregação é um novo campo no documento que é uma "matriz" de todos os meses distintos cobertos entre startDate e endDate . Isso fornece um "iterador" para o restante da operação.

Por "iterador" quero dizer que quando aplicamos $unwind obtemos uma cópia do documento original para cada mês distinto coberto no intervalo. Isso permite os seguintes dois $group etapas para primeiro aplicar um agrupamento à chave comum de "mês" e "tipo" para "totalizar" as contagens por meio de $sum e em seguida $group torna a chave apenas o "tipo" e coloca os resultados em uma matriz via $push .

Isso dá o resultado nos dados acima:
{
        "_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
                }
        ]
}

Observe que a cobertura de "meses" só está presente onde há dados reais. Embora seja possível produzir valores zero em um intervalo, isso requer um pouco de discussão e não é muito prático. Se você deseja valores zero, é melhor adicionar isso no pós-processamento no cliente assim que os resultados forem recuperados.

Se você realmente deseja os valores zero, deve consultar separadamente $min e $max valores e passá-los para "força bruta" o pipeline para gerar as cópias para cada valor de intervalo possível fornecido.

Dessa vez, o "intervalo" é feito externamente a todos os documentos e você usa um $cond declaração no acumulador para ver se os dados atuais estão dentro do intervalo agrupado produzido. Além disso, como a geração é "externa", realmente não precisamos do operador MongoDB 3.4 de $range , portanto, isso também pode ser aplicado a versões 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" }
    }
  }}
])

O que produz preenchimentos zero consistentes para todos os meses possíveis em todos os agrupamentos:
{
        "_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
                }
        ]
}

MapReduce


Todas as versões do MongoDB suportam mapReduce, e o caso simples do "iterador" como mencionado acima é tratado por um for loop no mapeador. Podemos obter a saída gerada até o primeiro $group de cima simplesmente fazendo:
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 produz:
{
        "_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
}

Portanto, ele não tem o segundo agrupamento para compor as matrizes, mas produzimos a mesma saída agregada básica.