$lookup是MongoDB实现左外连接的唯一标准方式,必须指定大小写敏感的localField和foreignField;跨库需用{db: "", coll: ""}格式;性能优化须避免前置$unwind、确保双字段索引、限制嵌套层级并提前投影裁剪。

用 $lookup 实现左外连接是最直接的方案
MongoDB 本身不支持 SQL 风格的 JOIN,但 $lookup 聚合阶段能完成等价功能——它本质上是左外连接(left outer join),即以主集合为基准,把匹配的关联文档“挂载”到每条结果上。
常见错误是误以为 $lookup 默认能跨数据库查:它默认只在当前数据库内工作;若要查其他库,必须显式指定 from 为 "otherdb.collection" 格式(4.2+ 支持),且目标集合需在同副本集或分片集群中可访问。
典型用法包括:
-
localField和foreignField必须类型一致,比如都是ObjectId或都是字符串;混用字符串 ID 和 ObjectId 会导致无声失败(查不到数据) - 如果关联字段带索引(尤其
foreignField),$lookup性能会明显提升;没索引时可能触发全表扫描 - 结果会生成一个数组字段(如
users: []),即使只匹配一条也包在数组里;后续要用$arrayElemAt或$unwind展开
需要内连接效果?加 $unwind 并设 preserveNullAndEmptyArrays: false
原生 $lookup 是左外连接,会保留主集合所有文档,哪怕关联无结果(此时输出空数组)。但业务常需要类似 SQL INNER JOIN 的行为:只返回两边都有匹配的记录。
这时不能只靠 $lookup,得接一个 $unwind 并关闭空值保留:
{
$unwind: {
path: "$profile",
preserveNullAndEmptyArrays: false
}
}
关键点:
-
preserveNullAndEmptyArrays: false是核心开关,设为true(默认)时,空数组或 null 字段会被跳过,但文档仍保留;设为false才真正过滤掉无关联的文档 - 如果关联字段可能为 null 或缺失,建议先用
$addFields+$ifNull统一补默认值,避免$unwind报错 - 注意
$unwind后若关联多条,主文档会被“复制”多次——这是预期行为,不是 bug
关联查询慢?优先检查 foreignField 是否有索引
$lookup 的性能瓶颈几乎总卡在 foreignField 的查找效率上。MongoDB 不会自动为 $lookup 创建索引,必须手动建:
db.profiles.createIndex({ userId: 1 })
几个易忽略的细节:
- 索引方向(
1或-1)对单字段查找无影响,但复合索引要注意字段顺序 - 如果
$lookup中用了pipeline选项(比如带$match或$project),索引仍只加速初始foreignField匹配,后续 pipeline 操作不会走索引 - 分片集合上做
$lookup,要求localField和foreignField都是分片键,否则操作会被拒绝(4.4+ 更严格)
想查多个关联集合?连续写多个 $lookup,但注意顺序和字段名冲突
一个聚合管道可以链式调用多次 $lookup,比如先连 profiles,再连 orders。但实际写时容易踩两个坑:
- 第二次
$lookup的localField必须引用第一次的结果字段名,比如第一次输出到"profile",第二次就得写localField: "profile.userId",而不是原始字段 - 如果两次
$lookup都输出到同名字段(如都用as: "data"),后一次会覆盖前一次;务必用不同字段名,比如as: "profile"和as: "orders" - 多个
$lookup会显著增加内存消耗,特别是关联结果量大时;必要时用$limit或$project提前裁剪字段
跨集合关联没有银弹,$lookup 看似简单,但字段类型、索引、空值处理、命名冲突这些点,漏掉任何一个都可能让查询返回空结果或性能骤降。


















