$lookup在副本集中变慢的主因是子查询强制走主节点且不利用从节点索引,若localField与foreignField未建同向索引会导致COLLSCAN;必须在所有副本成员上创建严格同向的索引,并通过explain确认IXSCAN生效及totalDocsExamined合理。

为什么$lookup在副本集里会突然变慢
不是驱动或网络问题,而是$lookup执行时默认走主节点,且不自动利用从节点索引——哪怕被关联集合(from指定的集合)在从节点上有完全相同的索引,MongoDB 4.4 也不会把$lookup的子查询下推到从节点执行。更关键的是,如果localField和foreignField没建索引,或者索引方向不一致(比如一个是{ a: 1 },另一个是{ a: -1 }),就会触发全集合扫描(COLLSCAN),数据量一过千万,单次查询轻松卡在 2s+。
必须加的两个索引组合
只在主节点上建索引不够,所有副本集成员都得有——因为读偏好(readPreference)设为secondaryPreferred时,$lookup的主查询可能发到从节点,但子查询仍由主节点执行;而索引缺失会导致主节点对被关联集合做全表扫。
-
db.mainCollection.createIndex({ localField: 1 })—— 主集合上用于匹配的字段,升序即可 -
db.foreignCollection.createIndex({ foreignField: 1 })—— 被关联集合上对应字段,必须严格同向(不能一个1一个-1) - 如果
$lookup带pipeline(例如过滤+投影),且该 pipeline 里还有字段参与筛选,得额外给那些字段建索引,比如:db.foreignCollection.createIndex({ foreignField: 1, status: 1, createdAt: -1 })
用explain("executionStats")确认是否真走索引
别信“有索引就快”,得看执行计划里totalDocsExamined是不是接近nReturned。如果totalDocsExamined是几百万,nReturned才几十,说明索引没生效,还在全表扫。
执行示例:
db.mainCollection.aggregate([
{ $lookup: {
from: "foreignCollection",
localField: "refId",
foreignField: "id",
as: "detail"
}
}
]).explain("executionStats")
重点看输出里的:
-
stages[0].$lookup.executionStats.totalDocsExamined—— 这个值应 ≤ 被关联集合总文档数的 5%,否则索引失效 -
stages[0].$lookup.executionStats.executionTimeMillis—— 若 > 300ms,基本可判定瓶颈在此 -
stages[0].$lookup.executionStats.executionStages[0].stage应为IXSCAN,不是COLLSCAN
副本集场景下容易被忽略的三个硬限制
即使索引全对、explain看着也 OK,高并发下$lookup仍可能集体变慢——根源常不在查询本身,而在资源争抢。
-
maxConns设太低:副本集每个节点默认maxConns=819,一个慢$lookup占住连接 1~2 秒,QPS 到 400 就开始排队;检查方式:db.serverStatus().connections.current接近maxConns就危险 -
wiredTigerCacheSizeGB不足:若被关联集合远大于内存缓存,每次$lookup都要刷盘读,性能断崖下跌;建议设为物理内存的 50%~60%,且确保所有副本节点配置一致 - 未禁用
readConcern: "majority":4.4 默认开启,$lookup子查询也会等多数节点写入确认,放大延迟;如业务能接受“已提交但未多数确认”的数据,显式设readConcern: { level: "local" }
真正卡住的往往不是语法,是索引跨节点不一致、缓存配比失衡、或读关注悄悄拖慢了整个 pipeline。调完索引务必跑一次真实流量压测,光看单条 explain 没用。


















