【问题标题】:How to sum values in a nested date range in MongoDB如何在 MongoDB 中对嵌套日期范围内的值求和
【发布时间】:2018-05-10 00:05:04
【问题描述】:

我需要对集​​合中每个文档的 2018-06-01 到 2018-06-30 的值求和。 “天”中的每个键都是不同的日期和值。 mongo 聚合命令应该是什么样的?结果应该类似于 { _id:产品_123, 六月总和: 值}

【问题讨论】:

  • 请不要张贴明显是“文字”的东西的截图。而是突出显示“文本”并将数据作为“文本”包含在您的帖子中。一个简单的复制/粘贴操作。在您获得高于您目前拥有的声誉分数之前,您无法在此处将图像添加到您的帖子中是有原因的。原因是“教你”在处理编码和数据的网站上发布“文本”。
  • 您认为提供的答案中是否有某些内容无法解决您的问题?如果是这样,那么请对答案发表评论,以澄清究竟需要解决哪些尚未解决的问题。如果它确实回答了您提出的问题,请注意Accept your Answers您提出的问题

标签: mongodb mapreduce mongodb-query aggregation-framework


【解决方案1】:

对于您现在想要执行的那种操作,这确实不是一个很好的结构。以这种格式保存数据的全部意义在于您可以随时“增加”它。

例如:

 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
}

如果您实际上并没有这样做,那么您“应该”这样做,因为这是这种结构的预期使用模式。

如果不在存储这些键的文档中保留“总计”或类似类型的条目,则在处理过程中“聚合”剩下的唯一方法是有效地将“键”强制转换为“数组”形式。

带有 $objectToArray 的 MongoDB 3.6

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 } }
)

这真的很简单。添加 upsert 会增加“一点”复杂性:

// 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 日期而不是“字符串”会更加高效和实用。

【讨论】:

    猜你喜欢
    • 2018-03-31
    • 1970-01-01
    • 2018-12-29
    • 1970-01-01
    • 2011-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多