$lookup慢主因是foreignfield未建单字段索引,复合索引无效;必须为foreignfield显式创建单字段索引,嵌套字段写完整路径,分片集群需各分片统一索引;localfield所在集合须前置$match走索引过滤,避免数据膨胀导致$lookup次数激增。

$lookup慢,八成是因为foreignField没建对索引,不是数据量大,而是索引建错位置。
foreignField必须单独建单字段索引,复合索引完全无效
很多人在products集合上建了{name: 1, category: 1},然后用category做foreignField,结果$lookup还是全表扫。MongoDB查foreignField只走最左前缀匹配,等值查询不会跳到复合索引第二字段。
- 正确做法:显式为
foreignField建单字段索引,比如db.products.createIndex({"category": 1}) - 嵌套字段要写完整路径:
db.products.createIndex({"metadata.status": 1}) - 分片集群下,该索引必须在每个分片上都存在,且类型一致(不能一个分片是哈希索引,另一个是B-tree)
localField索引不是可选,而是前置加速必要条件
localField所在集合的索引不直接加速$lookup本身,但决定有多少文档会流入$lookup阶段。如果前面没走索引过滤,几万条文档挨个去联查,性能必然崩。
- 错误写法:
[{$project: {productId: 1}}, {$lookup: {...}}]——$project后无法利用productId上的索引 - 正确顺序:先
$match(带索引字段),再$lookup,最后$project - 若
localField是数组(如tags: ["a","b"]),MongoDB会对每个元素执行一次$lookup,foreignField索引压力翻倍,需确认是否真需要这种语义
验证索引是否生效,别信explain里的“IXSCAN”字样
db.collection.aggregate(pipeline).explain("executionStats")输出里出现IXSCAN,不代表$lookup用了索引。它可能只是管道里其他阶段(比如前置$match)走了索引。
- 真正要看的是
from集合的执行计划:把$lookup拆出来单独模拟,比如db.products.find({ _id: ObjectId("...") }),再.explain()确认是否命中索引 - 检查
executionStats.nReturned和executionStats.totalDocsExamined是否接近——如果后者远大于前者,说明没走索引 - 跨库关联时,
from: {db: "otherdb", coll: "items"}必须写全,否则默认查当前库,索引再好也查不到数据
最容易被忽略的是:$lookup的瓶颈往往不在联查动作本身,而在它之前的数据膨胀。一个没索引的$match放后面,或一个过早的$unwind,会让$lookup被迫执行成千上万次。优化永远从管道入口开始,而不是盯着$lookup改参数。











