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
相关产品推荐
相关产品推荐

