优化MongoDB按外键ObjectId统计数量的GraphQL查询
优化GraphQL getChapters Resolver的N+1查询问题
原实现因循环每个Chapter调用Question.countDocuments产生N+1查询,性能低效,以下是两种高效优化方案:
方案1:批量统计问题数后关联章节数据
先通过聚合一次性统计所有(chapterRef, subjectRef)组合的问题数量,再查询章节数据并匹配统计结果,仅需2次数据库查询:
// 批量统计各章节对应的问题数量 const questionStats = await Question.aggregate([ { $group: { _id: { chapterRef: "$chapterRef", subjectRef: "$subjectRef" }, count: { $sum: 1 } } } ]); // 转成映射表,快速查找对应统计值 const countMap = {}; questionStats.forEach(stat => { const key = `${stat._id.chapterRef}-${stat._id.subjectRef}`; countMap[key] = stat.count; }); // 查询所有章节数据 const chapters = await Chapter.find({}); // 为每个章节追加问题数量字段 return chapters.map(chapter => { const key = `${chapter._id}-${chapter.subjectRef}`; return { ...chapter.toObject(), questionCount: countMap[key] || 0 }; });
方案2:单次聚合查询完成所有操作
利用MongoDB的$lookup管道聚合,在查询章节的同时直接关联统计对应问题数量,仅需1次数据库查询:
const chapters = await Chapter.aggregate([ // 关联questions集合并统计符合条件的问题数 { $lookup: { from: "questions", // 对应Question集合的实际名称 let: { chapterId: "$_id", subjectId: "$subjectRef" }, pipeline: [ { $match: { $expr: { $and: [ { $eq: ["$chapterRef", "$$chapterId"] }, { $eq: ["$subjectRef", "$$subjectId"] } ] } } }, { $count: "total" } ], as: "questionCountTemp" } }, // 提取统计结果,处理空值为0 { $addFields: { questionCount: { $arrayElemAt: ["$questionCountTemp.total", 0] } } }, { $set: { questionCount: { $ifNull: ["$questionCount", 0] } } }, // 移除临时字段(可选) { $project: { questionCountTemp: 0 } } ]); // 转换为mongoose文档对象(按需选择) return chapters.map(chapter => new Chapter(chapter));
优化说明
两种方案均将查询次数从N+1降至1或2次,大幅提升性能:
- 方案1逻辑直观,适合需要单独复用问题统计数据的场景
- 方案2用单次聚合完成所有操作,性能最优,推荐多数场景下使用
内容的提问来源于stack exchange,提问作者soubhagya
相关产品推荐
相关产品推荐

