MongoDB $lookup关联查询慢致CPU占满100%如何优化
MongoDB关联查询好友列表性能优化方案
问题描述
- 需求:关联
friends与user集合,查询指定用户的所有好友名称 - 初始方案:手动逐次查询,好友量级达到10000条时出现服务崩溃、耗时过长问题
- 替换方案:使用
$lookup算子将关联逻辑下放到数据库层,仍存在性能问题:CPU占用飙升至100%,两个方向的好友查询分别耗时86088ms、86100ms - 当前问题代码如下:
const { friendModel } = require("../Utils/Shemas.js"); const { buildXML } = require("../Utils/Util.js"); exports.data = { SOAPAction: "GetFriendListWithName", needTicket: true, levelModerator: 0 }; exports.run = async (request, ActorId) => { // const friends = await friendModel.find({ RequesterId: ActorId, Status: 1 }); // const friends1 = await friendModel.find({ ReceiverId: ActorId, Status: 1 }); let startD = Date.now(); const friends1 = await friendModel.aggregate([ { $match: { RequesterId: ActorId, Status: 1 }}, { $lookup: { from: "users", localField: "ReceiverId", foreignField: "ActorId", as: "user" }} ]); console.log(`Friends1: ${Date.now() - startD} ms`); // 86 088 ms const friends2 = await friendModel.aggregate([ { $match: { ReceiverId: ActorId, Status: 1 }}, { $lookup: { from: "users", localField: "RequesterId", foreignField: "ActorId", as: "user" }} ]); console.log(`Friends2: ${Date.now() - startD} ms`); // 86 100 ms // console.log(friends1); // console.log(friends2); let FriendData = [ ]; for (let friend of friends1) { FriendData.push({ actorId: friend.user[0].ActorId, name: friend.user[0].Name }); } for (let friend of friends2) { FriendData.push({ actorId: friend.user[0].ActorId, name: friend.user[0].Name }); } /* for(let f = 0; f < friends.length; f++) { const { ActorId, Name } = await userModel.findOne({ ActorId: friends[f].ReceiverId }); FriendData.push({ actorId: ActorId, name: Name }); }; for(let j = 0; j < friends1.length; j++) { const { ActorId, Name } = await userModel.findOne({ ActorId: friends1[j].RequesterId }); FriendData.push({ actorId: ActorId, name: Name }); }; */ return buildXML("GetFriendListWithName", { FriendData: FriendData }); };
性能问题根因
- 缺少必要索引:筛选字段、关联字段无索引会触发全集合扫描,是CPU占满、耗时过长的核心原因
- 查询冗余:分两次独立执行聚合+关联逻辑,重复触发数据库IO和计算
- 无效数据传输:关联查询拉取了用户文档的全量字段,实际仅需要
ActorId和Name两个字段 - 初始手动查询属于典型N+1问题:1万条好友数据会触发1万次单条用户查询,连接开销极大,必然引发服务崩溃
优化步骤
1. 建立对应索引覆盖查询场景
先在MongoDB中执行以下命令创建索引,可直接将查询性能提升两个数量级:
// friends集合创建复合索引,覆盖两个方向的好友状态筛选 db.friends.createIndex({ RequesterId: 1, Status: 1 }) db.friends.createIndex({ ReceiverId: 1, Status: 1 }) // users集合创建关联字段索引,加速$lookup匹配 db.users.createIndex({ ActorId: 1 }, { unique: true })
注意确认
users是实际的集合名称,MongoDB Mongoose默认会将模型名转为小写复数形式作为集合名,集合名写错会导致$lookup关联空集合,引发异常。
2. 重构聚合逻辑,合并查询减少开销
用$unionWith合并两个方向的好友数据,单次聚合完成筛选、关联、字段裁剪全流程,避免多次查询开销,优化后代码如下:
const { friendModel } = require("../Utils/Shemas.js"); const { buildXML } = require("../Utils/Util.js"); exports.data = { SOAPAction: "GetFriendListWithName", needTicket: true, levelModerator: 0 }; exports.run = async (request, ActorId) => { const startD = Date.now(); const FriendData = await friendModel.aggregate([ // 匹配我作为申请人的已通过好友 { $match: { RequesterId: ActorId, Status: 1 } }, // 提前裁剪字段,只保留关联需要的好友ID { $project: { targetActorId: "$ReceiverId", _id: 0 } }, // 合并我作为接收人的已通过好友数据 { $unionWith: { coll: "friends", pipeline: [ { $match: { ReceiverId: ActorId, Status: 1 } }, { $project: { targetActorId: "$RequesterId", _id: 0 } } ] } }, // 单次关联用户信息,仅拉取需要的字段 { $lookup: { from: "users", localField: "targetActorId", foreignField: "ActorId", as: "userInfo", pipeline: [ { $project: { ActorId: 1, Name: 1, _id: 0 } } ] } }, // 解构关联结果,去掉数组嵌套 { $unwind: "$userInfo" }, // 格式化最终输出字段 { $project: { actorId: "$userInfo.ActorId", name: "$userInfo.Name", _id: 0 } } ]); console.log(`好友列表查询总耗时: ${Date.now() - startD} ms`); return buildXML("GetFriendListWithName", { FriendData }); };
3. 进阶优化(可选,面向十万级以上好友量场景)
- 冗余字段换性能:在
friends集合中直接冗余存储好友名称,查询时不需要关联users集合即可直接返回,用户修改名称时异步更新对应好友关系表的冗余字段即可 - 增加缓存:将用户好友列表存入Redis,设置1-5分钟的过期时间,减少重复查库压力
- 分页查询:如果单用户好友量过大,不要一次性返回全量好友,采用分页拉取的方式降低单次查询负载
内容的提问来源于stack exchange,提问作者cypolo
相关产品推荐
相关产品推荐

