根本原因是foreignField未建单字段B-tree索引;必须为from集合的foreignField显式创建单字段索引(如{"_id":1}),复合索引、哈希索引均无效,嵌套字段需全路径索引,分片集群须各分片单独创建,且localField索引是前置过滤刚需。

根本原因不是“没走索引”,而是你压根没在被关联的集合(from集合)上,为foreignField建对索引。
foreignField 必须有单字段 B-tree 索引,复合索引完全无效
很多人在 products 集合上建了 {_id: 1, name: 1} 或 {category: 1, _id: 1},以为 $lookup 查 _id 能用上。错。MongoDB 对 foreignField 的等值匹配只认单字段索引,且必须是 {_id: 1} 这种显式声明的结构。
- 嵌套字段如
"metadata.code",索引必须写全路径:db.products.createIndex({"metadata.code": 1}) - 哈希索引(
{_id: "hashed"})不支持等值路由,$lookup无法使用 - 分片集群下,该索引必须在每个分片上手动创建,不能只在 mongos 上建——否则部分分片仍会全表扫描
localField 索引不是可选,而是前置过滤刚需
localField 所在集合的索引不加速 $lookup 本身,但决定有多少文档会进入 $lookup 阶段。没它,$lookup 可能被调用成千上万次。
- 错误顺序:
[{$project: {userId: 1}}, {$lookup: {...}}]——$project后无法利用userId上的索引,大量无关文档涌入 - 正确顺序:
[{$match: {status: "paid"}}, {$lookup: {...}}, {$project: {...}}],且status字段必须有索引 - 若
localField是数组(如tags: ["a", "b"]),MongoDB 会对每个元素执行一次$lookup,索引压力翻倍
分片环境下,$lookup 默认不推送到分片执行
即使 from 集合已分片,$lookup 默认行为仍是把左表所有匹配文档拉到 mongos 再做关联——这不是索引问题,是路由机制限制。
- 只有当两个集合分片键完全一致(如都按
{tenantId: 1}分片),且let绑定的变量严格来自该分片键前缀时,才能精准路由 -
let: {tid: "$tenantId"}安全;let: {uid: "$userId"}不安全——除非userId单独也是分片键 - 一旦加了
pipeline参数(如pipeline: [{$match: {active: true}}]),立刻退回到mongos执行,索引再好也白搭
验证索引是否真生效,别信 explain 里的 IXSCAN
explain("executionStats") 输出里出现 IXSCAN,不代表 $lookup 用了索引。关键看 executionStats.shards 下每个分片的 totalDocsExamined 是否接近实际匹配数。
- 如果某分片的
totalDocsExamined接近该分片总文档数,说明它在本地做了全表扫描——索引没建或类型不一致 - 查不到数据却没报错?
$lookup默认静默失败:字段缺失、类型不匹配(ObjectIdvs 字符串)、集合名拼错、版本不支持分片from,都会导致空数组 - 真正麻烦的是这些点漏掉任何一个,
$lookup就变成“看起来在跑,实际没干活”











