主要集合是零售商,其中包含商店阵列。每个商店都包含一系列优惠(您可以在这家商店购买)。这个提供的数组有一系列的大小。 (见下面的例子)
现在,我尝试查找所有商品,尺寸为L
。
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
{
"_id" : ObjectId("56f277b5279871c20b8b4783"),
"offers" : [
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size": [
"XS",
"S",
"M"
]
},
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size": [
"S",
"L",
"XL"
]
}
]
}
}
我已尝试此查询:db.getCollection('retailers').find({'stores.offers.size': 'L'})
我希望有类似的输出:
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
{
"_id" : ObjectId("56f277b5279871c20b8b4783"),
"offers" : [
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size": [
"S",
"L",
"XL"
]
}
]
}
}
但我的查询输出还包含size
XS,X和M的非匹配商品。
我如何强制MongoDB只返回与我的查询匹配的商品?
问候和感谢。
答案 0 :(得分:98)
所以你实际上选择了"文件"就像它应该的那样。但是你要找的是过滤数组"包含,以便返回的元素只匹配查询的条件。
真正的答案当然是除非你真的通过过滤掉这些细节来节省大量带宽,否则你甚至不应该尝试,或者至少超过第一次位置匹配。
MongoDB有一个positional $
operator,它将从查询条件返回匹配索引处的数组元素。但是,这只会返回"第一个" "外部"的匹配指数大多数数组元素。
db.getCollection('retailers').find(
{ 'stores.offers.size': 'L'},
{ 'stores.$': 1 }
)
在这种情况下,它仅表示"stores"
数组位置。所以,如果有多个"商店"条目,然后只有"一个"包含匹配条件的元素将被返回。 但是,这对"offers"
的内部数组没有任何作用,因此每个"提供"在匹配的"stores"
数组中仍然会返回。
MongoDB无法过滤"过滤"这在标准查询中,因此以下不起作用:
db.getCollection('retailers').find(
{ 'stores.offers.size': 'L'},
{ 'stores.$.offers.$': 1 }
)
MongoDB实际上必须使用聚合框架来实现此级别的操作。但分析应该告诉你为什么你可能"不应该这样做,而只是在代码中过滤数组。
按照如何在每个版本中实现此目的。
首先使用 $filter
操作 MongoDB 3.2.x :
db.getCollection('retailers').aggregate([
{ "$match": { "stores.offers.size": "L" } },
{ "$project": {
"stores": {
"$filter": {
"input": {
"$map": {
"input": "$stores",
"as": "store",
"in": {
"_id": "$$store._id",
"offers": {
"$filter": {
"input": "$$store.offers",
"as": "offer",
"cond": {
"$setIsSubset": [ ["L"], "$$offer.size" ]
}
}
}
}
}
},
"as": "store",
"cond": { "$ne": [ "$$store.offers", [] ]}
}
}
}}
])
然后使用 $map
和 $setDifference
MongoDB 2.6.x 及更高版本
db.getCollection('retailers').aggregate([
{ "$match": { "stores.offers.size": "L" } },
{ "$project": {
"stores": {
"$setDifference": [
{ "$map": {
"input": {
"$map": {
"input": "$stores",
"as": "store",
"in": {
"_id": "$$store._id",
"offers": {
"$setDifference": [
{ "$map": {
"input": "$$store.offers",
"as": "offer",
"in": {
"$cond": {
"if": { "$setIsSubset": [ ["L"], "$$offer.size" ] },
"then": "$$offer",
"else": false
}
}
}},
[false]
]
}
}
}
},
"as": "store",
"in": {
"$cond": {
"if": { "$ne": [ "$$store.offers", [] ] },
"then": "$$store",
"else": false
}
}
}},
[false]
]
}
}}
])
最后在 MongoDB 2.2.x 之上的任何版本中都引入了聚合框架。
db.getCollection('retailers').aggregate([
{ "$match": { "stores.offers.size": "L" } },
{ "$unwind": "$stores" },
{ "$unwind": "$stores.offers" },
{ "$match": { "stores.offers.size": "L" } },
{ "$group": {
"_id": {
"_id": "$_id",
"storeId": "$stores._id",
},
"offers": { "$push": "$stores.offers" }
}},
{ "$group": {
"_id": "$_id._id",
"stores": {
"$push": {
"_id": "$_id.storeId",
"offers": "$offers"
}
}
}}
])
让我们分解一下解释。
所以一般来说,$filter
是前往这里的方式,因为它的设计目的在于此。由于阵列有多个级别,因此您需要在每个级别应用此级别。首先,您要深入"offers"
"stores"
内的每个$filter
考试和"size"
内容。
这里的简单比较是" ["L"]
数组是否包含我正在寻找的元素" 。在这个逻辑上下文中,要做的就是使用$setIsSubset
操作将true
的数组(" set")与目标数组进行比较。如果该条件为"offers"
(它包含" L"),则保留$filter
的数组元素并在结果中返回。
在更高级别$filter
中,您将查看前一个[]
的结果是否为"offers"
返回了一个空数组$filter
。如果它不为空,则返回该元素,否则将其删除。
这与现代流程非常相似,只是因为此版本中没有false
,您可以使用$map
检查每个元素,然后使用$setDifference
过滤掉任何元素已作为$map
返回。
所以$cond
将返回整个数组,但false
操作只决定是返回元素还是返回$setDifference
值。在[false]
与单个元素的比较中,&#34; set&#34; <{1}}将删除返回数组中的所有false
个元素。
在所有其他方面,逻辑与上述相同。
因此,在MongoDB 2.6下,使用数组的唯一工具是$unwind
,仅此目的,你应该不使用聚合框架&#34;只是&#34;为了这个目的。
这个过程确实很简单,简单来说就是&#34;分开&#34;每个阵列,过滤掉你不需要的东西然后把它重新组合在一起。主要关心在&#34;两个&#34; $group
阶段,&#34;第一&#34;重新构建内部数组,然后重新构建外部数组。所有级别都有不同的_id
值,因此只需要在每个级别的分组中包含这些值。
但问题是$unwind
非常昂贵。虽然它确实有目的,但它的主要使用意图是不对每个文档进行这种过滤。事实上,在现代版本中,它的唯一用途应该是当数组的一个元素需要成为&#34;分组键的一部分时。本身。
因此,在这样的阵列的多个级别上获取匹配并不是一个简单的过程,事实上,如果实施不当,它可能非常昂贵。
只有两个现代列表才能用于此目的,因为他们使用的是单个&#34;管道阶段除了&#34;查询&#34; $match
为了进行&#34;过滤&#34;。由此产生的效果比.find()
的标准形式更具开销。
一般来说,这些列表仍然具有一定的复杂性,实际上除非你真正大幅度减少这种过滤返回的内容,使得服务器和客户端之间使用的带宽得到显着改善,你最好过滤初始查询和基本预测的结果。
db.getCollection('retailers').find(
{ 'stores.offers.size': 'L'},
{ 'stores.$': 1 }
).forEach(function(doc) {
// Technically this is only "one" store. So omit the projection
// if you wanted more than "one" match
doc.stores = doc.stores.filter(function(store) {
store.offers = store.offers.filter(function(offer) {
return offer.size.indexOf("L") != -1;
});
return store.offers.length != 0;
});
printjson(doc);
})
所以使用返回的对象&#34; post&#34;查询处理远不如使用聚合管道来做到这一点。正如所陈述的唯一&#34;真实&#34;差异在于你要丢弃&#34;服务器上的其他元素&#34;而不是删除它们&#34;每个文件&#34;收到时,可以节省一点带宽。
但除非您在仅 $match
和$project
的现代版本中执行此操作,否则&#34;成本&#34;在服务器上处理将大大超过&#34;增益&#34;首先通过剥离不匹配的元素来减少网络开销。
在所有情况下,您都会得到相同的结果:
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
{
"_id" : ObjectId("56f277b5279871c20b8b4783"),
"offers" : [
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size" : [
"S",
"L",
"XL"
]
}
]
}
]
}
答案 1 :(得分:9)
当你的数组被嵌入时我们不能使用$ elemMatch,而是你可以使用聚合框架来获得你的结果:
db.retailers.aggregate([
{$match:{"stores.offers.size": 'L'}}, //just precondition can be skipped
{$unwind:"$stores"},
{$unwind:"$stores.offers"},
{$match:{"stores.offers.size": 'L'}},
{$group:{
_id:{id:"$_id", "storesId":"$stores._id"},
"offers":{$push:"$stores.offers"}
}},
{$group:{
_id:"$_id.id",
stores:{$push:{_id:"$_id.storesId","offers":"$offers"}}
}}
]).pretty()
此查询的作用是展开数组(两次),然后匹配大小,然后将文档重新整形为上一个表单。您可以删除$ group步骤并查看其打印方式。 玩得开心!