Как суммировать значения во вложенном диапазоне дат в MongoDB - PullRequest
0 голосов
/ 10 мая 2018

Мне нужно суммировать значения с 2018-06-01 по 2018-06-30 для каждого документа в коллекции. Каждый ключ в «днях» - это разные дата и значение. Как должна выглядеть команда mongo aggregate? Результат должен выглядеть примерно так: _id: Product_123, June_Sum: значение} enter image description here

1 Ответ

0 голосов
/ 10 мая 2018

Это действительно не очень хорошая структура для той операции, которую вы сейчас хотите выполнить. Весь смысл сохранения данных в таком формате заключается в том, что вы «увеличиваете» их по мере продвижения.

Например:

 var now = Date.now(),
     today = new Date(now - ( now % ( 1000 * 60 * 60 * 24 ))).toISOString().substr(0,10);

 var product = "Product_123";

 db.counters.updateOne(
   { 
     "month": today.substr(0,7),
     "product": product
   },
   { 
     "$inc": { 
       [`dates.${today}`]: 1,
       "totals": 1
     }
   },
   { "upsert": true }
 )

Таким образом, последующие обновления с $inc применяются как к «ключу», используемому для «даты», так и к приращению свойства «итоги» соответствующего документа. Поэтому после нескольких итераций вы получите что-то вроде:

{
        "_id" : ObjectId("5af395c53945a933add62173"),
        "product": "Product_123",
        "month": "2018-05",
        "dates" : {
                "2018-05-10" : 2,
                "2018-05-09" : 1
        },
        "totals" : 3
}

Если вы на самом деле этого не делаете, то вам «следует», поскольку это предполагаемый шаблон использования для такой структуры.

Без сохранения «итогов» или аналогичного типа записи в документе (ах), в котором хранятся эти ключи, единственные методы, оставшиеся для «агрегации» при обработке, - это эффективное приведение «ключей» в форму «массива».

MongoDB 3.6 с $ objectToArray

db.colllection.aggregate([
  // Only consider documents with entries within the range
  { "$match": {
    "$expr": {
      "$anyElementTrue": {
        "$map": {
          "input": { "$objectToArray": "$days" },
          "in": {
            "$and": [
              { "$gte": [ "$$this.k", "2018-06-01" ] },
              { "$lt": [ "$$this.k", "2018-07-01" ] }
            ]
          }
        }
      }
    }
  }},
  // Aggregate for the month 
  { "$group": {
    "_id": "$product",           // <-- or whatever your key for the value is
    "total": {
      "$sum": {
        "$sum": {
          "$map": {
            "input": { "$objectToArray": "$days" },
            "in": {
              "$cond": {
                "if": {
                  "$and": [
                    { "$gte": [ "$$this.k", "2018-06-01" ] },
                    { "$lt": [ "$$this.k", "2018-07-01" ] }
                  ]
                },
                "then": "$$this.v",
                "else": 0
              }
            }
          }
        }
      }
    }
  }}
])   

Другие версии с mapReduce

db.collection.mapReduce(
  // Taking the same presumption on your un-named key for "product"
  function() {
    Object.keys(this.days)
      .filter( k => k >= "2018-06-01" && k < "2018-07-01")
      .forEach(k => emit(this.product, this.days[k]));
  },
  function(key,values) {
    return Array.sum(values);
  },
  {
    "out": { "inline": 1 },
    "query": {
      "$where": function() {
        return Object.keys(this.days).some(k => k >= "2018-06-01" && k < "2018-07-01")
      }
    }
  }
)

И то, и другое довольно ужасно, так как вам нужно вычислить, попадают ли «ключи» в требуемый диапазон, даже чтобы выбрать документы, и даже после этого еще раз отфильтровать ключи в этих документах, чтобы решить, следует ли их накапливать или нет. .

Также отметим, что если ваш "Product_123' также является «именем ключа» в документе, а НЕ «значением», то вы выполняете еще больше «гимнастики», чтобы просто преобразовать этот «ключ» в «ценность», то есть как работают базы данных, и весь смысл ненужного принуждения, происходящего здесь.


Лучший вариант

Таким образом, в отличие от обработки, как первоначально показывалось, когда вы «должны» накапливать «по ходу» при каждой записи в документ (ы) под рукой, это лучший вариант, чем необходимость «обработки» для того, чтобы привести к Формат массива - это просто поместить данные в массив в первую очередь:

{
        "_id" : ObjectId("5af395c53945a933add62173"),
        "product": "Product_123",
        "month": "2018-05",
        "dates" : [
          { "day": "2018-05-09", "value": 1 },
          { "day": "2018-05-10", "value": 2 }
        },
        "totals" : 3
}

Это бесконечно лучше для целей запроса и дальнейшего анализа:

db.counters.aggregate([
  { "$match": {
    // "month": "2018-05"    // <-- or really just that, since it's there
    "dates": {
      "day": {
        "$elemMatch": {
          "$gte": "2018-05-01", "$lt": "2018-06-01"
        }
      }
    }
  }},
  { "$group": {
    "_id": null,
    "total": {
      "$sum": {
        "$sum": {
          "$filter": {
            "input": "$dates",
            "cond": {
              "$and": [
                { "$gte": [ "$$this.day", "2018-05-01" ] },
                { "$lt": [ "$$this.day", "2018-06-01" ] }
              ]
            }
          }
        }
      }
    }
  }}
])

Что, конечно, действительно эффективно и как бы намеренно избегает поля "total", которое уже существует только для демонстрации. Но, конечно, вы сохраняете «накопленное накопление» при записи, выполняя:

db.counters.updateOne(
   { "product": product, "month": today.substr(0,7)}, "dates.day": today },
   { "$inc": { "dates.$.value": 1, "total": 1 } }
)

Что действительно просто. Добавление upserts добавляет немного больше сложности:

// A "batch" of operations with bulkWrite
db.counter.bulkWrite([
  // Incrementing the matched element
  { "udpdateOne": {
    "filter": {
      "product": product,
      "month": today.substr(0,7)},
      "dates.day": today 
    },
    "update": {
      "$inc": { "dates.$.value": 1, "total": 1 }
    }
  }},
  // Pushing a new "un-matched" element
  { "updateOne": {
    "filter": {
      "product": product,
      "month": today.substr(0,7)},
      "dates.day": { "$ne": today }
    },
    "update": {
      "$push": { "dates": { "day": today, "value": 1 } },
      "$inc": { "total": 1 }
    }
  }},
  // "Upserting" a new document were not matched
  { "updateOne": {
    "filter": {
      "product": product,
      "month": today.substr(0,7)},
    },
    "update": {
      "$setOnInsert": {
        "dates": [{ "day": today, "value": 1 }],
        "total": 1
      }
    },
    "upsert": true
  }}
])

Но обычно вы получаете «лучшее из обоих миров», имея что-то простое для накопления «по ходу дела», а также что-то, что легко и эффективно запрашивать и выполнять другой анализ позже.

Общая мораль этой истории - «выбрать правильную структуру» для того, что вы действительно хотите сделать. Не помещайте вещи в «ключи», которые явно предназначены для использования в качестве «ценностей», поскольку это анти-паттерн, который просто добавляет сложность и неэффективность остальным вашим целям, даже если это кажется правильным для «одного» цель, когда вы изначально хранили его таким образом.

ПРИМЕЧАНИЕ Также на самом деле здесь не рекомендуется хранить «строки» для «дат». Как уже отмечалось, лучшим подходом является использование «значений», когда вы действительно имеете в виду «значения», которые вы намерены использовать. При сохранении данных даты в качестве «значения» всегда гораздо эффективнее и практичнее хранить в виде даты BSON, а НЕ «строки».

...