$lookup慢90%因foreignField索引建错:必须为foreignField单独建单字段索引,复合索引无效;嵌套字段需全路径索引;分片集群下须各分片统一建索引。

$lookup慢,90%是因为foreignField没建对索引——不是没建,是建错了。
foreignField必须单独建单字段索引,复合索引完全无效
很多人给products集合建了{_id: 1, category: 1},以为查_id能走索引。错。$lookup只做等值匹配,且只用foreignField单个字段值,不走复合索引前缀。
-
db.products.createIndex({ _id: 1 })才有效;{_id: 1, name: 1}或{name: 1, _id: 1}都无效 - 如果
foreignField是嵌套字段,比如"metadata.code",索引必须写全路径:db.products.createIndex({ "metadata.code": 1 }) - 分片集群下,该索引需在每个分片上存在,且类型一致(不能一个分片是哈希索引,另一个是B-tree)
localField索引不是可选,而是前置加速前提
localField所在集合的索引不直接加速$lookup本身,但决定了有多少文档会流进$lookup阶段。没它,$lookup可能被调用成千上万次。
- 错误写法:
[{$project: {productId: 1}}, {$lookup: {}}]—— $project后无法利用productId上的索引过滤,大量无关文档涌入 - 正确顺序:先
$match(带索引字段),再$lookup,最后$project - 如果
localField是数组(如tags: ["a","b"]),MongoDB会对每个元素分别执行一次$lookup,索引压力翻倍,务必确认是否真需要这种语义
分片集群下$lookup默认不推送到分片执行
默认行为是把左表所有匹配文档拉到mongos再关联,网络和内存压力巨大。4.4+虽支持“交换算子下推”,但条件苛刻:
- 右表(
from集合)必须未分片,或与左表同shard key -
foreignField必须是右表的_id或唯一索引字段 -
$lookup不能带pipeline参数——一旦写了pipeline: [{ $match: { status: "paid" } }],立刻退回到mongos执行 - 验证是否下推:开profiler后查
slowms日志,找"executionStages.stage": "LOOKUP_SHARDING",而不是"LOOKUP"
事务中$lookup读不到事务快照
事务内跑$lookup,查到的右表数据用的是最终一致视图,不是事务开启那一刻的MVCC快照。并发更新时,结果可能比事务内其他操作“旧几毫秒”。
- 这不是bug,是MongoDB模型限制:$lookup不参与事务快照机制
- 分片集群下更明显:各分片读取时间点不统一
- 强一致需求下,优先考虑冗余关键字段(如把
user.email嵌入orders),或事务内先findOne
最容易被忽略的一点:索引是否生效,不能只看explain里有没有IXSCAN字样——它出现在管道某阶段,不代表$lookup用了索引。真正要查的是from集合本身的explain输出,或者直接在system.profile里抓LOOKUP操作的docsExamined值。











