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

如何用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
    }
  }
])

各阶段说明

  1. $match:快速过滤出包含目标用户的聊天文档,建议给participants字段建立索引提升查询效率。
  2. $lookup(嵌套管道):
    • 关联message集合,通过chat_id匹配当前聊天ID;
    • 对每个聊天的消息按create_date倒序排序后取第一条,确保拿到最新消息;
    • 嵌套子查询统计该聊天中目标用户的未读消息数(仅统计其他用户发送且未查看的消息)。
  3. $unwind:将last_message数组转为单个对象,preserveNullAndEmptyArrays确保没有消息的聊天也能保留在结果中。
  4. $addFields:给未读数设置默认值0,避免空值影响前端展示。
  5. $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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 09:06:39