MongoDB 可以做集合(表)关联,但和 MySQL JOIN 完全不一样
一、核心结论
- MongoDB支持跨集合关联查询,靠聚合管道
$lookup实现,等价 SQL左外连接 LEFT JOINMongoDB。 - 限制:只能同库关联,不支持跨库;关联结果永远是数组,没有直接 INNER JOIN,需要手动
$unwind模拟内连接。 - 设计理念:MongoDB 优先推荐内嵌文档,少用
$lookup,多表关联性能远不如 MySQL。
二、$lookup 两种用法完整示例
前置测试数据
集合 1:users用户
db.users.insertMany([ {_id: ObjectId("600001"), name:"张三", phone:"13800011111"}, {_id: ObjectId("600002"), name:"李四", phone:"13800022222"} ])集合 2:orders订单(存 userId 关联用户)
db.orders.insertMany([ {_id:1001, userId:ObjectId("600001"), amount:99, createTime:ISODate()}, {_id:1002, userId:ObjectId("600001"), amount:199, createTime:ISODate()}, {_id:1003, userId:ObjectId("600003"), amount:59, createTime:ISODate()} ])1)基础等值 $lookup(简单左连接)
语法
{ $lookup:{ from: "关联集合名", localField: "当前集合关联字段", foreignField: "关联集合匹配字段", as: "存放匹配结果的数组字段名" } }查询:查订单,带出对应用户信息
db.orders.aggregate([ // 先过滤订单(可选,提前缩小数据量提升性能) {$match: {amount: {$gte: 50}}}, // 关联用户表 { $lookup:{ from:"users", localField:"userId", foreignField:"_id", as:"userList" // 匹配到的用户会存在数组里,无匹配则为空数组 [] } } ])返回结果结构:
{ "_id":1001, "userId":ObjectId("600001"), "amount":99, "userList":[{_id:ObjectId("600001"), name:"张三", phone:"13800011111"}] }, { "_id":1003, "userId":ObjectId("600003"), "amount":59, "userList":[] // 无匹配用户,数组空 }模拟 INNER JOIN(只保留有匹配的数据)
用$unwind拆解数组,空数组会直接过滤掉:
db.orders.aggregate([ {$lookup:{from:"users",localField:"userId",foreignField:"_id",as:"userList"}}, {$unwind:"$userList"}, // 拆解数组,空数组文档直接丢弃 {$project:{ // 自定义返回字段 orderId:"$_id", amount:1, userName:"$userList.name", userPhone:"$userList.phone" }} ])2)高级带管道 $lookup(复杂条件关联,3.6 + 支持)
基础$lookup只能等值匹配;带pipeline可以做不等值、多条件、过滤、投影。 场景:关联用户时只返回姓名,并且过滤手机号不为空的用户
db.orders.aggregate([ { $lookup:{ from:"users", let:{uid:"$userId"}, // 定义变量,当前文档字段 pipeline:[ {$match:{ $expr:{ $and:[ {$eq:["$_id","$$uid"]}, // 关联匹配 {$ne:["$phone",null]} // 附加过滤条件 ] } }}, {$project:{name:1,_id:0}} // 只返回name字段 ], as:"userInfo" } } ])三、多集合连续关联(三表联查)
订单 → 用户 → 商品
db.orders.aggregate([ // 关联用户 {$lookup:{from:"users",localField:"userId",foreignField:"_id",as:"user"}}, {$unwind:"$user"}, // 再关联商品 {$lookup:{from:"goods",localField:"goodsId",foreignField:"_id",as:"goods"}}, {$unwind:"$goods"} ])四、$lookup 和 MySQL JOIN 关键区别
| 对比项 | MySQL JOIN | MongoDB $lookup |
|---|---|---|
| 连接类型 | INNER/LEFT/RIGHT/FULL | 仅原生左外连接,INNER 需手动 $unwind |
| 返回结构 | 行扁平化 | 匹配数据封装成数组,需 $unwind 摊平 |
| 关联条件 | 等值、不等值、多条件直接写 | 基础版仅等值;复杂条件必须 pipeline |
| 跨库 | 支持 | 不支持,只能同数据库集合关联 |
| 性能 | JOIN 有优化器、索引友好 | 大量 $lookup 性能差,分片环境开销更高 |
| 外键约束 | 支持物理外键 | 无外键,关联靠业务 ID 手动维护 |
五、MongoDB 两种关联模型(最佳实践)
方案 1:内嵌文档(优先推荐,完全不用 $lookup)
适合一对一、少量关联数据,查询一次拿到全部信息,无联表开销
// 订单直接内嵌用户简化信息 { _id:1001, amount:99, user:{name:"张三", phone:"13800011111"} }方案 2:引用 + $lookup(适合一对多、数据量大)
用户独立集合,订单存 userId,查询时用$lookup关联; 缺点:聚合查询变慢,分页、过滤逻辑复杂。
六、使用踩坑与性能优化
- 关联字段必须建索引:
foreignField(如 users._id 自带索引,自定义字段要手动创建) - 先
$match再$lookup:提前过滤数据,减少关联扫描行数 - 大集合避免多层连续
$lookup,会严重拖慢查询 - 一对一场景用
$unwind摊平数组,简化返回结构 - 不要用
$lookup做高频分页查询,优先内嵌设计
七、一句话总结
MongoDB 通过聚合$lookup实现集合关联,等价左连接,但性能弱于 MySQL JOIN;业务设计优先内嵌文档减少联表,实在需要拆分集合再使用引用 +$lookup。