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

如何避免在forEach循环中调用数据库?Express+MongoDB聊天应用优化

优化聊天应用用户列表的消息关联查询

我正在开发一款聊天应用,后端用Express JS + MongoDB,前端是Angular。现有Users和ChatHistory两个集合,Schema如下:

Users集合Schema

const UserSchema = new mongoose.Schema({
    name:{type: type.String, required: true},
    contact:{type: type.String, required: true},
    email:{type: type.String, required: true},
    password:{type: type.String, required: true},
    age:{type: type.Number, default:0},
    pic:{type: type.String, default:''},
    dob: {type:type.Date, default: ''},
    last_seen: {type:type.Date, default: ''},
    online_status: {type:type.String, default: 'offline'},
    show_last_seen: {type:type.Boolean, default: true},
})

ChatHistory集合Schema

const ChatHistorySchema = new mongoose.Schema({
    sender_id:{type: type.ObjectId, ref:'Users', required: true}, // 修正原拼写错误:Usres → Users
    receiver_id:{type: type.ObjectId, ref:'Users',required: true},
    message:{type: type.String,required: true},
    is_deleted:{type: type.Boolean,default:false},
    target_messages_id:{type:type.String,default:''},
    file_name:{type:type.String,default:''},
    message_read_status:{type: type.Boolean,default:false},
})

我需要在展示用户列表时,同时显示每个用户的最后接收消息和未读消息总数。当前实现是先查询所有用户,再循环每个用户调用数据库查询消息,代码如下:

userList = await UsersCollection.find({},{name:1,email:1,online_status:1,last_seen:1,pic:1});
//获取所有用户列表

if(userList.length > 0){
    //遍历每个用户,获取其最后消息与未读消息总数
    async forEach((obj)=>{
        await getLastMessage();
        await getAllUnreadMessageCount()
    })
}

这种循环查库的方式性能很差,有没有更优的实现方案?


优化方案:使用MongoDB聚合查询(Aggregation Pipeline)

直接通过聚合管道将用户数据与聊天记录关联,一次性查询出所有需要的信息,彻底避免N+1查询问题。核心逻辑是:

  1. 过滤排除当前登录用户
  2. 关联聊天记录,提取每个用户与当前用户对话的最后一条消息
  3. 统计当前用户收到的来自该用户的未读消息数
  4. 合并数据并返回需要的字段

具体代码实现(假设当前登录用户ID为currentUserId)

const currentUserId = req.user._id; // 从登录态获取当前用户ID

const userListWithMessages = await UsersCollection.aggregate([
    // 排除当前用户自己
    { $match: { _id: { $ne: currentUserId } } },
    // 关联聊天记录,获取与当前用户的最后一条对话
    {
        $lookup: {
            from: "chathistories", // MongoDB默认集合名为小写复数
            let: { userId: "$_id" },
            pipeline: [
                {
                    $match: {
                        $expr: {
                            $or: [
                                { $and: [{ $eq: ["$sender_id", "$$userId"] }, { $eq: ["$receiver_id", currentUserId] }] },
                                { $and: [{ $eq: ["$receiver_id", "$$userId"] }, { $eq: ["$sender_id", currentUserId] }] }
                            ]
                        },
                        is_deleted: false // 排除已删除消息
                    }
                },
                { $sort: { _id: -1 } }, // 按消息生成时间降序(ObjectId自带时间戳)
                { $limit: 1 }, // 取最后一条消息
                {
                    $project: {
                        last_message: "$message",
                        last_message_time: "$_id"
                    }
                }
            ],
            as: "last_chat"
        }
    },
    // 提取最后消息的具体字段(将数组转为单个值)
    {
        $addFields: {
            last_message: { $arrayElemAt: ["$last_chat.last_message", 0] },
            last_message_time: { $arrayElemAt: ["$last_chat.last_message_time", 0] }
        }
    },
    { $unset: "last_chat" }, // 移除临时数组字段
    // 关联未读消息统计
    {
        $lookup: {
            from: "chathistories",
            let: { userId: "$_id" },
            pipeline: [
                {
                    $match: {
                        $expr: {
                            $and: [
                                { $eq: ["$sender_id", "$$userId"] },
                                { $eq: ["$receiver_id", currentUserId] }
                            ]
                        },
                        message_read_status: false,
                        is_deleted: false
                    }
                },
                { $count: "unread_count" }
            ],
            as: "unread_info"
        }
    },
    // 处理未读消息数,默认0
    {
        $addFields: {
            unread_count: { $ifNull: [{ $arrayElemAt: ["$unread_info.unread_count", 0] }, 0] }
        }
    },
    { $unset: "unread_info" },
    // 保留需要返回的用户字段
    {
        $project: {
            name: 1,
            email: 1,
            online_status: 1,
            last_seen: 1,
            pic: 1,
            last_message: 1,
            last_message_time: 1,
            unread_count: 1
        }
    }
]);

额外性能优化建议

给ChatHistory集合建立复合索引,提升聚合查询速度:

ChatHistorySchema.index({ sender_id: 1, receiver_id: 1, message_read_status: 1, is_deleted: 1 });

内容的提问来源于stack exchange,提问作者Irshad Khan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 04:55:21