面试题答案
一键面试实现思路
- 筛选过去一周的数据:利用
$match
阶段,通过timestamp
字段筛选出过去一周内的日志文档。假设timestamp
为日期类型,可使用$gte
操作符获取过去一周的起始时间戳对应的日期。例如,在JavaScript中获取过去一周的起始时间可如下计算:
const currentDate = new Date();
const oneWeekAgo = new Date(currentDate.getTime() - 7 * 24 * 60 * 60 * 1000);
在聚合中$match
阶段使用类似:
{
"$match": {
"timestamp": {
"$gte": oneWeekAgo
}
}
}
- 按
user_id
和event_type
分组并统计次数:使用$group
阶段,以user_id
和event_type
作为分组依据,使用$sum
操作符统计每个分组中的文档数量,即每个用户触发每种事件类型的次数。
{
"$group": {
"_id": {
"user_id": "$user_id",
"event_type": "$event_type"
},
"count": {
"$sum": 1
}
}
}
- 计算每种事件类型在所有用户中的占比:
- 首先,再次使用
$group
阶段,以event_type
为分组依据,将所有用户触发该事件类型的次数累加起来,得到每种事件类型的总次数。
- 首先,再次使用
{
"$group": {
"_id": "$_id.event_type",
"total_count": {
"$sum": "$count"
}
}
}
- 然后,使用`$lookup`将上述结果与之前按`user_id`和`event_type`分组统计的结果进行关联,以便在每个用户的每种事件类型统计结果中添加该事件类型的总次数。
{
"$lookup": {
"from": "aggregation_result_alias",
"localField": "_id.event_type",
"foreignField": "_id",
"as": "total_info"
}
}
这里假设之前的聚合结果存储在一个临时集合或使用$out
操作输出到一个新集合,aggregation_result_alias
为该集合的别名。
- 最后,使用$addFields
阶段计算占比。
{
"$addFields": {
"percentage": {
"$divide": [
"$count",
{
"$arrayElemAt": [
"$total_info.total_count",
0
]
}
]
}
}
}
性能优化
- 索引优化:确保
timestamp
、user_id
和event_type
字段上都有索引。可使用如下命令创建复合索引:
db.log_collection.createIndex({timestamp: 1, user_id: 1, event_type: 1});
这样在$match
和$group
阶段能利用索引快速定位和分组数据。
2. 分块和并行处理:如果数据量极大,可以考虑对数据进行分块处理,将数据按时间范围(如按天)进行划分,然后并行处理每个时间块的数据,最后合并结果。MongoDB的分片集群架构可以支持这种并行处理方式,通过合理的分片键(如timestamp
)将数据分布到不同的分片上,从而提高处理效率。
3. 限制数据扫描范围:在$match
阶段尽量准确地筛选数据,避免不必要的数据参与后续聚合操作。除了筛选时间范围,还可以根据业务需求对其他字段进行筛选,减少初始数据集的大小。
4. 使用内存限制:在聚合操作中,可以通过$maxTimeMS
设置聚合操作的最长执行时间,防止长时间运行占用过多资源。同时,可以使用allowDiskUse
选项,当内存不足以完成聚合操作时,允许MongoDB将数据写入临时文件,避免因内存不足导致聚合失败,但此选项应谨慎使用,因为磁盘操作通常比内存操作慢。
db.log_collection.aggregate([/*聚合管道阶段*/], {maxTimeMS: 60000, allowDiskUse: true});