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

Node.js MongoDB用户统计API实现合理性及性能负载疑问

用户数据关联统计API的性能优化分析

问题背景

需要实现一个API,查询MongoDB中的所有用户数据,同时为每个用户统计Test和Enrollment两个集合的关联文档数量,当前已实现的代码如下:

const getUsersSummary = async (req, res, next) => {
  try {
    const users = await User.fetch();
    const usersWithCount = await Promise.all(
      users.map(async (user) => {
        let tests;
        let enrollments;
        try {
          tests = await Test.countDocuments(
            { user: user._id }
          );
          enrollments = await Enrollment.countDocuments(
            { user: user._id }
          );
        } catch (e) {
          tests = 0;
          enrollments = 0;
        }
        return {
          _id: user._id,
          name: user.name,
          address: user.address,
          tests: tests,
          enrollments: enrollments,
        };
      })
    );
    return res.json({
      users: usersWithCount,
    });
  } catch (err) {
    next(err);
  }
};

当前代码可正常运行,但存在性能隐患,以下是具体分析和优化方案:

当前实现的性能问题

  • N+1查询陷阱:先查询所有用户(1次请求),再对每个用户分别发起2次countDocuments请求。如果有N个用户,总请求次数就是1 + 2*N。当用户数量较多时,会大幅增加数据库和服务器的负载,API响应时间也会随用户数量线性变长。
  • 错误处理不够精准:单个用户的某一个集合计数查询失败时,会直接把两个计数都设为0,无法定位具体是哪个集合的查询出了问题,不利于后续排查。

优化方案

方案1:使用MongoDB聚合查询(优先推荐)

利用MongoDB的聚合框架,在数据库层面一次性完成用户数据查询和关联集合计数,把总请求次数降到1次,性能提升非常明显。

示例代码:

const getUsersSummary = async (req, res, next) => {
  try {
    const usersWithCount = await User.aggregate([
      // 关联Test集合并获取关联文档
      {
        $lookup: {
          from: 'tests', // 注意这里是数据库中Test集合的实际名称,不是模型名
          localField: '_id',
          foreignField: 'user',
          as: 'tests'
        }
      },
      // 关联Enrollment集合并获取关联文档
      {
        $lookup: {
          from: 'enrollments', // Enrollment集合的实际名称
          localField: '_id',
          foreignField: 'user',
          as: 'enrollments'
        }
      },
      // 将关联文档数组转换为计数,同时保留需要的用户字段
      {
        $project: {
          _id: 1,
          name: 1,
          address: 1,
          tests: { $size: '$tests' },
          enrollments: { $size: '$enrollments' }
        }
      }
    ]);

    return res.json({ users: usersWithCount });
  } catch (err) {
    next(err);
  }
};

方案2:批量预统计关联计数

如果因业务限制无法使用聚合查询,可以先批量统计所有用户的关联集合计数,再和用户数据关联,总请求次数减少到3次(用户查询 + Test批量统计 + Enrollment批量统计)。

示例代码:

const getUsersSummary = async (req, res, next) => {
  try {
    // 1. 查询所有用户
    const users = await User.fetch();
    const userIds = users.map(user => user._id);

    // 2. 批量统计每个用户的Test文档数
    const testCounts = await Test.aggregate([
      { $match: { user: { $in: userIds } } },
      { $group: { _id: '$user', count: { $sum: 1 } } }
    ]);
    // 转成键值对,方便快速查找
    const testCountMap = testCounts.reduce((map, item) => {
      map[item._id.toString()] = item.count;
      return map;
    }, {});

    // 3. 批量统计每个用户的Enrollment文档数
    const enrollmentCounts = await Enrollment.aggregate([
      { $match: { user: { $in: userIds } } },
      { $group: { _id: '$user', count: { $sum: 1 } } }
    ]);
    const enrollmentCountMap = enrollmentCounts.reduce((map, item) => {
      map[item._id.toString()] = item.count;
      return map;
    }, {});

    // 4. 将计数关联到用户数据
    const usersWithCount = users.map(user => ({
      _id: user._id,
      name: user.name,
      address: user.address,
      tests: testCountMap[user._id.toString()] || 0,
      enrollments: enrollmentCountMap[user._id.toString()] || 0
    }));

    return res.json({ users: usersWithCount });
  } catch (err) {
    next(err);
  }
};

必做辅助优化:添加索引

不管用哪种方案,都要给Test和Enrollment集合的user字段添加单字段索引,能大幅提升关联查询和统计的速度:

// 在Test模型初始化时添加索引
Test.createIndex({ user: 1 });
// 在Enrollment模型初始化时添加索引
Enrollment.createIndex({ user: 1 });

内容的提问来源于stack exchange,提问作者Peter Nelson

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 13:15:24