$lookup (聚合)
本页内容
定义
$lookup
变更在版本8.0.
在相同数据库的集合中执行左外连接,以过滤来自“连接”集合的文档进行处理。此
$lookup
阶段为每个输入文档添加一个新数组字段。新数组字段包含来自“连接”集合的匹配文档。该$lookup
阶段将这些重塑后的文档传递到下一阶段。从 MongoDB 5.1 开始,您可以使用
$lookup
与分片集合一起使用。要合并来自两个不同集合的元素,请使用
$unionWith
管道阶段。
兼容性
您可以使用 $lookup
在以下环境中部署
MongoDB Atlas:MongoDB 在云中的完全托管服务
MongoDB Enterprise:基于订阅、自行管理的 MongoDB 版本
MongoDB Community:源代码可用的、免费使用并自行管理的 MongoDB 版本
语法
$lookup
阶段具有以下各节中显示的语法变体。
使用单个连接条件进行等值匹配
要执行输入文档中的一个字段与“连接”集合中的字段之间的等值匹配,$lookup
阶段具有以下语法
{ $lookup: { from: <collection to join>, localField: <field from the input documents>, foreignField: <field from the documents of the "from" collection>, as: <output array field> } }
$lookup
读取包含以下字段的文档
字段 | 描述 |
---|---|
指定与连接操作一起执行的数据库中的集合。
从 MongoDB 5.1 开始, | |
指定要添加到输入文档中的新数组字段的名称。该新数组字段包含来自 |
该操作对应于以下伪 SQL 语句
SELECT *, ( SELECT ARRAY_AGG(*) FROM <collection to join> WHERE <foreignField> = <collection.localField> ) AS <output array field> FROM collection;
注意
此页面上的 SQL 语句包含用于与 MongoDB 聚合管道语法进行比较。这些 SQL 语句不可执行。
有关 MongoDB 示例,请参阅这些页面
在连接集合上的连接条件和子查询
MongoDB 支持
在连接集合上执行管道。
多个连接条件。
关联和非关联子查询。
在MongoDB中,相关子查询是在管道中的一个$lookup
阶段,它引用了已连接集合中的文档字段。非相关子查询不引用已连接字段。
注意
MongoDB的相关子查询类似于SQL的相关子查询,其中内部查询引用外部查询的值。一个SQL的非相关子查询不引用外部查询的值。
MongoDB 5.0还支持简洁的相关子查询。
要使用两个集合执行相关和非相关子查询,并执行除了单个等值匹配之外的其它连接条件,请使用这个$lookup
语法
{ $lookup: { from: <joined collection>, let: { <var_1>: <expression>, …, <var_n>: <expression> }, pipeline: [ <pipeline to run on joined collection> ], as: <output array field> } }
$lookup
阶段接受包含这些字段的文档
字段 | 描述 |
---|---|
指定进行连接操作的同一数据库中的集合。
从MongoDB 5.1开始, | |
指定在连接集合上运行的 该 该 要在管道阶段中引用变量,请使用 | |
指定要添加到连接文档的新数组字段的名称。该新数组字段包含来自连接集合的匹配文档。如果指定的名称已存在于连接文档中,则覆盖现有字段。 |
该操作对应于以下伪 SQL 语句
SELECT *, <output array field> FROM collection WHERE <output array field> IN ( SELECT <documents as determined from the pipeline> FROM <collection to join> WHERE <pipeline> );
请参阅以下示例
使用简洁语法的相关子查询
新功能在版本5.0.
从MongoDB 5.0版本开始,您可以使用简洁语法来使用相关子查询。相关子查询引用了与在本地集合上运行的aggregate()
方法相关的“外部”集合的文档字段。
以下新的简洁语法去除了在$expr
运算符内部对外部和本地字段进行相等匹配的要求
{ $lookup: { from: <foreign collection>, localField: <field from local collection's documents>, foreignField: <field from foreign collection's documents>, let: { <var_1>: <expression>, …, <var_n>: <expression> }, pipeline: [ <pipeline to run> ], as: <output array field> } }
$lookup
运算符接受包含以下字段的文档
字段 | 描述 |
---|---|
指定要连接到本地集合的同一数据库中的外部集合。
从MongoDB 5.1开始, | |
指定本地文档的 如果本地文档不包含 | |
指定外部文档的 如果外部文档不包含 | |
指定在外国集合上运行的 管道不能包含 管道不能直接访问文档字段。相反,使用 let 选项为文档字段定义变量,然后在管道阶段中引用这些变量。 要在管道阶段中引用变量,请使用 | |
指定要添加到外部文档的新数组字段的名称。新数组字段包含来自外部集合的匹配文档。如果指定的名称已存在于外部文档中,则覆盖现有字段。 |
该操作对应于以下伪 SQL 语句
SELECT *, <output array field> FROM localCollection WHERE <output array field> IN ( SELECT <documents as determined from the pipeline> FROM <foreignCollection> WHERE <foreignCollection.foreignField> = <localCollection.localField> AND <pipeline match condition> );
查看此示例
行为
视图和排序
如果执行涉及多个视图的聚合操作,例如使用 $lookup
或 $graphLookup
,则这些视图必须具有相同的 排序规则。
限制
您不能在 $out
或 $merge
阶段中包含 $lookup
阶段。也就是说,当指定 连接集合的管道 时,您不能在 pipeline
字段中包含这些阶段。
{ $lookup: { from: <collection to join>, let: { <var_1>: <expression>, …, <var_n>: <expression> }, pipeline: [ <pipeline to execute on the joined collection> ], // Cannot include $out or $merge as: <output array field> } }
Atlas Search 支持
从 MongoDB 6.0 开始,您可以在 Atlas Search $search
或 $searchMeta
管道中指定 $lookup
,以在 Atlas 集群上搜索集合。$search
或 $searchMeta
阶段必须是 $lookup
管道中的第一个阶段。
例如,当您在连接集合上使用连接条件和子查询或运行使用简洁语法的相关子查询时,您可以在如下所示的管道中指定$search
或$searchMeta
。
[{ "$lookup": { "from": <joined collection>, localField: <field from the input documents>, foreignField: <field from the documents of the "from" collection>, "as": <output array field>, "pipeline": [{ "$search": { "<operator>": { <operator-specification> } }, ... }] } }]
[{ "$lookup": { "from": <joined collection>, localField: <field from the input documents>, foreignField: <field from the documents of the "from" collection>, "as": <output array field>, "pipeline": [{ "$searchMeta": { "<collector>": { <collector-specification> } }, ... }] } }]
要查看使用$search
的$lookup
示例,请参阅Atlas Search教程使用$lookup运行Atlas Search $search查询。
分片集合
从MongoDB 5.1开始,您可以在$lookup
阶段的from
参数中指定分片集合。
从MongoDB 8.0开始,您可以在针对分片集合时使用事务在$lookup
阶段。
基于槽位的查询执行引擎
从版本6.0开始,MongoDB可以使用基于槽位的执行查询引擎来执行$lookup
阶段,前提是管道中的所有先前的阶段也可以由该基于槽位的执行引擎执行并且以下条件均不成立
$lookup
操作在连接集合上执行管道。要查看此类操作的示例,请参阅连接集合上的连接条件和子查询。《$lookup》操作的
localField
或foreignField
指定数字组件。例如:{ localField: "restaurant.0.review" }
。管道中任何《$lookup》操作的
from
字段指定了一个视图或分片集合。
有关更多信息,请参阅$lookup
优化。
性能考虑因素
《$lookup》的性能取决于执行的操作类型。请参阅以下表格了解不同《$lookup》操作的性能考虑因素。
《$lookup》操作 | 性能考虑 |
---|---|
| |
| |
|
重要
在查询中过度使用 $lookup
可能会降低性能。为了避免多个 $lookup
阶段,考虑使用 嵌入式数据模型 来优化查询性能。
示例
使用 $lookup
进行单等值连接
创建一个包含以下文档的集合 orders
db.orders.insertMany( [ { "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 }, { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 }, { "_id" : 3 } ] )
创建另一个包含以下文档的集合 inventory
db.inventory.insertMany( [ { "_id" : 1, "sku" : "almonds", "description": "product 1", "instock" : 120 }, { "_id" : 2, "sku" : "bread", "description": "product 2", "instock" : 80 }, { "_id" : 3, "sku" : "cashews", "description": "product 3", "instock" : 60 }, { "_id" : 4, "sku" : "pecans", "description": "product 4", "instock" : 70 }, { "_id" : 5, "sku": null, "description": "Incomplete" }, { "_id" : 6 } ] )
以下对 orders
集合的聚合操作使用 orders
集合中的 item
字段和 inventory
集合中的 sku
字段将来自 orders
的文档与来自 inventory
的文档进行连接
db.orders.aggregate( [ { $lookup: { from: "inventory", localField: "item", foreignField: "sku", as: "inventory_docs" } } ] )
操作返回以下文档
{ "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2, "inventory_docs" : [ { "_id" : 1, "sku" : "almonds", "description" : "product 1", "instock" : 120 } ] } { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1, "inventory_docs" : [ { "_id" : 4, "sku" : "pecans", "description" : "product 4", "instock" : 70 } ] } { "_id" : 3, "inventory_docs" : [ { "_id" : 5, "sku" : null, "description" : "Incomplete" }, { "_id" : 6 } ] }
该操作对应于以下伪 SQL 语句
SELECT *, inventory_docs FROM orders WHERE inventory_docs IN ( SELECT * FROM inventory WHERE sku = orders.item );
有关更多信息,请参阅 等值匹配性能考虑因素。
使用带有数组的 $lookup
如果localField
是一个数组,您可以在不进行$unwind
阶段的情况下,将数组元素与标量foreignField
进行匹配。
例如,创建一个包含以下文档的示例集合classes
db.classes.insertMany( [ { _id: 1, title: "Reading is ...", enrollmentlist: [ "giraffe2", "pandabear", "artie" ], days: ["M", "W", "F"] }, { _id: 2, title: "But Writing ...", enrollmentlist: [ "giraffe1", "artie" ], days: ["T", "F"] } ] )
创建另一个包含以下文档的集合members
db.members.insertMany( [ { _id: 1, name: "artie", joined: new Date("2016-05-01"), status: "A" }, { _id: 2, name: "giraffe", joined: new Date("2017-05-01"), status: "D" }, { _id: 3, name: "giraffe1", joined: new Date("2017-10-01"), status: "A" }, { _id: 4, name: "panda", joined: new Date("2018-10-11"), status: "A" }, { _id: 5, name: "pandabear", joined: new Date("2018-12-01"), status: "A" }, { _id: 6, name: "giraffe2", joined: new Date("2018-12-01"), status: "D" } ] )
以下聚合操作通过enrollmentlist
字段与name
字段匹配,将classes
集合中的文档与members
集合中的文档进行连接
db.classes.aggregate( [ { $lookup: { from: "members", localField: "enrollmentlist", foreignField: "name", as: "enrollee_info" } } ] )
操作返回以下结果
{ "_id" : 1, "title" : "Reading is ...", "enrollmentlist" : [ "giraffe2", "pandabear", "artie" ], "days" : [ "M", "W", "F" ], "enrollee_info" : [ { "_id" : 1, "name" : "artie", "joined" : ISODate("2016-05-01T00:00:00Z"), "status" : "A" }, { "_id" : 5, "name" : "pandabear", "joined" : ISODate("2018-12-01T00:00:00Z"), "status" : "A" }, { "_id" : 6, "name" : "giraffe2", "joined" : ISODate("2018-12-01T00:00:00Z"), "status" : "D" } ] } { "_id" : 2, "title" : "But Writing ...", "enrollmentlist" : [ "giraffe1", "artie" ], "days" : [ "T", "F" ], "enrollee_info" : [ { "_id" : 1, "name" : "artie", "joined" : ISODate("2016-05-01T00:00:00Z"), "status" : "A" }, { "_id" : 3, "name" : "giraffe1", "joined" : ISODate("2017-10-01T00:00:00Z"), "status" : "A" } ] }
使用$lookup
和$mergeObjects
结合使用
$mergeObjects
运算符将多个文档合并为单个文档。
创建一个包含以下文档的集合 orders
db.orders.insertMany( [ { "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 }, { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 } ] )
创建另一个包含以下文档的集合items
db.items.insertMany( [ { "_id" : 1, "item" : "almonds", description: "almond clusters", "instock" : 120 }, { "_id" : 2, "item" : "bread", description: "raisin and nut bread", "instock" : 80 }, { "_id" : 3, "item" : "pecans", description: "candied pecans", "instock" : 60 } ] )
以下操作首先使用$lookup
阶段通过item
字段将两个集合连接起来,然后在$replaceRoot
中使用$mergeObjects
来合并来自items
和orders
的连接文档
db.orders.aggregate( [ { $lookup: { from: "items", localField: "item", // field in the orders collection foreignField: "item", // field in the items collection as: "fromItems" } }, { $replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ "$fromItems", 0 ] }, "$$ROOT" ] } } }, { $project: { fromItems: 0 } } ] )
操作返回以下文档
{ _id: 1, item: 'almonds', description: 'almond clusters', instock: 120, price: 12, quantity: 2 }, { _id: 2, item: 'pecans', description: 'candied pecans', instock: 60, price: 20, quantity: 1 }
使用多个连接条件和关联子查询
管道可以在连接的集合上执行,并包含多个连接条件。$expr
运算符允许更复杂的连接条件,包括合取和非相等匹配。
连接条件可以引用本地集合中的字段,该字段是在执行aggregate()
方法时使用的,并且可以引用连接集合中的字段。这允许两个集合之间进行相关子查询。
MongoDB 5.0 支持简明相关子查询。
创建一个包含以下文档的集合 orders
db.orders.insertMany( [ { "_id" : 1, "item" : "almonds", "price" : 12, "ordered" : 2 }, { "_id" : 2, "item" : "pecans", "price" : 20, "ordered" : 1 }, { "_id" : 3, "item" : "cookies", "price" : 10, "ordered" : 60 } ] )
创建另一个包含以下文档的集合warehouses
db.warehouses.insertMany( [ { "_id" : 1, "stock_item" : "almonds", warehouse: "A", "instock" : 120 }, { "_id" : 2, "stock_item" : "pecans", warehouse: "A", "instock" : 80 }, { "_id" : 3, "stock_item" : "almonds", warehouse: "B", "instock" : 60 }, { "_id" : 4, "stock_item" : "cookies", warehouse: "B", "instock" : 40 }, { "_id" : 5, "stock_item" : "cookies", warehouse: "A", "instock" : 80 } ] )
以下示例
使用相关子查询与在
orders.item
和warehouse.stock_item
字段上进行的连接。确保库存中物品的数量可以满足订单数量。
db.orders.aggregate( [ { $lookup: { from: "warehouses", let: { order_item: "$item", order_qty: "$ordered" }, pipeline: [ { $match: { $expr: { $and: [ { $eq: [ "$stock_item", "$$order_item" ] }, { $gte: [ "$instock", "$$order_qty" ] } ] } } }, { $project: { stock_item: 0, _id: 0 } } ], as: "stockdata" } } ] )
操作返回以下文档
{ _id: 1, item: 'almonds', price: 12, ordered: 2, stockdata: [ { warehouse: 'A', instock: 120 }, { warehouse: 'B', instock: 60 } ] }, { _id: 2, item: 'pecans', price: 20, ordered: 1, stockdata: [ { warehouse: 'A', instock: 80 } ] }, { _id: 3, item: 'cookies', price: 10, ordered: 60, stockdata: [ { warehouse: 'A', instock: 80 } ] }
该操作对应于以下伪 SQL 语句
SELECT *, stockdata FROM orders WHERE stockdata IN ( SELECT warehouse, instock FROM warehouses WHERE stock_item = orders.item AND instock >= orders.ordered );
将比较运算符 $eq
、$lt
、$lte
、$gt
和 $gte
放置在 $expr
运算符中,可以用于在 $lookup
阶段引用的 from
集合上的索引。限制条件
索引只能用于字段和常量之间的比较,因此
let
操作数必须解析为常量。例如,比较
$a
和常量值可以使用索引,但比较$a
和$b
则不能。当
let
操作数解析为空或缺失值时,不会使用索引进行比较。多键索引 不被使用。
例如,如果warehouses
集合上存在索引{ stock_item: 1, instock: 1 }
在
warehouses.stock_item
字段上的等值匹配使用了索引。在
warehouses.instock
字段上的查询范围也使用了复合索引中的索引字段。
使用$lookup
执行无相关子查询
聚合管道中的$lookup
阶段可以在连接的集合上执行管道,这允许执行无相关子查询。无相关子查询不引用连接的文档字段。
注意
创建一个包含以下文档的集合absences
db.absences.insertMany( [ { "_id" : 1, "student" : "Ann Aardvark", sickdays: [ new Date ("2018-05-01"),new Date ("2018-08-23") ] }, { "_id" : 2, "student" : "Zoe Zebra", sickdays: [ new Date ("2018-02-01"),new Date ("2018-05-23") ] }, ] )
创建另一个包含以下文档的集合holidays
db.holidays.insertMany( [ { "_id" : 1, year: 2018, name: "New Years", date: new Date("2018-01-01") }, { "_id" : 2, year: 2018, name: "Pi Day", date: new Date("2018-03-14") }, { "_id" : 3, year: 2018, name: "Ice Cream Day", date: new Date("2018-07-15") }, { "_id" : 4, year: 2017, name: "New Years", date: new Date("2017-01-01") }, { "_id" : 5, year: 2017, name: "Ice Cream Day", date: new Date("2017-07-16") } ] )
以下操作将absences
集合与来自holidays
集合的2018年假期信息进行连接
db.absences.aggregate( [ { $lookup: { from: "holidays", pipeline: [ { $match: { year: 2018 } }, { $project: { _id: 0, date: { name: "$name", date: "$date" } } }, { $replaceRoot: { newRoot: "$date" } } ], as: "holidays" } } ] )
操作返回以下结果
{ _id: 1, student: 'Ann Aardvark', sickdays: [ ISODate("2018-05-01T00:00:00.000Z"), ISODate("2018-08-23T00:00:00.000Z") ], holidays: [ { name: 'New Years', date: ISODate("2018-01-01T00:00:00.000Z") }, { name: 'Pi Day', date: ISODate("2018-03-14T00:00:00.000Z") }, { name: 'Ice Cream Day', date: ISODate("2018-07-15T00:00:00.000Z") } ] }, { _id: 2, student: 'Zoe Zebra', sickdays: [ ISODate("2018-02-01T00:00:00.000Z"), ISODate("2018-05-23T00:00:00.000Z") ], holidays: [ { name: 'New Years', date: ISODate("2018-01-01T00:00:00.000Z") }, { name: 'Pi Day', date: ISODate("2018-03-14T00:00:00.000Z") }, { name: 'Ice Cream Day', date: ISODate("2018-07-15T00:00:00.000Z") } ] }
该操作对应于以下伪 SQL 语句
SELECT *, holidays FROM absences WHERE holidays IN ( SELECT name, date FROM holidays WHERE year = 2018 );
有关更多信息,请参阅无相关子查询性能考虑因素。
使用$lookup
执行简明相关子查询
新功能在版本5.0.
从MongoDB 5.0版本开始,聚合管道中的 $lookup
阶段支持了一种简明的相关子查询语法,这提高了集合之间的连接操作。新的简明语法取消了在 $expr
操作符中进行的 $match
阶段内对外键和本地字段的等值匹配的要求。
创建一个集合 restaurants
db.restaurants.insertMany( [ { _id: 1, name: "American Steak House", food: [ "filet", "sirloin" ], beverages: [ "beer", "wine" ] }, { _id: 2, name: "Honest John Pizza", food: [ "cheese pizza", "pepperoni pizza" ], beverages: [ "soda" ] } ] )
创建另一个集合 orders
,包含食物和可选的饮料订单
db.orders.insertMany( [ { _id: 1, item: "filet", restaurant_name: "American Steak House" }, { _id: 2, item: "cheese pizza", restaurant_name: "Honest John Pizza", drink: "lemonade" }, { _id: 3, item: "cheese pizza", restaurant_name: "Honest John Pizza", drink: "soda" } ] )
以下示例
通过将
orders.restaurant_name
的本地字段与restaurants.name
的外键字段进行匹配来连接orders
和restaurants
集合。匹配操作在执行pipeline
之前进行。在
orders.drink
和使用$$orders_drink
和$beverages
分别访问的restaurants.beverages
字段之间执行一个$in
数组匹配。
db.orders.aggregate( [ { $lookup: { from: "restaurants", localField: "restaurant_name", foreignField: "name", let: { orders_drink: "$drink" }, pipeline: [ { $match: { $expr: { $in: [ "$$orders_drink", "$beverages" ] } } } ], as: "matches" } } ] )
在 orders.drink
和 restaurants.beverages
字段中有一个与 soda
值的匹配。此输出显示了 matches
数组,包含所有匹配的 restaurants
集合的联合字段。
{ "_id" : 1, "item" : "filet", "restaurant_name" : "American Steak House", "matches" : [ ] } { "_id" : 2, "item" : "cheese pizza", "restaurant_name" : "Honest John Pizza", "drink" : "lemonade", "matches" : [ ] } { "_id" : 3, "item" : "cheese pizza", "restaurant_name" : "Honest John Pizza", "drink" : "soda", "matches" : [ { "_id" : 2, "name" : "Honest John Pizza", "food" : [ "cheese pizza", "pepperoni pizza" ], "beverages" : [ "soda" ] } ] }
在简明相关子查询引入之前,您必须在 pipeline
中的 $eq
等值匹配中使用本地字段和连接字段,如 使用多个连接条件和相关子查询。 所示。
此示例使用MongoDB 5.0之前的旧式详细语法,并返回与上一个简明示例相同的输出
db.orders.aggregate( [ { $lookup: { from: "restaurants", let: { orders_restaurant_name: "$restaurant_name", orders_drink: "$drink" }, pipeline: [ { $match: { $expr: { $and: [ { $eq: [ "$$orders_restaurant_name", "$name" ] }, { $in: [ "$$orders_drink", "$beverages" ] } ] } } } ], as: "matches" } } ] )
前面的示例对应于以下伪SQL语句
SELECT *, matches FROM orders WHERE matches IN ( SELECT * FROM restaurants WHERE restaurants.name = orders.restaurant_name AND restaurants.beverages = orders.drink );
有关更多信息,请参阅 相关子查询性能考虑。
子管道中的命名空间
从MongoDB 8.0版本开始,对 $lookup
和 $unionWith
中的子管道中的命名空间进行验证,以确保正确使用 from
和 coll
字段。
对于
$lookup
操作符,如果您使用一个不需要指定集合的子管道,可以省略from
字段。例如,一个$documents
阶段。同样,对于
$unionWith
操作符,可以省略coll
字段。
行为不变
对于以集合阶段开始的
$lookup
操作符,例如一个$match
或$collStats
子管道,您必须包含from
字段并指定集合。同样,对于
$unionWith
操作符,包含coll
字段并指定集合。
以下场景展示了示例。
创建一个名为 cakeFlavors
的集合
db.cakeFlavors.insertMany( [ { _id: 1, flavor: "chocolate" }, { _id: 2, flavor: "strawberry" }, { _id: 3, flavor: "cherry" } ] )
从 MongoDB 8.0 版本开始,以下示例返回错误,因为它包含一个无效的 from
字段
db.cakeFlavors.aggregate( [ { $lookup: { from: "cakeFlavors", pipeline: [ { $documents: [ {} ] } ], as: "test" } } ] )
在 MongoDB 8.0 版本之前的版本中,上述示例可以正常运行。
有关包含有效 from
字段的示例,请参阅 使用 $lookup
执行单等性连接。