如何减少嵌套文档的聚合管道中的展开阶段?

时间:2018-05-18 21:26:24

标签: mongodb aggregation-framework

我是mongodb的新手并尝试使用嵌套文档。我有一个查询如下

    db.EndpointData.aggregate([
{ "$group" : { "_id" : "$EndpointId", "RequestCount" : { "$sum" : 1 }, "FirstActivity" : { "$min" : "$DateTime" }, "LastActivity" : { "$max" : "$DateTime" }, "Tags" : { "$push" : "$Tags" } } }, 
{ "$unwind" : "$Tags" }, 
{ "$unwind" : "$Tags" }, 
{ "$group" : { "_id" : "$_id", "RequestCount" : { "$first" : "$RequestCount" }, "Tags" : { "$push" : "$Tags" }, "FirstActivity" : { "$first" : "$FirstActivity" }, "LastActivity" : { "$first" : "$LastActivity" } } }, 
{ "$unwind" : "$Tags" }, 
{ "$unwind" : "$Tags.Sensors" }, 
{ "$group" : { "_id" : { "EndpointId" : "$_id", "Uid" : "$Tags.Uid", "Type" : "$Tags.Sensors.Type" }, "RequestCount" : { "$first" : "$RequestCount" }, "FirstActivity" : { "$first" : "$FirstActivity" }, "LastActivity" : { "$first" : "$LastActivity" } } }, 
{ "$group" : { "_id" : { "EndpointId" : "$_id.EndpointId", "Uid" : "$_id.Uid" }, "count" : { "$sum" : 1 }, "RequestCount" : { "$first" : "$RequestCount" }, "FirstActivity" : { "$first" : "$FirstActivity" }, "LastActivity" : { "$first" : "$LastActivity" } } }, 
{ "$group" : { "_id" : "$_id.EndpointId", "TagCount" : { "$sum" : 1 }, "SensorCount" : { "$sum" : "$count" }, "RequestCount" : { "$first" : "$RequestCount" }, "FirstActivity" : { "$first" : "$FirstActivity" }, "LastActivity" : { "$first" : "$LastActivity" } } }])

我的数据结构如下

{
  "_id": "6aef51dfaf42ea1b70d0c4db",  
  "EndpointId": "98799bcc-e86f-4c8a-b340-8b5ed53caf83",  
  "DateTime": "2018-05-06T19:05:02.666Z",
  "Url": "test",
  "Tags": [
    {
      "Uid": "C1:3D:CA:D4:45:11",
      "Type": 1,
      "DateTime": "2018-05-06T19:05:02.666Z",
      "Sensors": [
        {
          "Type": 1,
          "Value": { "$numberDecimal": "-95" }
        },
        {
          "Type": 2,
          "Value": { "$numberDecimal": "-59" }
        },
        {
          "Type": 3,
          "Value": { "$numberDecimal": "11.029802536740132" }
        }
      ]
    },
    {
      "Uid": "C1:3D:CA:D4:45:11",
      "Type": 1,
      "DateTime": "2018-05-06T19:05:02.666Z",
      "Sensors": [
        {
          "Type": 1,
          "Value": { "$numberDecimal": "-92" }
        },
        {
          "Type": 2,
          "Value": { "$numberDecimal": "-59" }
        }
      ]
    }   
  ]
}

此查询正常正确。我计算每个EdpointID的标签,传感器和重复次数。但问题当我处理大量数据(大约10,000,000个文档)时,我得到内存问题。在此查询中似乎有4个级别的展开问题。如何减少此查询中的展开?

2 个答案:

答案 0 :(得分:1)

只要您的数据在每个文档中具有唯一的传感器和标记读数,就会显示您所呈现的内容,那么您根本不需要$unwind

事实上,你真正需要的只是一个$group

db.endpoints.aggregate([
  // In reality you would $match to limit the selection of documents
  { "$match": { 
    "DateTime": { "$gte": new Date("2018-05-01"), "$lt": new Date("2018-06-01") }
  }},
  { "$group": {
    "_id": "$EndpointId",
    "FirstActivity" : { "$min" : "$DateTime" },
    "LastActivity" : { "$max" : "$DateTime" },
    "RequestCount": { "$sum": 1 },
    "TagCount": {
      "$sum": {
        "$size": { "$setUnion": ["$Tags.Uid",[]] }
      }
    },
    "SensorCount": {
      "$sum": {
        "$sum": {
          "$map": {
            "input": { "$setUnion": ["$Tags.Uid",[]] },
            "as": "tag",
            "in": {
              "$size": {
                "$reduce": {
                  "input": {
                    "$filter": {
                      "input": {
                        "$map": {
                          "input": "$Tags",
                          "in": {
                            "Uid": "$$this.Uid",
                            "Type": "$$this.Sensors.Type"
                          }
                        }
                      },
                      "cond": { "$eq": [ "$$this.Uid", "$$tag" ] }
                    }
                  },
                  "initialValue": [],
                  "in": { "$setUnion": [ "$$value", "$$this.Type" ] }
                }
              }
            }
          }
        }
      }
    }
  }}
])

或者,如果您确实需要从不同的文档中累积“传感器”和“标签”的“唯一”值,那么您仍然需要初始$unwind语句才能获得正确的分组,但不能接近就像你现在一样:

db.endpoints.aggregate([
  // In reality you would $match to limit the selection of documents
  { "$match": { 
    "DateTime": { "$gte": new Date("2018-05-01"), "$lt": new Date("2018-06-01") }
  }},
  { "$unwind": "$Tags" },
  { "$unwind": "$Tags.Sensors" },
  { "$group": {
    "_id": {
      "EndpointId": "$EndpointId",
      "Uid": "$Tags.Uid",
      "Type": "$Tags.Sensors.Type"
    },
    "FirstActivity": { "$min": "$DateTime" },
    "LastActivity": { "$max": "$DateTime" },
    "RequestCount": { "$addToSet": "$_id" }
  }},
  { "$group": {
    "_id": {
      "EndpointId": "$_id.EndpointId",
      "Uid": "$_id.Uid",
    },
    "FirstActivity": { "$min": "$FirstActivity" },
    "LastActivity": { "$max": "$LastActivity" },
    "count": { "$sum": 1 },
    "RequestCount": { "$addToSet": "$RequestCount" }
  }},
  { "$group": {
    "_id": "$_id.EndpointId",
    "FirstActivity": { "$min": "$FirstActivity" },
    "LastActivity": { "$max": "$LastActivity" },
    "TagCount": { "$sum": 1 },
    "SensorCount": { "$sum": "$count" },
    "RequestCount": { "$addToSet": "$RequestCount" }
  }},
  { "$addFields": {
    "RequestCount": {
      "$size": {
        "$reduce": {
          "input": {
            "$reduce": {
              "input": "$RequestCount",
              "initialValue": [],
              "in": { "$setUnion": [ "$$value", "$$this" ] }
            }
          },
          "initialValue": [],
          "in": { "$setUnion": [ "$$value", "$$this" ] }
        }
      }
    }
  }}
],{ "allowDiskUse": true })

从MongoDB 4.0开始,您可以在ObjectId内的_id上使用$toString,只需合并那些唯一键即可使RequestCount使用$mergeObjects }}。这比推送嵌套数组内容并展平它更清晰,更具可伸缩性

db.endpoints.aggregate([
  // In reality you would $match to limit the selection of documents
  { "$match": { 
    "DateTime": { "$gte": new Date("2018-05-01"), "$lt": new Date("2018-06-01") }
  }},
  { "$unwind": "$Tags" },
  { "$unwind": "$Tags.Sensors" },
  { "$group": {
    "_id": {
      "EndpointId": "$EndpointId",
      "Uid": "$Tags.Uid",
      "Type": "$Tags.Sensors.Type"
    },
    "FirstActivity": { "$min": "$DateTime" },
    "LastActivity": { "$max": "$DateTime" },
    "RequestCount": {
      "$mergeObjects": {
        "$arrayToObject": [[{ "k": { "$toString": "$_id" }, "v": 1 }]]
      }
    }
  }},
  { "$group": {
    "_id": {
      "EndpointId": "$_id.EndpointId",
      "Uid": "$_id.Uid",
    },
    "FirstActivity": { "$min": "$FirstActivity" },
    "LastActivity": { "$max": "$LastActivity" },
    "count": { "$sum": 1 },
    "RequestCount": { "$mergeObjects": "$RequestCount" }
  }},
  { "$group": {
    "_id": "$_id.EndpointId",
    "FirstActivity": { "$min": "$FirstActivity" },
    "LastActivity": { "$max": "$LastActivity" },
    "TagCount": { "$sum": 1 },
    "SensorCount": { "$sum": "$count" },
    "RequestCount": { "$mergeObjects": "$RequestCount" }
  }},
  { "$addFields": {
    "RequestCount": {
      "$size": {
        "$objectToArray": "$RequestCount"
      }
    }
  }}
],{ "allowDiskUse": true })

任何一种形式都会返回相同的数据,但结果中的键顺序可能会有所不同:

{
        "_id" : "89799bcc-e86f-4c8a-b340-8b5ed53caf83",
        "FirstActivity" : ISODate("2018-05-06T19:05:02.666Z"),
        "LastActivity" : ISODate("2018-05-06T19:05:02.666Z"),
        "RequestCount" : 2,
        "TagCount" : 4,
        "SensorCount" : 16
}

结果是从您最初在the original question on the topic中作为样本来源提供的这些示例文档中获得的:

{
    "_id" : ObjectId("5aef51dfaf42ea1b70d0c4db"),    
    "EndpointId" : "89799bcc-e86f-4c8a-b340-8b5ed53caf83",    
    "DateTime" : ISODate("2018-05-06T19:05:02.666Z"),
    "Url" : "test",
    "Tags" : [ 
        {
            "Uid" : "C1:3D:CA:D4:45:11",
            "Type" : 1,
            "DateTime" : ISODate("2018-05-06T19:05:02.666Z"),
            "Sensors" : [ 
                {
                    "Type" : 1,
                    "Value" : NumberDecimal("-95")
                }, 
                {
                    "Type" : 2,
                    "Value" : NumberDecimal("-59")
                }, 
                {
                    "Type" : 3,
                    "Value" : NumberDecimal("11.029802536740132")
                }, 
                {
                    "Type" : 4,
                    "Value" : NumberDecimal("27.25")
                }, 
                {
                    "Type" : 6,
                    "Value" : NumberDecimal("2924")
                }
            ]
        },         
        {
            "Uid" : "C1:3D:CA:D4:45:11",
            "Type" : 1,
            "DateTime" : ISODate("2018-05-06T19:05:02.666Z"),
            "Sensors" : [ 
                {
                    "Type" : 1,
                    "Value" : NumberDecimal("-95")
                }, 
                {
                    "Type" : 2,
                    "Value" : NumberDecimal("-59")
                }, 
                {
                    "Type" : 3,
                    "Value" : NumberDecimal("11.413037961112279")
                }, 
                {
                    "Type" : 4,
                    "Value" : NumberDecimal("27.25")
                }, 
                {
                    "Type" : 6,
                    "Value" : NumberDecimal("2924")
                }
            ]
        },          
        {
            "Uid" : "E5:FA:2A:35:AF:DD",
            "Type" : 1,
            "DateTime" : ISODate("2018-05-06T19:05:02.666Z"),
            "Sensors" : [ 
                {
                    "Type" : 1,
                    "Value" : NumberDecimal("-97")
                }, 
                {
                    "Type" : 2,
                    "Value" : NumberDecimal("-58")
                }, 
                {
                    "Type" : 3,
                    "Value" : NumberDecimal("10.171658037099185")
                }
            ]
        }
    ]
}

/* 2 */
{
    "_id" : ObjectId("5aef51e0af42ea1b70d0c4dc"),    
    "EndpointId" : "89799bcc-e86f-4c8a-b340-8b5ed53caf83",    
    "Url" : "test",
    "Tags" : [ 
        {
            "Uid" : "E2:02:00:18:DA:40",
            "Type" : 1,
            "DateTime" : ISODate("2018-05-06T19:05:04.574Z"),
            "Sensors" : [ 
                {
                    "Type" : 1,
                    "Value" : NumberDecimal("-98")
                }, 
                {
                    "Type" : 2,
                    "Value" : NumberDecimal("-65")
                }, 
                {
                    "Type" : 3,
                    "Value" : NumberDecimal("7.845424441900629")
                }, 
                {
                    "Type" : 4,
                    "Value" : NumberDecimal("0.0")
                }, 
                {
                    "Type" : 6,
                    "Value" : NumberDecimal("3012")
                }
            ]
        }, 
        {
            "Uid" : "12:3B:6A:1A:B7:F9",
            "Type" : 1,
            "DateTime" : ISODate("2018-05-06T19:05:04.574Z"),
            "Sensors" : [ 
                {
                    "Type" : 1,
                    "Value" : NumberDecimal("-95")
                }, 
                {
                    "Type" : 2,
                    "Value" : NumberDecimal("-59")
                }, 
                {
                    "Type" : 3,
                    "Value" : NumberDecimal("12.939770381907275")
                }
            ]
        }
    ]
}

底线是你可以在这里使用第一个给定的表格,它将累积“在每个文档中”然后在一个阶段中“累积每个端点”并且是最优的,或者你实际上需要识别像标签上的"Uid"或传感器上的"Type",这些值在端点分组的任何文档组合中不止一次出现。

迄今为止提供的样本数据仅显示这些值在“每个文档中都是唯一的”,因此如果所有剩余数据都是这种情况,则第一个给定的表单将是最佳的。

如果不是这样,那么“展开”两个嵌套数组以“聚合文档中的细节”是解决这个问题的唯一方法。您可以限制日期范围或其他条件,因为大多数“查询”通常都有一些界限,并且实际上不会处理“整个”集合数据,但主要的事实仍然是数组将“解开”为每个创建基本上的文档副本数组成员。

优化意味着您只需要“两次”执行此操作,因为只有两个数组。连续执行$group$unwind$group始终是您做错事的明确标志。一旦你“分开”,你应该只需要“重新组合”一次。在此处演示的一系列评分步骤中,一次方法进行了优化。

问题范围之外仍然存在:

  • 为查询添加其他现实约束以减少处理的文档,甚至可以在“批处理”中执行此操作并合并结果
  • allowDiskUse选项添加到管道以允许使用临时存储。 (实际上在命令上演示)
  • 考虑“嵌套数组”可能不是您想要进行分析的最佳存储方法。当您知道需要$unwind将这种“展开”形式的数据直接写入集合时,它总是更有效。

答案 1 :(得分:0)

如果您正在处理10,000,000个文档的数据,那么您将很容易地遇到聚合管道大小限制。具体来说,according to the MongoDB documentation,管道RAM使用限制为100MB。如果每个文档至少有10个字节的数据,那么这足以达到该限制,并且您的文档绝对会超过该数量。

有几个选项可供您解决此问题:

1)您可以使用文档中提到的allowDiskUse选项。

2)您可以在展开阶段之间进一步投影文档以限制文档大小(单独使用它不太可能)。

3)您可以定期生成有关数据子集的摘要文档,然后对这些摘要文档执行聚合。例如,如果您在大小为1,000的子集上运行摘要文档,则可以将管道中的文档数量从10,000,000减少到10,000。

4)您可以查看集合中的sharding并在集群上运行这些集合操作,以减少任何单个服务器上的负载。

选项1和2都是非常短期的解决方案。它们易于实施,但从长远来看不会有太大帮助。然而,选项3和4涉及的实施起来要复杂得多,但是它们将提供最大的可扩展性,并且更有可能长期满足您的需求。

但请注意,如果您计划接近选项4,则需要做好充分准备。分片集合不能无分支,并且搞乱可能会导致潜在的无法修复的数据丢失。建议拥有一名具有MongoDB集群经验的专职DBA。

相关问题