虽然您的问题应该更清楚,但您的源输出样本表明您正在寻找:
- 每个“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 来源也有一个错字,分隔符在所有行的时间戳后面插入:,而不是逗号。