Mongoose异步查询重复执行问题排查与优化求助
Mongoose查询重复执行与聚合性能优化解决方案
一、区分Mongoose Query/Aggregate与原生Promise,避免重复执行
- Mongoose中,
Model.find()、Model.findOne()等CRUD方法返回Query对象,Model.aggregate()返回Aggregate对象,二者都支持await或.then()调用,但并非原生Promise。 - 核心差异:原生Promise决议后,多次访问结果不会重复执行;Query/Aggregate每次被
await或调用.then()时,都会重新向数据库发送请求。 - 避免重复执行的两种方法:
- 调用
.exec()将Query/Aggregate转为原生Promise,后续操作该Promise即可:// 转为原生Promise,多次await仅执行一次 const videosPromise = Video.aggregate([...]).exec(); const videos = await videosPromise; - 确保对同一个Query/Aggregate对象只执行一次
await或.then(),不重复触发。
- 调用
你的代码中仅对Video.aggregate()执行了一次await,因此函数内部不存在重复执行问题,建议检查调用该函数的上层代码是否存在多次await返回值的情况。
二、解决聚合查询扫描过多文档的性能问题
数据库总文档仅5k却扫描10万+对象,核心问题是聚合管道执行顺序不合理+缺少必要索引,导致大量无效数据被提前处理:
1. 调整$match到管道最前端
当前管道先执行$lookup关联users集合,再做$match过滤,会导致MongoDB先处理所有video的关联操作,再过滤数据,极大增加扫描和处理量。重构后先过滤再关联:
const videos = await Video.aggregate([ // 先过滤符合条件的视频,减少后续处理数据量 { $match: { ...keyword, isPublic: true, status: 'finished', isLivePro: false, duration: { $gte: 300, $lte: 3600, }, }, }, // 再关联users集合 { $lookup: { from: 'users', localField: 'user', foreignField: '_id', as: 'user', pipeline: [ { $project: { profileImageUrl: 1, username: 1, hiddenChannel: 1, }, }, ], }, }, { $unwind: { path: '$user', preserveNullAndEmptyArrays: true } }, // 关联后再过滤隐藏频道的用户 { $match: { 'user.hiddenChannel': false, }, }, // 后续管道保持不变 { $lookup: { from: 'profiles', localField: 'user._id', foreignField: 'user', as: 'profile', }, }, { $setWindowFields: { output: { totalCount: { $count: {} } }, }, }, { $addFields: { viewCount: { $cond: { if: { $isArray: '$views' }, then: { $size: '$views' }, else: 0, }, }, }, }, { $unwind: { path: '$profile', preserveNullAndEmptyArrays: true } }, ...(sample ? [ { $sample: { size: limit }, }, ] : []), { $sort: { [sortArray[0]]: sortArray[1] === 'asc' ? -1 : 1 } }, { $skip: (page - 1) * limit }, { $limit: Number(limit) }, ]);
2. 添加必要索引
为$match中的查询字段添加复合索引,让MongoDB快速定位符合条件的文档,减少全表扫描:
// 在videoModel.js中添加复合索引 VideoSchema.index({ isPublic: 1, status: 1, isLivePro: 1, duration: 1, }); // 针对视频标题的模糊查询,添加文本索引(支持不区分大小写的高效匹配) VideoSchema.index({ videoTitle: 'text' }); // 注:使用文本索引后,可将$regex查询替换为$text查询以利用索引: // const keyword = key ? { $text: { $search: key } } : {};
3. 其他优化点
$setWindowFields的$count会遍历当前管道所有文档,若仅需分页总条数,可单独发起计数查询,避免在聚合管道中计算:const totalCount = await Video.countDocuments({ ...keyword, isPublic: true, status: 'finished', isLivePro: false, duration: { $gte: 300, $lte: 3600 }, });- 当
sample为true时,$sample放在$sort之前可减少排序的数据量,当前代码顺序合理。
三、关于async/await的使用
你的代码中async/await的使用完全正确,Mongoose官方推荐使用该方式而非回调函数,无需切换到回调模式。
内容的提问来源于stack exchange,提问作者Austin Howard
相关产品推荐
相关产品推荐

