MongoDB 聚合管道中的 $lookup 关联查询

FreeGuideOnline 最新 2026-07-09

javascript { $lookup: { from: <要连接的集合名称>, localField: <当前集合中的字段>, foreignField: <目标集合中的字段>, as: <输出数组中存放连接结果的新字段名> } }


对于更复杂的关联条件,MongoDB 3.6+ 提供了带有嵌套管道(pipeline)的语法:

```javascript
{
  $lookup: {
    from: <目标集合>,
    let: { <变量1>: <表达式>, ... },
    pipeline: [ <在目标集合上执行的管道阶段> ],
    as: <输出字段名>
  }
}

参数说明

参数 说明
from 要关联的集合名称,必须是同一数据库中的集合。
localField 当前集合并入连接条件的字段。
foreignField 目标集合中用于连接的字段。
as 存放匹配结果的新数组字段名称。如果当前文档中已存在该字段,则会被覆盖。
let (可选) 定义在管道中使用的变量,变量的值可以来自当前文档的字段。
pipeline (可选) 在目标集合上执行的聚合管道,用于过滤、投射、转换等操作。注意不能包含 $out$merge 阶段。

基础示例:一对多关联

假设有两个集合:orders(订单)和 customers(客户)。

orders 文档结构:

{ "_id": 1, "item": "笔记本", "price": 8000, "customerId": 101 }
{ "_id": 2, "item": "鼠标", "price": 150, "customerId": 102 }

customers 文档结构:

{ "_id": 101, "name": "张三", "email": "zhangsan@example.com" }
{ "_id": 102, "name": "李四", "email": "lisi@example.com" }

查询每个订单及其对应的客户信息:

db.orders.aggregate([
  {
    $lookup: {
      from: "customers",
      localField: "customerId",
      foreignField: "_id",
      as: "customer"
    }
  }
])

返回结果:

[
  {
    "_id": 1,
    "item": "笔记本",
    "price": 8000,
    "customerId": 101,
    "customer": [
      { "_id": 101, "name": "张三", "email": "zhangsan@example.com" }
    ]
  },
  {
    "_id": 2,
    "item": "鼠标",
    "price": 150,
    "customerId": 102,
    "customer": [
      { "_id": 102, "name": "李四", "email": "lisi@example.com" }
    ]
  }
]

customer 始终是一个数组,即使只匹配到一个文档。如果目标集合中没有匹配的文档,数组为空 []

进阶用法:使用 pipeline 实现复杂关联

当简单的等值连接无法满足需求时,比如需要连接多个条件、对目标集合进行过滤或字段转换,可以使用带 pipeline$lookup

示例:连接时过滤目标文档

依然使用 orders 集合,但这次我们只想取回状态为“active”的客户。

customers 集合增加一个 status 字段:

{ "_id": 101, "name": "张三", "email": "zhangsan@example.com", "status": "active" }
{ "_id": 102, "name": "李四", "email": "lisi@example.com", "status": "inactive" }

查询:

db.orders.aggregate([
  {
    $lookup: {
      from: "customers",
      let: { custId: "$customerId" },
      pipeline: [
        { $match: { $expr: { $eq: ["$_id", "$$custId"] }, status: "active" } },
        { $project: { _id: 0, name: 1, email: 1 } }
      ],
      as: "customer"
    }
  }
])

说明:

  • let 定义了变量 custId,其值为当前文档的 customerId
  • pipeline 中,$match 使用 $expr 结合 $$custId 引用外部变量,并额外过滤 status
  • $project 仅返回需要的字段。

这样,如果客户是 inactive 状态,customer 数组就会是空。

多字段关联与数组关联

有时连接条件不止一个字段,或者目标字段是数组,同样可以用 pipeline 优雅解决。

示例:多字段连接

假设 orderscustomers 需要通过 customerIdregion 字段共同关联。

db.orders.aggregate([
  {
    $lookup: {
      from: "customers",
      let: { o_custId: "$customerId", o_region: "$region" },
      pipeline: [
        { $match: {
            $expr: {
              $and: [
                { $eq: ["$_id", "$$o_custId"] },
                { $eq: ["$region", "$$o_region"] }
              ]
            }
          }
        }
      ],
      as: "customer"
    }
  }
])

示例:foreignField 是数组

如果目标集合的关联字段是数组,不能直接用简单的 localField / foreignField 语法(那只适用于单值字段),需要使用 pipeline$in

customers 集合中 orderIds 是一个数组:

{ "_id": 101, "name": "张三", "orderIds": [1, 3, 4] }

反向查找某个订单所属的客户:

db.orders.aggregate([
  {
    $lookup: {
      from: "customers",
      let: { orderId: "$_id" },
      pipeline: [
        { $match: { $expr: { $in: ["$$orderId", "$orderIds"] } } }
      ],
      as: "customer"
    }
  }
])

$lookup 与 $unwind 配合

由于 $lookup 产生的关联字段是一个数组,如果确定每条记录最多只有一个匹配结果(例如一对一或一对一并入),我们通常使用 $unwind 将数组展开为单个对象,方便后续处理。

db.orders.aggregate([
  {
    $lookup: {
      from: "customers",
      localField: "customerId",
      foreignField: "_id",
      as: "customer"
    }
  },
  { $unwind: "$customer" }
])

更安全的方式是使用带 preserveNullAndEmptyArrays 选项的 $unwind,避免因未匹配到文档而导致整条文档被丢弃。

{ $unwind: { path: "$customer", preserveNullAndEmptyArrays: true } }