如何用MongoDB聚合查询为指定用户聊天列表关联最新消息
如何用MongoDB聚合查询实现带最新消息的用户聊天列表获取
核心思路
先筛选出指定用户参与的所有聊天,再通过关联查询获取每个聊天的最新消息,最后将消息合并到聊天文档中,同时可顺带计算未读消息数。
完整聚合查询(MongoDB Shell 语法)
假设目标用户ID为 targetUserId,以下是聚合管道:
db.chat.aggregate([ // 1. 筛选当前用户参与的聊天 { $match: { participants: targetUserId } }, // 2. 关联message集合,获取每个聊天的最新消息及未读数 { $lookup: { from: "message", let: { chatId: "$id" }, pipeline: [ { $match: { $expr: { $eq: ["$chat_id", "$$chatId"] } } }, // 按创建时间倒序,取最新的一条消息 { $sort: { create_date: -1 } }, { $limit: 1 }, // 统计当前用户在该聊天中的未读消息数 { $lookup: { from: "message", let: { chatId: "$chat_id", userId: targetUserId }, pipeline: [ { $match: { $expr: { $and: [{ $eq: ["$chat_id", "$$chatId"] }, { $ne: ["$from_id", "$$userId"] }, { viewed: false }] } } }, { $count: "unread_count" } ], as: "unread_info" } }, { $addFields: { unread: { $arrayElemAt: ["$unread_info.unread_count", 0] } } }, { $project: { unread_info: 0 } } ], as: "last_message" } }, // 3. 将数组形式的最新消息转为单个对象,保留无消息的聊天 { $unwind: { path: "$last_message", preserveNullAndEmptyArrays: true } }, // 4. 调整字段结构,补全未读数默认值 { $addFields: { unread: { $ifNull: ["$last_message.unread", 0] } } }, // 5. 按最新消息时间倒序排列聊天列表 { $sort: { "last_message.create_date": -1 } } ])
各阶段说明
- $match:快速过滤出包含目标用户的聊天文档,建议给
participants字段建立索引提升查询效率。 - $lookup(嵌套管道):
- 关联
message集合,通过chat_id匹配当前聊天ID; - 对每个聊天的消息按
create_date倒序排序后取第一条,确保拿到最新消息; - 嵌套子查询统计该聊天中目标用户的未读消息数(仅统计其他用户发送且未查看的消息)。
- 关联
- $unwind:将
last_message数组转为单个对象,preserveNullAndEmptyArrays确保没有消息的聊天也能保留在结果中。 - $addFields:给未读数设置默认值0,避免空值影响前端展示。
- $sort:按最新消息时间倒序排列,符合常用的聊天列表展示逻辑。
Go 语言实现示例(使用mongodb-go-driver)
import ( "context" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) func GetUserChatsWithLastMessage(col *mongo.Collection, targetUserId string) ([]Chat, error) { pipeline := []bson.M{ { "$match": bson.M{ "participants": targetUserId, }, }, { "$lookup": bson.M{ "from": "message", "let": bson.M{"chatId": "$id"}, "pipeline": []bson.M{ {"$match": bson.M{"$expr": bson.M{"$eq": []string{"$chat_id", "$$chatId"}}}}, {"$sort": bson.M{"create_date": -1}}, {"$limit": 1}, { "$lookup": bson.M{ "from": "message", "let": bson.M{"chatId": "$chat_id", "userId": targetUserId}, "pipeline": []bson.M{ { "$match": bson.M{ "$expr": bson.M{ "$and": []bson.M{ {"$eq": []string{"$chat_id", "$$chatId"}}, {"$ne": []string{"$from_id", "$$userId"}}, {"viewed": false}, }, }, }, }, {"$count": "unread_count"}, }, "as": "unread_info", }, }, {"$addFields": bson.M{"unread": bson.M{"$arrayElemAt": []string{"$unread_info.unread_count", "0"}}}}, {"$project": bson.M{"unread_info": 0}}, }, "as": "last_message", }, }, { "$unwind": bson.M{ "path": "$last_message", "preserveNullAndEmptyArrays": true, }, }, { "$addFields": bson.M{ "unread": bson.M{"$ifNull": []interface{}{"$last_message.unread", 0}}, }, }, { "$sort": bson.M{"last_message.create_date": -1}, }, } cursor, err := col.Aggregate(context.TODO(), pipeline, options.Aggregate()) if err != nil { return nil, err } defer cursor.Close(context.TODO()) var chats []Chat if err := cursor.All(context.TODO(), &chats); err != nil { return nil, err } return chats, nil }
性能优化建议
- 给
chat.participants建立多值索引:db.chat.createIndex({participants: 1}) - 给
message.chat_id和message.create_date建立复合索引:db.message.createIndex({chat_id: 1, create_date: -1}) - 给
message.chat_id、message.from_id和message.viewed建立复合索引(优化未读数统计):db.message.createIndex({chat_id: 1, from_id: 1, viewed: 1})
内容的提问来源于stack exchange,提问作者alex
相关产品推荐
相关产品推荐

