You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mongoose异步查询重复执行问题排查与优化求助

Mongoose查询重复执行与聚合性能优化解决方案

一、区分Mongoose Query/Aggregate与原生Promise,避免重复执行

  • Mongoose中,Model.find()、Model.findOne()等CRUD方法返回Query对象,Model.aggregate()返回Aggregate对象,二者都支持await或.then()调用,但并非原生Promise。
  • 核心差异:原生Promise决议后,多次访问结果不会重复执行;Query/Aggregate每次被await或调用.then()时,都会重新向数据库发送请求。
  • 避免重复执行的两种方法:
    1. 调用.exec()将Query/Aggregate转为原生Promise,后续操作该Promise即可:
      // 转为原生Promise,多次await仅执行一次
      const videosPromise = Video.aggregate([...]).exec();
      const videos = await videosPromise;
      
    2. 确保对同一个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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 22:49:54