Mongodb 聚合并减少嵌套文档和数组
编辑: 我们的用例: 我们从服务器上不断收到关于访客的报告。我们预先聚合服务器上的数据几秒钟,然后将这些报告插入MongoDB 在我们的仪表板中,我们希望根据时间范围查询不同的浏览器、操作系统、地理位置国家等 比如:在过去7天内,有1000名游客使用Chrome浏览器,500名来自德国,200名来自英国等等 我被仪表板需要的MongoDB查询卡住了 我们有以下报告条目:Mongodb 聚合并减少嵌套文档和数组,mongodb,mongodb-query,aggregation-framework,Mongodb,Mongodb Query,Aggregation Framework,编辑: 我们的用例: 我们从服务器上不断收到关于访客的报告。我们预先聚合服务器上的数据几秒钟,然后将这些报告插入MongoDB 在我们的仪表板中,我们希望根据时间范围查询不同的浏览器、操作系统、地理位置国家等 比如:在过去7天内,有1000名游客使用Chrome浏览器,500名来自德国,200名来自英国等等 我被仪表板需要的MongoDB查询卡住了 我们有以下报告条目: { "_id" : ObjectId("59b9d08e402025326e1a0f30"), "channe
{
"_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\u info对象就是问题所在
我们不只是将所有源信息按组放入数组中,而是需要实际地累积它们
所以,如果我们有这样的东西:
{
sources_info: [
{
browsers: [
{
name: "Chrome,
count: 1
}
]
},
{
browsers: [
{
name: "Chrome,
count: 1
}
]
}
]
}
/* 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)
}
]
}
该阵列应缩减为:
{
sources_info:
{
browsers: [
{
name: "Chrome,
count: 2
}
]
}
}
我们从MySQL迁移到MongoDB进行分析,但我不知道如何在Mongo中模拟这种行为。关于文档,我几乎认为这是不可能的,至少在当前的数据结构中是不可能的
有什么好的解决办法吗?或者甚至是一种不同的数据结构
干杯,
Chris from StreetCDN您面临的基本问题是,您使用的是命名键,而实际上您可能应该使用一致属性路径的值。这意味着,与浏览器之类的键不同,每个条目上的键可能都应该是type:browser等等 这种情况的原因应该在聚合数据的一般方法上变得显而易见。一般来说,它也确实有助于查询。但这些方法基本上涉及将初始数据格式强制转换为这种结构,以便首先对其进行聚合 对于最新版本的MongoDB 3.4.4及更高版本,我们可以通过以下方式处理命名密钥:
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" ] }
]
}
}
}}
])
我们甚至可以通过使用和代替,将其回溯到MongoDB 3.2,但一般方法是要解释的主要内容
串联数组
需要做的主要事情是从许多具有命名键的不同数组中获取数据,并使用表示每个键名称的type属性创建单个数组。这可以说是数据应该首先存储的方式,任何一种方法的第一个聚合阶段都是这样的:
{
sources_info: [
{
browsers: [
{
name: "Chrome,
count: 1
}
]
},
{
browsers: [
{
name: "Chrome,
count: 1
}
]
}
]
}
/* 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)
}
]
}
放松和分组
要累积的部分数据实际上包括数组中的那些类型和名称属性。每当您需要从一个数组中跨文档累加时,您使用的过程就是为了能够作为分组键的一部分访问这些值
这意味着,在组合数组上使用后,您需要同时使用这两个键和减少的时间戳细节,以确定计数值
由于您有子详细级别,即浏览器中的每个浏览器名称,因此您使用其他管道阶段,逐渐降低分组键的粒度,并使用将详细信息累积到数组中
在任何一种情况下,忽略输出的最后一个阶段,累积结构如下所示:
/* 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)
}
]
}
]
}
这实际上是数据的最终状态,尽管其表示形式与最初发现的形式不同。在这一点上,它可以说是完整的,因为任何进一步的处理对于再次以命名键的形式输出来说都只是表面的
输出到命名键
如图所示,各种方法要么通过匹配的键名查找数组条目,要么通过使用将数组内容转换回具有命名键的对象
另一种方法是简单地在代码中执行最后一次操作,如下所示。在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 }),{})
}))
这当然适用于任何一种聚合管道方法
当然,只要所有条目都具有唯一的名称和类型标识组合,就可以替换为,这意味着通过处理光标来修改最终输出的应用程序,您可以应用该技术,甚至可以追溯到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\u info的每个键下的每个数组项都会减少为共享相同名称的每个其他项的累积计数。基本问题是,您使用的是命名键,而实际上可能应该使用一致属性路径的值。这意味着不需要像 浏览器,这可能只是简单的类型:browser等等 这种情况的原因应该在聚合数据的一般方法上变得显而易见。一般来说,它也确实有助于查询。但这些方法基本上涉及将初始数据格式强制转换为这种结构,以便首先对其进行聚合 对于最新版本的MongoDB 3.4.4及更高版本,我们可以通过以下方式处理命名密钥:
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" ] }
]
}
}
}}
])
我们甚至可以通过使用和代替,将其回溯到MongoDB 3.2,但一般方法是要解释的主要内容
串联数组
需要做的主要事情是从许多具有命名键的不同数组中获取数据,并使用表示每个键名称的type属性创建单个数组。这可以说是数据应该首先存储的方式,任何一种方法的第一个聚合阶段都是这样的:
{
sources_info: [
{
browsers: [
{
name: "Chrome,
count: 1
}
]
},
{
browsers: [
{
name: "Chrome,
count: 1
}
]
}
]
}
/* 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)
}
]
}
放松和分组
要累积的部分数据实际上包括数组中的那些类型和名称属性。每当您需要从一个数组中跨文档累加时,您使用的过程就是为了能够作为分组键的一部分访问这些值
这意味着,在组合数组上使用后,您需要同时使用这两个键和减少的时间戳细节,以确定计数值
由于您有子详细级别,即浏览器中的每个浏览器名称,因此您使用其他管道阶段,逐渐降低分组键的粒度,并使用将详细信息累积到数组中
在任何一种情况下,忽略输出的最后一个阶段,累积结构如下所示:
/* 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)
}
]
}
]
}
这实际上是数据的最终状态,尽管其表示形式与最初发现的形式不同。在这一点上,它可以说是完整的,因为任何进一步的处理对于再次以命名键的形式输出来说都只是表面的
输出到命名键
如图所示,各种方法要么通过匹配的键名查找数组条目,要么通过使用将数组内容转换回具有命名键的对象
另一种方法是简单地在代码中执行最后一次操作,如下所示。在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 }),{})
}))
这当然适用于任何一种聚合管道方法
当然,只要所有条目都具有唯一的名称和类型标识组合,就可以替换为,这意味着通过处理光标来修改最终输出的应用程序,您可以应用该技术,甚至可以追溯到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\u info每个键下的每个数组项都会减少为共享相同名称的每个其他项的累积计数。尝试了$unwind,然后对其进行分组?尝试过,但实际上我们讨论的是5种不同的分组,对吗?比如浏览器,操作系统,conti…,等等。感觉有点错误和缓慢。如果你能勾勒出你的计划,那就太好了!你需要更具体一些。示例文档中的浏览器远远不止这些,因此您使用的$push应该只是将每个文档中的每个完整属性推送到结果数组中。它也必然会破坏任何合理大小的数据量。所以,如果你真正期望的是所有的钥匙都被减少到它们的数量,那么你真的需要在你的问题中这样说。还不清楚为什么这些属性一开始就存在于数组中。因此,详细说明并充分解释您的预期结果并没有什么坏处。@NeilLunn感谢您的回复。我已经更新了这个问题以澄清用例。如果您对报告结构有任何建议,请随时帮助我:-。如果我们有作为一系列文档的源信息,$unwind是否可能?每个文档都有一个键值对,其中包含浏览器及其内容数组?它将深入嵌套和展开到2-3级。我希望能解决你的问题,试过$unwind,然后分组?试过了,但我们实际上是在讨论5种不同的分组,对吗?比如浏览器,操作系统,conti…,等等。感觉有点错误和缓慢。如果你能勾勒出你的计划,那就太好了!你需要更具体一些。您的示例文档中包含的浏览器远远不止这些,因此您使用的$push应该只是将每个文档中的每个完整属性推入其中
结果数组。它也必然会破坏任何合理大小的数据量。所以,如果你真正期望的是所有的钥匙都被减少到它们的数量,那么你真的需要在你的问题中这样说。还不清楚为什么这些属性一开始就存在于数组中。因此,详细说明并充分解释您的预期结果并没有什么坏处。@NeilLunn感谢您的回复。我已经更新了这个问题以澄清用例。如果您对报告结构有任何建议,请随时帮助我:-。如果我们有作为一系列文档的源信息,$unwind是否可能?每个文档都有一个键值对,其中包含浏览器及其内容数组?它将深入嵌套和展开到2-3级。我希望这能解决你的问题