【问题标题】:Mongodb aggregate query, or too complex?Mongodb聚合查询,还是太复杂?
【发布时间】:2016-02-02 19:01:39
【问题描述】:

我有一个如下所示的数据集:

{ uid: 1000000, from: "aaa", to: "bbb": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 1000000, from: "aaa", to: "bbb": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 1000000, from: "bbb", to: "ccc": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 1000000, from: "bbb", to: "ccc": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 2000000, from: "aaa", to: "bbb": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 2000000, from: "aaa", to: "bbb": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 2000000, from: "aaa", to: "bbb": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 2000000, from: "aaa", to: "bbb": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 3000000, from: "aaa", to: "aaa": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 3000000, from: "bbb", to: "bbb": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 3000000, from: "ccc", to: "ccc": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 3000000, from: "ddd", to: "bbb": timestamp: ISODate("2016-02-02T18:42:06.336Z") },
{ uid: 3000000, from: "eee", to: "eee": timestamp: ISODate("2016-02-02T18:42:06.336Z") }

是否可以通过执行一个 mongo 查询来获得所需的输出,还是我必须运行许多单独的查询?小时是文档的一天中的小时。

我还应该注意,该集合每天大约有大约 100 万个条目。大约有 400 种不同的 uid。

{ _id: 1000000, count: 4, from_count: 2, to_count: 2, hours: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0 ] },
{ _id: 2000000, count: 4, from_count: 1, to_count: 1, hours: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0 ] },
{ _id: 3000000, count: 5, from_count: 5, to_count: 5, hours: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0 ] },

【问题讨论】:

  • 这背后的逻辑是什么? .请在您的问题上使用edit 链接来解释您要做什么。你应该阅读How do I ask a good question?
  • 虽然可以通过查看结果数据来判断逻辑,但通常最好在这里提问时“解释”你的逻辑,以便有一个清晰的理解,正如建议的那样通过@user3100115。不是一个简单的过程,但确实有可能。

标签: mongodb mongodb-query aggregation-framework


【解决方案1】:

虽然您的问题应该更清楚,但您的源输出样本表明您正在寻找:

  • 每个“uid”的消息总数
  • “to”中值的不同计数
  • “from”中值的不同计数
  • 每个“uid”的每“小时”计数摘要

这一切都可以在单个聚合语句中实现,只需要仔细管理不同的列表,然后进行一些操作来映射 24 小时内每个小时的结果。

这里的最佳方法是由 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 } }
 ])

在 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, 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 } }
])

为了打破这一点,这两种方法都遵循相同的基本步骤,唯一真正的区别在于 24 小时期间的“小时”映射。

在第一个聚合$group 阶段,目标是获取数据中存在的每小时结果以及每个“uid”值的结果。 $hour 的简单日期聚合运算符有助于获取此值作为分组键的一部分。

$addToSet 操作本身就是一种“迷你组”,这允许为每个“to”和“from”值保留“不同的集合”,同时基本上仍然每小时分组。

下一个$group 更具“组织性”,因为每小时记录的“计数”保存在一个数组中,同时汇总所有数据以按“uid”分组。这基本上为您提供了结果真正需要的所有“数据”,但当然这里的$addToSet 操作只是添加每小时确定的不同集合的“数组中的数组”。

为了使这些值成为每个“uid”真正不同的列表,并且只有,有必要使用$unwind 解构每个数组,然后最后组合成不同的“集合”。相同的$addToSet 对此进行压缩,$first 操作仅采用其他字段的“第一个”值,对于目标“每个 uid”数据,这些值已经相同。我们对这些感到满意,因此请保持原样。

这里的最后阶段本质上是“装饰性的”,同样可以在客户端代码中实现。由于不是每个小时间隔都存在数据,因此需要将其映射到代表每小时的值数组中。这里的两种方法因版本之间可用运算符的功能而异。

在 MongoDB 3.2 版本中,$filter$arrayElemAt 运算符有效地允许您创建逻辑以将所有可能的索引位置(24 小时)的输入源“转置”为已确定的值对于可用数据中这些小时的计数。这基本上是对每个可用小时已经记录的值的“直接查找”,以查看它是否存在,将计数转置到完整数组中。如果不存在,则使用默认值 0

如果没有这些运算符,进行这种“匹配”本质上意味着对两个数组(记录的数据和完整的 24 个位置)进行反规范化,以便进行比较和转置。这是第二种方法中发生的情况,通过简单比较“索引”值来查看该小时是否有结果。这里主要使用$max 运算符,因为有两个$unwind 语句,源数据中的每个记录值都将针对每个可能的索引位置进行复制。这“压缩”到每个“索引小时”所需的值。

在后一种方法中,$sort 在分组 _id 值上变得很重要。这是因为它包含“索引”位置,并且在将此内容移回您希望排序的数组时将需要它。这当然是最后的$group 阶段,在这里将有序位置放入带有$push 的数组中。

回到“不同的列表”,$size 运算符在所有情况下都用于确定“to”和“from”列表中不同值的“长度”,从而确定“计数”。这至少是对 MongoDB 2.6 的唯一真正约束,但可以用其他方式替换为简单地“展开”每个数组,然后在已经存在的 _id 上分组,以便计算每个集合中的数组条目。这是一个基本过程,但正如您应该看到的那样,$size 运算符是此处整体性能的更好选择。


作为最后一点,您的结论数据有点偏离,因为“from”中带有“ddd”的条目可能在“to”中也相同,但被记录为“bbb”。这会将“to”的第三个“uid”分组的不同计数更改为一个条目。但当然,给定源数据的逻辑结果是合理的:

{ "_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 来源也有一个错字,分隔符在所有行的时间戳后面插入:,而不是逗号。

【讨论】:

    猜你喜欢
    • 2021-03-09
    • 2019-11-04
    • 2014-10-21
    • 2016-03-11
    • 2019-06-18
    • 2020-03-02
    • 1970-01-01
    • 2012-08-18
    • 1970-01-01
    相关资源
    最近更新 更多