
本文详解如何在 mongodb 聚合管道中精准保留 `likedvideos` 数组原始插入顺序(即最新点赞排第一),并支持 `skip`/`limit` 分页,避免 `$lookup` + `$unwind` 后顺序错乱问题。
在使用 MongoDB 实现「用户点赞视频列表」功能时,一个常见但易被忽视的关键点是:$lookup 关联后默认不保留原始数组顺序。由于 likedVideos 是按时间追加的 ObjectId 数组(如 [video_1, video_2, video_3],其中 video_3 为最新),前端要求「最新点赞优先展示 + 支持懒加载分页」,这就要求聚合结果必须严格按该数组的逆序索引(从末尾到开头)排序,而非文档自然顺序或 _id 时间顺序。
原始代码的问题根源在于:
- $unwind 后立即 $group + $push 无法还原原始位置信息;
- $slice 在 $project 阶段截取的是 $lookup 返回的无序数组(BSON 数组本身无稳定顺序保证,尤其跨集合关联后);
- 缺少显式排序逻辑,导致 skip/limit 应用于错误顺序的数据流。
✅ 正确解法是:在 $unwind 后,利用 $indexOfArray 动态计算每个视频在原始 likedVideos 数组中的索引位置,再按该索引倒序排序,最后分页。以下是优化后的完整聚合管道(已适配 Mongoose 和标准 MongoDB Driver):
const { userId, skip = 0, limit = 10 } = req.query;
const pipeline = [
{ $match: { _id: new mongoose.Types.ObjectId(userId) } },
{
$lookup: {
from: "videos",
localField: "likedVideos", // 注意字段名一致性(原文案中为 liked_videos,实际应与 Schema 一致)
foreignField: "_id",
as: "likedVideos"
}
},
{
$unwind: {
path: "$likedVideos",
preserveNullAndEmptyArrays: true // 确保用户未点赞时返回空数组而非报错
}
},
{
$addFields: {
likedIndex: {
$indexOfArray: ["$likedVideos", "$likedVideos._id"] // ✅ 关键:获取当前视频在原始 likedVideos 数组中的位置
}
}
},
{ $sort: { likedIndex: -1 } }, // 降序 → 最新点赞(索引最大)排最前
{ $skip: parseInt(skip) },
{ $limit: parseInt(limit) },
{ $replaceRoot: { newRoot: "$likedVideos" } } // 将视频文档提升为根对象,输出干净数组
];
const result = await User.aggregate(pipeline).toArray();
res.status(200).json(result);? 关键要点说明:
- preserveNullAndEmptyArrays: true 防止用户 likedVideos 为空时管道中断;
- $indexOfArray 的第一个参数必须是原始数组字段("$likedVideos"),第二个参数是当前 unwind 出的文档 _id("$likedVideos._id"),二者类型需严格匹配(均为 ObjectId);
- 排序必须在 $skip/$limit 之前执行,否则分页将作用于未排序数据;
- $replaceRoot 确保最终响应是纯视频文档数组(如 [{_id: ..., videoUrl: ...}, ...]),而非嵌套结构。
⚠️ 注意事项:
- 若 likedVideos 字段名在 Schema 中为 liked_videos(下划线命名),请同步修正 $lookup.localField 和 $indexOfArray 的第一个参数;
- skip/limit 值务必校验为非负整数,建议添加中间件防护(如 Math.max(0, parseInt(skip)));
- 对于高并发点赞场景,该方案性能良好($indexOfArray 为 O(n) 但 n 通常 ≤ 数千),若需极致性能(如百万级点赞),可考虑冗余存储 likedAt 时间戳并建复合索引。
此方案兼顾语义清晰性、执行可靠性与分页准确性,是处理「数组顺序依赖型关联查询」的标准实践。










