$lookup慢的主因是默认不裁剪字段且对数组localfield会多次全表扫描,必须为localfield和foreignfield建单字段索引,目标集合不能分片,应先$match再$lookup,用$project精简字段,子文档过滤须用$set+$filter而非$lookup内$match。

为什么 $lookup 会慢得明显
不是 $lookup 本身慢,而是它默认不做任何裁剪:哪怕你只想要子文档的 name 和 status 字段,$lookup 仍会把整个目标文档(可能含大字段如 data、content)全拉过来。更关键的是,如果 localField 是数组(比如 group_ids: [1,2,3]),MongoDB 会为每个 ID 单独查一次目标集合——没有索引时就是 N 次全表扫描。
-
localField和foreignField都必须有单字段索引,缺一不可;否则$lookup阶段无法走索引,性能断崖式下跌 - 目标集合(
from)不能是分片集合,否则直接报错cannot run $lookup on sharded collection - 如果
localField为空数组或null,as字段仍会生成空数组[],但不会报错——容易被误认为“没数据”,其实是匹配逻辑失效
先过滤再 $lookup,顺序不能反
很多人习惯写成 $lookup → $match,这会让 MongoDB 先关联全部数据,再筛结果,浪费大量内存和网络传输。正确做法是把 $match 放在 $lookup 前面,尤其是对主集合做条件筛选时。
- 例如查某个用户的所有未归档工作簿:
$match先锁定workspace._id,再$lookup关联workbooks,而不是反过来 - 如果要按子文档字段过滤(如
isArchived: false),$match无法直接作用于$lookup输出的数组,必须用$set + $filter组合,且$filter必须放在$lookup之后 - 避免在
$lookup后立即$unwind—— 这会爆炸式放大文档数量,后续$match效率极低;应优先用$filter在数组层面收缩
限制字段比限制数量更关键
$project 不只是“选字段”,它是减少管道内存占用最直接的手段。一个 50KB 的子文档,和只取其中 3 个字段(共 200B),对网络、CPU、聚合缓冲区压力完全不同。
- 在
$lookup后加$project,明确指定目标集合中真正需要的字段,比如{"name": 1, "status": 1, "_id": 1},其他全不带 - 不要依赖应用层裁剪:MongoDB 不会在 wire protocol 层帮你省流量,字段越多,序列化/反序列化开销越大
- 若目标集合字段名和主集合冲突(比如都有
name),$project可重命名,避免覆盖,例如"wb_name": "$workbooks.name"
嵌套数组里筛子文档,别硬套 $match
很多人试图在 $lookup 的 pipeline 参数里写 $match 来过滤子文档,但这只在 MongoDB 5.0+ 且 localField 是单值时才生效;对数组引用字段(如 workbooks: [ObjectId(...)]),该 $match 会被忽略——这是最常踩的坑。
- 正确姿势是:先
$lookup拉全量子文档数组,再用$set+$filter处理,例如:$filter: { input: "$workbooks", cond: { $ne: ["$$this.isArchived", true] } } -
$$this是$filter的迭代变量,不是$开头的路径表达式,写成"$this.isArchived"会静默失败 - 如果子文档字段可能缺失(如部分没写
isArchived),$ne: ["$$this.isArchived", true]仍能保留它们;想严格等于false,得用$eq: ["$$this.isArchived", false]
localField 和 foreignField 索引存在,并把 $match 尽量前移、$project 尽量收紧、子文档过滤用 $filter 而非幻想 $lookup 内置条件,90% 的引用型慢查询就能落地见效。











