
本文详解如何通过 $unwind、$group、$setWindowFields 和 $lookup 四步聚合操作,精准统计用户练习频次最高的冥想课程,并关联获取其名称、时长等完整信息。
本文详解如何通过 `$unwind`、`$group`、`$setwindowfields` 和 `$lookup` 四步聚合操作,精准统计用户练习频次最高的冥想课程,并关联获取其名称、时长等完整信息。
在构建冥想类应用的数据看板时,一个常见需求是:基于所有用户的练习记录,统计出被播放次数最多的冥想课程(meditation)。由于用户练习历史以子文档数组形式嵌套在 users 集合的 userProgress.meditationsPracticed 字段中,且每条记录仅包含冥想 ID(字符串),因此需借助 MongoDB 聚合框架完成“反范式化统计 + 关联补全”的典型分析流程。
核心思路分为五步:
-
展开数组:用
$unwind将每个用户的多次练习记录拆为独立文档; -
聚合计数:按冥想 ID 分组,使用
$sum: 1统计总播放次数; -
处理并列排名:使用
$setWindowFields+$rank为各冥想按播放量降序打分,确保相同次数的冥想获得相同排名(如并列第1); -
筛选 Top N:通过
$match筛出rank: 1的最高频冥想(可轻松扩展为rank: { $lte: 5 }获取 Top 5); -
关联补充元数据:通过
$lookup关联meditations集合,获取课程名称、时长等业务字段,并用$unwind展平结果。
以下是完整、可直接运行的聚合查询(已适配 ObjectId 类型转换):
db.users.aggregate([
// 可选:限定特定用户(如调试时),生产环境通常移除此 $match
{
"$match": {
"_id": { "$oid": "627b519f73b2bd3375f5a7a5" }
}
},
// 步骤1:展开 meditationsPracticed 数组
{ "$unwind": "$userProgress.meditationsPracticed" },
// 步骤2:按冥想 ID 分组计数(注意:id 是字符串,需转为 ObjectId)
{
"$group": {
"_id": { "$toObjectId": "$userProgress.meditationsPracticed.id" },
"count": { "$sum": 1 }
}
},
// 步骤3:窗口函数计算排名(按 count 降序)
{
"$setWindowFields": {
"sortBy": { "count": -1 },
"output": { "rank": { "$rank": {} } }
}
},
// 步骤4:只保留排名第一的冥想(支持并列)
{ "$match": { "rank": 1 } },
// 步骤5:关联 meditations 集合补全信息
{
"$lookup": {
"from": "meditations",
"localField": "_id",
"foreignField": "_id",
"as": "meditationInfo"
}
},
{ "$unwind": { "path": "$meditationInfo", "preserveNullAndEmptyArrays": true } },
// (可选)格式化输出:合并计数与课程信息
{
"$project": {
"_id": 0,
"meditationId": "$_id",
"count": 1,
"name": "$meditationInfo.name",
"duration": "$meditationInfo.duration",
"rank": 1
}
}
])
✅ 关键注意事项:
-
"$toObjectId"是必需的:meditationsPracticed.id存储为字符串,而meditations._id是 ObjectId,类型不匹配将导致$lookup失败; - 若需全局 Top N(非单个用户),请移除首个
$match,并在$group前确保数据覆盖全部用户; -
$setWindowFields自 MongoDB 5.0+ 支持,若使用旧版本,可用$sort+$limit替代(但无法处理并列); - 生产环境中建议为
userProgress.meditationsPracticed.id添加索引提升聚合性能:db.users.createIndex({ "userProgress.meditationsPracticed.id": 1 })。
该方案结构清晰、可扩展性强,既满足基础统计需求,也为后续添加时间范围过滤、用户分群分析等高级功能预留了扩展接口。











