在MongoDB集合中,只检索对象数组中查询的元素

481

假设您在我的集合中有以下文档:

{  
   "_id":ObjectId("562e7c594c12942f08fe4192"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"blue"
      },
      {  
         "shape":"circle",
         "color":"red"
      }
   ]
},
{  
   "_id":ObjectId("562e7c594c12942f08fe4193"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"black"
      },
      {  
         "shape":"circle",
         "color":"green"
      }
   ]
}

执行查询:

db.test.find({"shapes.color": "red"}, {"shapes.color": 1})

或者

db.test.find({shapes: {"$elemMatch": {color: "red"}}}, {"shapes.color": 1})
返回匹配的文档(文档1),但始终包含shapes中的所有数组项。
{ "shapes": 
  [
    {"shape": "square", "color": "blue"},
    {"shape": "circle", "color": "red"}
  ] 
}

然而,我想要获取包含color=red的数组中的文档(文档1)

{ "shapes": 
  [
    {"shape": "circle", "color": "red"}
  ] 
}

我该如何做这件事?

20个回答

528

MongoDB 2.2的新$elemMatch投影运算符提供了另一种方式来改变返回的文档,仅包含第一个匹配的shapes元素:

db.test.find(
    {"shapes.color": "red"}, 
    {_id: 0, shapes: {$elemMatch: {color: "red"}}});

返回:

{"shapes" : [{"shape": "circle", "color": "red"}]}

在2.2版本中,您也可以使用$投影操作符来实现此操作,其中投影对象字段名称中的$表示查询结果中第一个匹配数组元素的索引。以下代码将返回与上面相同的结果:
db.test.find({"shapes.color": "red"}, {_id: 0, 'shapes.$': 1});

MongoDB 3.2 更新

从3.2版本开始,您可以使用新的$filter聚合操作符来在投影期间过滤数组,这样就可以包含所有匹配项,而不仅仅是第一个。

db.test.aggregate([
    // Get just the docs that contain a shapes element where color is 'red'
    {$match: {'shapes.color': 'red'}},
    {$project: {
        shapes: {$filter: {
            input: '$shapes',
            as: 'shape',
            cond: {$eq: ['$$shape.color', 'red']}
        }},
        _id: 0
    }}
])

结果:

[ 
    {
        "shapes" : [ 
            {
                "shape" : "circle",
                "color" : "red"
            }
        ]
    }
]

35
如果我希望它返回与之匹配的所有元素而不仅仅是第一个,有什么解决方案吗? - Steve Ng
1
此查询仅返回数组“shapes”,不会返回其他字段。有人知道如何同时返回其他字段吗? - Mark Thien
2
这个也可以运行:db.test.find({}, {shapes: {$elemMatch: {color: "red"}}}); - Paul
@johnnyhk 如何在数组中使用它,假设我必须返回颜色与 ["red", "green", "blue"] 匹配的形状? - iit2011081
1
这是一个错误吗:$$shape.color?$filter的条件中双重$$。 - Augusto Jara
显示剩余7条评论

117

MongoDB 2.2+中的新的聚合框架提供了一种替代Map/Reduce的方法。使用$unwind操作符可以将你的shapes数组拆分成一系列文档,以便进行匹配:

db.test.aggregate(
  // Start with a $match pipeline which can take advantage of an index and limit documents processed
  { $match : {
     "shapes.color": "red"
  }},
  { $unwind : "$shapes" },
  { $match : {
     "shapes.color": "red"
  }}
)

结果为:

{
    "result" : [
        {
            "_id" : ObjectId("504425059b7c9fa7ec92beec"),
            "shapes" : {
                "shape" : "circle",
                "color" : "red"
            }
        }
    ],
    "ok" : 1
}

7
在这种情况下,$elemMatch是另一个选项。我实际上是通过一个Google Group的问题来到这里的,其中$elemMatch无法工作,因为它仅返回每个文档的第一个匹配项。 - Stennie
1
谢谢,我之前不知道这个限制,现在知道了很好。抱歉删除了你回复的评论,我决定发表另一个答案,不想让人们感到困惑。 - JohnnyHK
3
没问题,现在已经有三个有用的回答了,;-) - Stennie
对于其他的搜索者,除了这个之外,我还尝试添加 { $project : { shapes : 1 } } - 这似乎有效,并且如果包含文档很大,你只想查看 shapes 键值时将会很有帮助。 - user1063287
2
@calmbird 我更新了示例,包括一个初始的 $match 阶段。如果您对更高效的功能建议感兴趣,我建议在 MongoDB 问题跟踪器中关注/点赞 SERVER-6612:支持在投影中投影多个数组值,如 $elemMatch 投影说明符 - Stennie
显示剩余3条评论

35

注意:这个答案提供的解决方案仅适用于MongoDB 2.2及以上版本之前。如果你使用的是更高版本的MongoDB,请查看其他答案。

字段选择器参数仅限于完整属性,无法用于选择数组的部分内容,只能选择整个数组。我尝试使用$ positional operator,但是它没有起作用。

最简单的方法是在客户端中过滤形状。

如果你确实需要直接从MongoDB获得正确的输出,可以使用映射-减少来过滤形状。

function map() {
  filteredShapes = [];

  this.shapes.forEach(function (s) {
    if (s.color === "red") {
      filteredShapes.push(s);
    }
  });

  emit(this._id, { shapes: filteredShapes });
}

function reduce(key, values) {
  return values[0];
}

res = db.test.mapReduce(map, reduce, { query: { "shapes.color": "red" } })

db[res.result].find()

35

另一个有趣的方法是使用$redact,它是MongoDB 2.6的新聚合特性之一。如果您正在使用2.6,您不需要$unwind,因为这可能会在处理大型数组时导致性能问题。

db.test.aggregate([
    { $match: { 
         shapes: { $elemMatch: {color: "red"} } 
    }},
    { $redact : {
         $cond: {
             if: { $or : [{ $eq: ["$color","red"] }, { $not : "$color" }]},
             then: "$$DESCEND",
             else: "$$PRUNE"
         }
    }}]);

$redact "根据文档本身存储的信息限制文档内容"。因此,它只能在文档内部运行。它基本上从头到尾扫描您的文档,并检查它是否与$cond中的if条件匹配。如果匹配,则会保留内容($$DESCEND)或删除内容($$PRUNE)。

在上面的示例中,首先$match返回整个shapes数组,然后$redact将其削减到预期结果。

请注意,{$not:"$color"}是必要的,因为它也会扫描顶级文档,如果$redact在顶级上没有找到color字段,这将返回false,可能会删去整个文档,这是我们不想要的。


1
完美的答案。正如您所提到的,$unwind 将消耗大量 RAM。因此,与之相比,这将更好。 - manojpt
我有一个疑问。在这个例子中,“shapes”是一个数组。 “$redact”会扫描“shapes”数组中的所有对象吗? 这样对性能有好处吗? - manojpt
这是关于编程的内容,请将其从英语翻译成中文。只返回翻译后的文本:不是全部,而是您第一个匹配的结果。这就是为什么将 $match 作为聚合管道的第一个阶段的原因。 - anvarik
如果在“color”字段上创建了索引,它是否仍将扫描“shapes”数组中的所有对象???有哪些匹配数组中多个对象的有效方法? - manojpt
@anvarik 如果我理解正确的话,没有办法使用在数组字段上定义的索引来加速匹配数组元素的检索:无论是$unwind还是$redact都必须扫描整个数组,但$redact使用的RAM要少得多,因为它只在单个文档上工作,而不是为每个数组元素创建一个“文档副本”。我是对的还是我漏掉了什么? - Cec
2
太棒了!我不明白$eq在这里是如何工作的。最初我没有使用它,但这对我没有起作用。不知何故,它会在形状数组中查找匹配项,但查询从未指定要查找哪个数组。比如,如果文档有形状和大小,$eq会在两个数组中查找匹配项吗?$redact只是在文档中查找与“if”条件匹配的任何内容吗? - Onosa

30

使用$slice可以更好地查询匹配的数组元素,有助于返回数组中的重要对象。

db.test.find({"shapes.color" : "blue"}, {"shapes.$" : 1})

$slice对于已知元素的索引值很有帮助,但有时您可能想要任何匹配您条件的数组元素。您可以使用$运算符返回匹配的元素。


它会返回所有包含 shapes.color : blue 的文档还是只返回第一个? - kojot
@kojot 这将只返回一个。 - undefined

22
 db.getCollection('aj').find({"shapes.color":"red"},{"shapes.$":1})

输出

{

   "shapes" : [ 
       {
           "shape" : "circle",
           "color" : "red"
       }
   ]
}

感谢您的查询,但是尽管条件匹配了数组中的多个元素,它仅返回第一个元素。有什么建议吗? - Deepam Gupta

14

在 MongoDB 中使用 find 的语法是:

    db.<collection name>.find(query, projection);

而你编写的第二个查询语句,即

    db.test.find(
    {shapes: {"$elemMatch": {color: "red"}}}, 
    {"shapes.color":1})

在这个查询中,你使用了$elemMatch运算符作为查询部分,然而如果你将此运算符用于投影部分,那么你将会得到所需的结果。你可以按照以下方式编写查询:

     db.users.find(
     {"shapes.color":"red"},
     {_id:0, shapes: {$elemMatch : {color: "red"}}})

这将会给你所期望的结果。


1
这对我有效。然而,在查询参数中(find方法的第一个参数)似乎不需要"shapes.color":"red"。你可以用{}替换它并获得相同的结果。 - Erik Olson
2
@ErikOlson 您在上述情况下的建议是正确的,我们需要找到所有具有红色的文档,并仅对它们应用投影。但是,假设有人需要查找所有具有蓝色的文档,但它应该仅返回那些颜色为红色的形状数组元素。在这种情况下,上面的查询也可以被其他人引用。 - Vicky
这似乎是最简单的方法,但我无法使其工作。它只返回第一个匹配的子文档。 - newman
@MahmoodHussain 这个答案已经有将近7年的历史了,所以可能是版本问题。你能否查看最新的文档呢?我会尝试在最新版本上运行类似的代码,并分享我的发现。你能解释一下你想要实现什么吗? - Vicky
嗨@MahmoodHussain,这看起来不像是本地的mongodb查询。您是否使用Mongoose或其他库来查询mongo?此问题仅适用于本地mongo查询。请提供有关您的实现的更多详细信息,并可能共享对象结构。您可以在此处创建一个新问题,其中包含所有详细信息,我们可以在那里更好地帮助您。 - Vicky
显示剩余2条评论

10

你只需要运行查询

db.test.find(
{"shapes.color": "red"}, 
{shapes: {$elemMatch: {color: "red"}}});

这个查询的输出结果是

{
    "_id" : ObjectId("562e7c594c12942f08fe4192"),
    "shapes" : [ 
        {"shape" : "circle", "color" : "red"}
    ]
}

正如你期望的那样,它将从数组中精确地返回与颜色:“red”匹配的字段。


10

感谢 JohnnyHK

这里我想要补充更多复杂的用法。

// Document 
{ 
"_id" : 1
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 

{ 
"_id" : 2
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 


// The Query   
db.contents.find({
    "_id" : ObjectId(1),
    "shapes.color":"red"
},{
    "_id": 0,
    "shapes" :{
       "$elemMatch":{
           "color" : "red"
       } 
    }
}) 


//And the Result

{"shapes":[
    {
       "shape" : "square",
       "color" : "red"
    }
]}

5

除了$project,否则匹配元素将与文档中的其他元素合并更加适当。

db.test.aggregate(
  { "$unwind" : "$shapes" },
  { "$match" : { "shapes.color": "red" } },
  { 
    "$project": {
      "_id":1,
      "item":1
    }
  }
)

1
你能否描述一下这个程序是如何利用输入和输出集来实现的? - Alexander Mills

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接