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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.31 00:06:23