当前位置:首页 > 问答 > 正文

MongoDB里头那些复杂的关联查询怎么搞,感觉有点绕但其实能实现不少花样

MongoDB的关联查询,核心就是那个 $lookup 阶段,你别把它想得太神秘,它就相当于SQL里的 LEFT OUTER JOIN,但因为它是在聚合管道里用的,所以组合起来特别灵活,能玩出很多花样,感觉绕,主要是因为它的写法跟SQL不一样,是分步骤、像流水线一样处理的。

最基础的关联:两个集合手拉手

假设你有两个集合,一个叫 orders(订单),一个叫 users(用户),每个订单文档里存了一个 user_id,对应着 users 集合里的 _id

你想查所有订单,并且把下单用户的信息也带出来,SQL你可能这么写:SELECT * FROM orders LEFT JOIN users ON orders.user_id = users.id

在MongoDB的聚合管道里,就这么搞:

db.orders.aggregate([
  {
    $lookup: {
      from: "users",        // 要从哪个集合关联?users集合
      localField: "user_id", // 当前orders集合里的关联字段是啥?user_id
      foreignField: "_id",   // 目标users集合里的关联字段是啥?_id
      as: "user_info"        // 关联查询的结果,作为一个新数组字段,叫什么名字?比如叫user_info
    }
  }
])

执行完后,每个订单文档都会多一个 user_info 字段,它是一个数组,因为$lookup默认设计就是处理“一对多”的,所以即使你明确知道是“一对一”,它也会返回数组,通常你拿到结果后,如果确定是一对一,可以再加个 $unwind 阶段把数组拆开,变成内嵌文档,看着更舒服。

进阶玩法:多层嵌套关联

这才是MongoDB关联查询威力显现的地方,订单里不仅有用户ID,每个订单还有多个商品ID(product_ids),商品详情在 products 集合里。

现在你想查订单,同时带上用户信息,以及每个商品的详细信息,这就需要进行“嵌套关联”或者叫“多次关联”。

思路是分两步走:

  1. 先把订单和用户关联起来。
  2. 在已经关联了用户信息的结果上,再去关联商品集合。
db.orders.aggregate([
  // 第一关:关联用户信息
  {
    $lookup: {
      from: "users",
      localField: "user_id",
      foreignField: "_id",
      as: "user_info"
    }
  },
  // 第二关:关联商品信息,注意,这里关联的是orders里的product_ids数组
  {
    $lookup: {
      from: "products",
      localField: "product_ids", // 这次本地字段是一个数组
      foreignField: "_id",
      as: "product_details"     // 商品详情数组
    }
  },
  // 可选:把user_info数组拆开(因为是一对一)
  {
    $unwind: {
      path: "$user_info",
      preserveNullAndEmptyArrays: true // 即使没关联到用户,也保留订单记录(左关联)
    }
  }
])

这样出来的结果,每个订单文档就有 user_info(用户对象)和 product_details(商品对象数组)了,查询逻辑非常清晰。

更骚的操作:带条件的关联

普通的 $lookup 是“全量关联”,但有时候我们关联的时候还想加点条件,我们不想关联用户的所有信息,只想关联那些“VIP用户”,或者只想关联最近活跃的用户。

这时候,就需要用到 $lookup 的“高级模式”了,也就是引入 pipeline

我们只想关联那些 level 字段为 "vip" 的用户:

db.orders.aggregate([
  {
    $lookup: {
      from: "users",
      let: { order_user_id: "$user_id" }, // 把本地字段的值赋给一个变量
      pipeline: [ // 在这里面写一个针对users集合的子聚合管道
        {
          $match: {
            $expr: {
              $and: [
                { $eq: ["$_id", "$$order_user_id"] }, // 关联条件:_id 等于 订单里的user_id
                { $eq: ["$level", "vip"] }            // 附加条件:用户等级是vip
              ]
            }
          }
        },
        // 你还可以在子管道里做投影,只取需要的字段,比如只取name和email
        {
          $project: { name: 1, email: 1 }
        }
      ],
      as: "vip_user_info"
    }
  }
])

这个功能非常强大,相当于你在关联的时候,还能对要关联的那个集合先进行一番筛选、加工,再把结果拿过来,这就不只是“关联”了,而是“有条件的精准关联”。

处理“一对多”关系中的“多”端查询

你的查询出发点不是“一”这一边,而是“多”那一边,你想查一个用户的所有订单。

有两种思路:

  1. 从用户集合出发,关联订单集合:在 users 集合上做聚合,用 $lookup 关联 orders 集合,本地字段是 _id,外部字段是 user_id,这样每个用户文档都会带一个他的所有订单的数组。
  2. 直接查询订单集合,用匹配条件:如果你最终想要的是订单列表,只是要按用户来筛选,那更简单,直接在 orders 集合里用 $matchdb.orders.find({ user_id: targetUserId }),这种情况下,甚至不需要用 $lookup

为啥感觉绕但又能实现花样:

  • 绕的原因:思维需要从SQL的“声明式”(直接写JOIN条件)切换到MongoDB聚合管道的“步骤式”(一步一步组装数据),特别是处理数组字段和嵌套关联时,需要清晰地知道每一步之后,你的文档变成了什么样子。
  • 花样的来源:正因为是“步骤式”的,你可以把 $lookup 和聚合框架里其他几十个阶段($match过滤, $project投影, $group分组, $unwind拆数组, $sort排序)任意组合,你可以先关联,再根据关联出来的某个字段进行分组统计;或者先拆开数组关联,再分组回去,这种流水线式的处理方式,给了你极大的灵活性。

玩转MongoDB关联查询的关键就两点:一是彻底理解 $lookup 的两种写法(基础版和pipeline版);二是学会在脑子里或纸上画出数据在聚合管道每一步中的形态变化,一旦习惯了这种思维方式,你会发现它能实现的查询场景,远比想象中要丰富。

MongoDB里头那些复杂的关联查询怎么搞,感觉有点绕但其实能实现不少花样