MongoDB聚合查询优化:屏蔽用户排除的未读消息统计提速方案
问题背景
我有两个MongoDB集合:
conversations2:存储聊天会话数据,每条记录对应一个用户与其他用户的对话,包含unreadMessageCount(未读消息数)字段user_blocked:存储系统中被屏蔽的用户ID
需求是统计指定用户(如userId=32)的未读消息总数,但需要排除对方(otherUserId)属于屏蔽列表的会话记录。当前使用的聚合查询能实现需求,但针对消息量大的用户(比如该用户有45万条会话记录)查询速度过慢。已为conversations2.userId、conversations2.otherUserId和user_blocked.id单独创建索引,现寻求优化方案。
当前查询代码
db.conversations2.aggregate([ { $match: { userId: 32 } }, { $lookup: { from: "user_blocked", localField: "otherUserId", foreignField: "id", as: "blockedUsers" } }, { $match: { blockedUsers: { $eq: [] } } }, { $group: { _id: "$userId", unreadMessageCount: { $sum: "$unreadMessageCount" } } } ])
集合示例数据
conversations2
{ "_id": { "$oid": "65c0f64030054c4b8f0481a0" }, "otherUserId": { "$numberLong": "45" }, "userId": { "$numberLong": "32" }, "lastMessage": "test", "lastMessageTime": { "$date": "2024-02-21T10:36:44.592Z" }, "lastMessageType": 1, "lastMessageWay": "in", "unreadMessageCount": 29 }
user_blocked
{ "_id": { "$oid": "66033f989bba279fe7d0862a" }, "id": { "$numberLong": "45" } }
原查询性能瓶颈分析
原查询的核心问题是:先通过$match过滤出目标用户的所有会话(45万条),然后对每条记录单独执行$lookup关联user_blocked集合。这种逐条关联的方式会触发大量的单条查询,即使有索引,也会产生极高的IO开销,导致整体速度缓慢。
优化方案
方案1:预取屏蔽列表,用$nin直接过滤(推荐)
先一次性获取所有被屏蔽的用户ID,再在$match阶段直接排除这些ID对应的会话,避免逐条关联操作。
// 第一步:获取所有被屏蔽的用户ID数组 const blockedUserIds = db.user_blocked.find({}, { id: 1, _id: 0 }).map(doc => doc.id); // 第二步:聚合统计未读消息数 db.conversations2.aggregate([ { $match: { userId: 32, otherUserId: { $nin: blockedUserIds } } }, { $group: { _id: "$userId", unreadMessageCount: { $sum: "$unreadMessageCount" } } } ])
优化点:
- 把原来的N次关联查询变成1次查询+批量过滤,大幅减少IO操作
conversations2上的userId和otherUserId索引可以被$match阶段高效利用,快速过滤出符合条件的记录
方案2:优化$lookup的关联逻辑
如果不想拆分两次查询,可以使用$lookup的子管道功能,在关联阶段就终止不必要的查询,减少数据传输量。
db.conversations2.aggregate([ { $match: { userId: 32 } }, { $lookup: { from: "user_blocked", let: { targetOtherId: "$otherUserId" }, pipeline: [ { $match: { $expr: { $eq: ["$id", "$$targetOtherId"] } } }, { $limit: 1 } // 匹配到第一条就停止查询,减少数据返回 ], as: "blockedUsers" } }, { $match: { blockedUsers: { $size: 0 } // 用$size判断空数组,比$eq: []性能更优 } }, { $group: { _id: "$userId", unreadMessageCount: { $sum: "$unreadMessageCount" } } } ])
优化点:
- 子管道中用
$expr做字段匹配,配合$limit:1避免返回多余数据 - 使用
$size:0替代$eq:[],MongoDB对$size的判断逻辑更高效
方案3:创建覆盖复合索引
为conversations2创建覆盖索引,让查询完全通过索引完成,不需要回表读取文档数据,进一步提升速度。
创建索引:
db.conversations2.createIndex({ userId: 1, otherUserId: 1, unreadMessageCount: 1 })
该索引包含了$match阶段需要过滤的userId、otherUserId,以及$group阶段求和需要的unreadMessageCount,MongoDB可以直接从索引中获取所有需要的数据,无需访问文档本身。
验证优化效果
可以通过explain("executionStats")查看查询计划,确认索引是否被命中,以及查询的耗时、扫描行数等指标:
db.conversations2.aggregate([/* 优化后的查询 */]).explain("executionStats")
内容的提问来源于stack exchange,提问作者Mevo

