聚合和减少嵌套文档和数组

时间:2017-09-14 10:09:29

标签: mongodb mongodb-query aggregation-framework

编辑: 我们的用例: 我们从服务器获取有关访问者的持续报告在将这些“报告”插入MongoDB之后,我们将服务器上的数据预先聚合几秒钟。

在我们的信息中心中,我们希望根据时间范围查询不同的浏览器,操作系统,地理位置(国家/地区等)。

如此:在过去7天内,有1000名访问者使用Chrome浏览器,500名访问者来自德国,200名访问者来自英国,依此类推。

我很困惑我们的仪表板需要一个MongoDB查询。

我们有以下报告条目:

{
    "_id" : ObjectId("59b9d08e402025326e1a0f30"),
    "channel_perm_id" : "c361049fb4144b0e81b71c0b6cfdc296",
    "source_id" : "insomnia",
    "start_timestamp" : ISODate("2017-09-14T00:42:54.510Z"),
    "end_timestamp" : ISODate("2017-09-14T00:42:54.510Z"),
    "timestamp" : ISODate("2017-09-14T00:42:54.510Z"),
    "resource_uri" : "b755d62a-8c0a-4e8a-945f-41782c13535b",
    "sources_info" : {
        "browsers" : [
            {
                "name" : "Chrome",
                "count" : NumberLong(2)
            }
        ],
        "operating_systems" : [
            {
                "name" : "Mac OS X",
                "count" : NumberLong(2)
            }
        ],
        "continent_ids" : [
            {
                "name" : "EU",
                "count" : NumberLong(1)
            }
        ],
        "country_ids" : [
            {
                "name" : "DE",
                "count" : NumberLong(1)
            }
        ],
        "city_ids" : [
            {
                "name" : "Solingen",
                "count" : NumberLong(1)
            }
        ]
    },
    "unique_sources" : NumberLong(1),
    "requests" : NumberLong(1),
    "cache_hits" : NumberLong(0),
    "cache_misses" : NumberLong(1),
    "cache_hit_size" : NumberLong(0),
    "cache_refill_size" : NumberLong("170000000000")
}

现在,我们需要根据时间戳汇总这些报告。 到目前为止,这很容易:

db.channel_report.aggregate([{
  $group: {
    _id: {
      $dateToString: {
        format: "%Y",
        date: "$timestamp"
      }
    },
    sources_info: {
      $push: "$sources_info"
    }
  },
}];

但现在对我来说很难。您可能已经注意到,sources_info对象就是问题所在。

我们需要实际累积它,而不是仅将所有源信息“推送”到每个组的数组中。

所以,如果我们有这样的事情:

{
  sources_info: [
    {
      browsers: [
        {
          name: "Chrome, 
          count: 1
        }
      ]
    },
    {
      browsers: [
        {
          name: "Chrome, 
          count: 1
        }
      ]
    }
  ]
}

数组应该简化为:

{
  sources_info:
    {
      browsers: [
        {
          name: "Chrome, 
          count: 2
        }
      ]
    }
}

我们从MySQL迁移到MongoDB进行分析,但我不知道如何在Mongo中模拟这种行为。关于文档,我几乎认为这是不可能的,至少不是当前的数据结构。

这有一个很好的解决方案吗?或者甚至可能是一种不同类型的数据结构?

干杯, 来自StriveCDN的Chris

1 个答案:

答案 0 :(得分:3)

你遇到的基本问题是你正在使用"命名键"您应该在哪里使用值来表示一致的属性路径。这意味着代替像"browsers"之类的密钥,这可能只是"type": "browser",而且每个条目都是如此。

对于汇总数据的一般方法,这一理由应该变得明显。它一般也有助于查询。但这些方法基本上涉及将您的初始数据格式强制转换为这种结构,以便首先聚合它。

对于最新版本(MongoDB 3.4.4及更高版本),我们可以通过$objectToArray使用您的命名密钥,并进行如下操作:

db.channel_report.aggregate([
  { "$project": {
    "timestamp": 1,
    "sources": {
      "$reduce": {
        "input": {
          "$map": {
            "input": { "$objectToArray": "$sources_info" },
            "as": "s",
            "in": {
              "$map": {
                "input": "$$s.v",
                "as": "v",
                "in": {
                  "type": "$$s.k",
                  "name": "$$v.name",
                  "count": "$$v.count"    
                }
              }
            }
          }     
        },
        "initialValue": [],
        "in": { "$concatArrays": ["$$value", "$$this"] }
      }
    }
  }},
  { "$unwind": "$sources" },
  { "$group": {
    "_id": { 
      "year": { "$year": "$timestamp" },
      "type": "$sources.type",
      "name": "$sources.name"
    },
    "count": { "$sum": "$sources.count" }
  }},
  { "$group": {
    "_id": { "year": "$_id.year", "type": "$_id.type" },
    "v": { "$push": { "name": "$_id.name", "count": "$count" } }  
  }},
  { "$group": {
    "_id": "$_id.year",
    "sources_info": {
      "$push": { "k": "$_id.type", "v": "$v" }  
    }  
  }},
  { "$addFields": {
    "sources_info": { "$arrayToObject": "$sources_info" }  
  }}
])

将这一点改回MongoDB 3.4(现在应该是大多数托管服务的默认设置),您可以手动声明每个键名:

db.channel_report.aggregate([
  { "$project": {
    "timestamp": 1,
    "sources": {
      "$concatArrays": [
        { "$map": {
          "input": "$sources_info.browsers",
          "in": {
            "type": "browsers",
            "name": "$$this.name",
            "count": "$$this.count"  
          }  
        }},
        { "$map": {
          "input": "$sources_info.operating_systems",
          "in": {
            "type": "operating_systems",
            "name": "$$this.name",
            "count": "$$this.count"  
          }  
        }},
        { "$map": {
          "input": "$sources_info.continent_ids",
          "in": {
            "type": "continent_ids",
            "name": "$$this.name",
            "count": "$$this.count"  
          }  
        }},
        { "$map": {
          "input": "$sources_info.country_ids",
          "in": {
            "type": "country_ids",
            "name": "$$this.name",
            "count": "$$this.count"  
          }  
        }},
        { "$map": {
          "input": "$sources_info.city_ids",
          "in": {
            "type": "city_ids",
            "name": "$$this.name",
            "count": "$$this.count"  
          }  
        }}
      ]  
    }  
  }},
  { "$unwind": "$sources" },
  { "$group": {
    "_id": { 
      "year": { "$year": "$timestamp" },
      "type": "$sources.type",
      "name": "$sources.name"
    },
    "count": { "$sum": "$sources.count" }
  }},
  { "$group": {
    "_id": { "year": "$_id.year", "type": "$_id.type" },
    "v": { "$push": { "name": "$_id.name", "count": "$count" } }  
  }},
  { "$group": {
    "_id": "$_id.year",
    "sources": {
      "$push": { "k": "$_id.type", "v": "$v" }  
    }  
  }},
  { "$project": {
    "sources_info": {
      "browsers": {
        "$arrayElemAt": [
          "$sources.v",
          { "$indexOfArray": [ "$sources.k", "browsers" ] }
        ]    
      },
      "operating_systems": {
        "$arrayElemAt": [
          "$sources.v",
          { "$indexOfArray": [ "$sources.k", "operating_systems" ] }
        ]    
      },
      "continent_ids": {
        "$arrayElemAt": [
          "$sources.v",
          { "$indexOfArray": [ "$sources.k", "continent_ids" ] }
        ]    
      },
      "country_ids": {
        "$arrayElemAt": [
          "$sources.v",
          { "$indexOfArray": [ "$sources.k", "country_ids" ] }
        ]    
      },
      "city_ids": {
        "$arrayElemAt": [
          "$sources.v",
          { "$indexOfArray": [ "$sources.k", "city_ids" ] }
        ]    
      }
    }    
  }}
])

我们甚至可以使用$map$filter代替$indexOfArray将其重新发送回MongoDB 3.2,但一般方法是要解释的主要方法。

连接数组

需要发生的主要事情是使用命名键从许多不同的数组中获取数据并制作一个"单个数组"表示每个键名的"type"属性。这可以说是数据应该如何存储在第一位,并且任何一种方法的第一个聚合阶段都是这样的:

/* 1 */
{
    "_id" : ObjectId("59b9d08e402025326e1a0f30"),
    "timestamp" : ISODate("2017-09-14T00:42:54.510Z"),
    "sources" : [ 
        {
            "type" : "browsers",
            "name" : "Chrome",
            "count" : NumberLong(2)
        }, 
        {
            "type" : "operating_systems",
            "name" : "Mac OS X",
            "count" : NumberLong(2)
        }, 
        {
            "type" : "continent_ids",
            "name" : "EU",
            "count" : NumberLong(1)
        }, 
        {
            "type" : "country_ids",
            "name" : "DE",
            "count" : NumberLong(1)
        }, 
        {
            "type" : "city_ids",
            "name" : "Solingen",
            "count" : NumberLong(1)
        }
    ]
}

展开和分组

您要累积的部分数据实际上包括来自""内的"type""name"属性。数组。每当您需要在数组"中累积文档时,您使用的进程为$unwind,以便能够作为分组键的一部分访问这些值。

这意味着在对组合数组使用$unwind后,您希望$group同时使用这两个键和缩减"timestamp"详细信息,以便{{3} } "count"值。

因为你有"子级别"详细信息(即浏览器中浏览器的每个名称)然后您使用其他$sum管道阶段,逐渐减少分组键的粒度,并使用$group将详细信息累积到数组中。

在任何一种情况下,省略输出的最后阶段,累积结构如下:

/* 1 */
{
    "_id" : 2017,
    "sources_info" : [ 
        {
            "k" : "continent_ids",
            "v" : [ 
                {
                    "name" : "EU",
                    "count" : NumberLong(1)
                }
            ]
        }, 
        {
            "k" : "city_ids",
            "v" : [ 
                {
                    "name" : "Solingen",
                    "count" : NumberLong(1)
                }
            ]
        }, 
        {
            "k" : "country_ids",
            "v" : [ 
                {
                    "name" : "DE",
                    "count" : NumberLong(1)
                }
            ]
        }, 
        {
            "k" : "browsers",
            "v" : [ 
                {
                    "name" : "Chrome",
                    "count" : NumberLong(2)
                }
            ]
        }, 
        {
            "k" : "operating_systems",
            "v" : [ 
                {
                    "name" : "Mac OS X",
                    "count" : NumberLong(2)
                }
            ]
        }
    ]
}

这确实是数据的最终状态,尽管没有以最初找到的相同形式表示。在这一点上可以说是完整的,因为任何进一步的处理仅仅是装饰性的,再次作为命名键输出。

输出到命名键

如图所示,不同的方法是通过匹配的键名查找数组条目,或者使用$push将数组内容转换回具有命名键的对象。

替代方法也是在代码中执行最后一次操作,如此.map()在shell中操作游标结果的示例所示:

db.channel_report.aggregate([
  { "$project": {
    "timestamp": 1,
    "sources": {
      "$reduce": {
        "input": {
          "$map": {
            "input": { "$objectToArray": "$sources_info" },
            "as": "s",
            "in": {
              "$map": {
                "input": "$$s.v",
                "as": "v",
                "in": {
                  "type": "$$s.k",
                  "name": "$$v.name",
                  "count": "$$v.count"    
                }
              }
            }
          }     
        },
        "initialValue": [],
        "in": { "$concatArrays": ["$$value", "$$this"] }
      }
    }
  }},
  { "$unwind": "$sources" },
  { "$group": {
    "_id": { 
      "year": { "$year": "$timestamp" },
      "type": "$sources.type",
      "name": "$sources.name"
    },
    "count": { "$sum": "$sources.count" }
  }},
  { "$group": {
    "_id": { "year": "$_id.year", "type": "$_id.type" },
    "v": { "$push": { "name": "$_id.name", "count": "$count" } }  
  }},
  { "$group": {
    "_id": "$_id.year",
    "sources_info": {
      "$push": { "k": "$_id.type", "v": "$v" }  
    }  
  }},
  /*
  { "$addFields": {
    "sources_info": { "$arrayToObject": "$sources_info" }  
  }}
  */
]).map( d => Object.assign(d,{
  "sources_info": d.sources_info.reduce((acc,curr) =>
    Object.assign(acc,{ [curr.k]: curr.v }),{})
}))

当然,这适用于任何聚合管道方法。

当然,只要所有条目都具有"name""type"的唯一标识组合,即使$arrayToObject也可以替换为$concatArrays(因为它们似乎是),这意味着通过处理光标修改最终输出的应用程序,你甚至可以在MongoDB 2.6之前应用该技术。

最终输出

最终输出(当然实际聚合,但问题只是采样一个文档)累积所有子键并从最后一个样本输出重建,如下所示:

{
    "_id" : 2017,
    "sources_info" : {
        "continent_ids" : [ 
            {
                "name" : "EU",
                "count" : NumberLong(1)
            }
        ],
        "city_ids" : [ 
            {
                "name" : "Solingen",
                "count" : NumberLong(1)
            }
        ],
        "country_ids" : [ 
            {
                "name" : "DE",
                "count" : NumberLong(1)
            }
        ],
        "browsers" : [ 
            {
                "name" : "Chrome",
                "count" : NumberLong(2)
            }
        ],
        "operating_systems" : [ 
            {
                "name" : "Mac OS X",
                "count" : NumberLong(2)
            }
        ]
    }
}

sources_info每个键下的每个数组条目都减少到共享相同"name"的每个其他条目的累积计数。