仅检索MongoDB集合中对象数组中的查询元素


377

假设您的收藏夹中包含以下文档:

{  
   "_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"}
  ] 
}

但是,我只想使用包含以下内容的数组来获取文档(文档1)color=red

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

我怎样才能做到这一点?

Answers:


416

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

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

返回值:

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

在2.2中,您也可以使用进行操作$ projection operator,其中$投影对象字段名称中的表示查询中该字段的第一个匹配数组元素的索引。以下返回与上面相同的结果:

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"
            }
        ]
    }
]

15
任何解决方案,如果我希望它返回匹配它的每个元素,而不仅仅是第一个?
史蒂夫吴

恐怕我正在使用Mongo 3.0.X:-(
charliebrownie

@charliebrownie然后使用使用的其他答案之一aggregate
JohnnyHK '16

该查询仅返回数组“ shapes”,而不会返回其他字段。有人也知道如何返回其他字段吗?
Mark Thien

1
这也适用:db.test.find({}, {shapes: {$elemMatch: {color: "red"}}});
Paul

97

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
}

6
@JohnnyHK:这$elemMatch是另一种选择。我实际上是通过Google Group问题来到这里的,其中$ elemMatch不起作用,因为它仅返回每个文档的第一个匹配项。
Stennie 2012年

1
谢谢,我没有意识到这个限制,所以很高兴知道。很抱歉删除您要回复的评论,我决定改为发布另一个答案,不想让人们感到困惑。
JohnnyHK'9

3
@JohnnyHK:不用担心,这个问题现在有三个有用的答案;-)
Stennie 2012年

对于其他搜索者,除此以外,我还尝试添加{ $project : { shapes : 1 } }-似乎很有效,如果附带的文档很大并且您只想查看shapes键值,这将很有帮助。
user1063287 2014年

2
@calmbird我将示例更新为包括初始$ match阶段。如果您对更有效的功能建议感兴趣,我将监视/ 支持SERVER-6612:支持在MongoDB问题跟踪器中的$ elemMatch投影说明符之类的投影中投影多个数组值
Stennie

30

另一种有趣的方式是使用$ 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 “基于存储在文档本身中的信息来限制文档的内容”。因此它将仅在文档内部运行。它基本上从上到下扫描您的文档,并检查它是否与您的if条件相匹配$cond,如果匹配,则将保留content($$DESCEND)或remove($$PRUNE)。

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

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


1
完美的答案。正如您提到的,$ unwind将消耗大量RAM。因此,相比较而言会更好。
manojpt 2015年

我有个疑问。在示例中,“形状”是一个数组。“ $ redact”会扫描“ shapes”数组中的所有对象吗?就性能而言,这将如何呢?
manojpt 2015年

不是全部,而是您第一次比赛的结果。这就是为什么您将其$match作为第一个汇总阶段的原因
anvarik

okkk ..如果在“颜色”字段上创建了索引,即使它会扫描“形状”数组中的所有对象?这可能是匹配数组中多个对象的有效方法???
manojpt 2015年

2
辉煌!我不明白$ eq在这里如何工作。我最初将其保留,这对我不起作用。不知何故,它在形状数组中查找要找到的匹配项,但查询从未指定要查找的数组。$ eq会在两个数组中查找匹配项吗?$ redact是否只是在文档中寻找与'if'条件匹配的内容?
Onosa 2015年

30

注意:这个答案提供了一个解决方案,这是有关在那个时候,引入了MongoDB的2.2及更高版本的新功能之前。如果您使用的是MongoDB的最新版本,请参阅其他答案。

字段选择器参数仅限于完整属性。它不能用于选择数组的一部分,而只能选择整个数组。我尝试使用$位置运算符,但这没有用。

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

如果确实需要直接从MongoDB输出正确的输出,则可以使用map-reduce过滤形状。

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()

24

最好使用匹配数组元素进行查询,$slice这有助于返回数组中的重要对象。

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

$slice当您知道元素的索引时,此方法将很有帮助,但有时您想要匹配条件的任何数组元素。您可以使用$运算符返回匹配的元素。


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

输出值

{

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

12

在mongodb中查找的语法是

    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
这对我有用。但是,似乎"shapes.color":"red"不需要查询参数(find方法的第一个参数)。您可以将其替换为{}并获得相同的结果。
埃里克·奥尔森

2
@ErikOlson在上述情况下,您的建议是正确的,在这里我们需要找到所有红色的文档并将投影仅应用于它们。但是,假设有人需要找出所有具有蓝色的文档,但它只应返回该形状数组中具有红色的那些元素。在这种情况下,上述查询也可以由其他人引用
。– Vicky

这似乎是最简单的方法,但我无法使其正常工作。它仅返回第一个匹配的子文档。
2015年

8

感谢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"
    }
]}

7

您只需要运行查询

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

该查询的输出是

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

如您所料,它将提供与color:'red'匹配的数组中的确切字段。


3

与$ project一起使用,更明智的做法是将其他明智的匹配元素与文档中的其他元素合并在一起。

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

您能描述一下这是通过输入和输出集完成的吗?
亚历山大·米尔斯

2

同样,您可以找到多个

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

这个答案确实是首选4.x的方式: $match以减少的空间,然后$filter让你想要什么,覆盖输入字段(使用输出的$filter现场shapes$project重新拉回shapes。样式注意:最好不要使用字段名称as之所以提出这样的论点,是因为那会在以后与$$shape和引起混淆$shape。我更喜欢zzas领域,因为它确实很突出
Buzz Moschetti

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

1
欢迎使用Stack Overflow!感谢您提供的代码段,它们可能会提供一些有限的即时帮助。通过描述为什么这是一个很好的解决方案的方法,正确的解释将大大提高其长期价值,并且对于将来有其他类似问题的读者来说,它将更加有用。请编辑您的答案以添加一些解释,包括您所做的假设。
sepehr

1

使用聚合函数并$project获取文档中的特定对象字段

db.getCollection('geolocations').aggregate([ { $project : { geolocation : 1} } ])

结果:

{
    "_id" : ObjectId("5e3ee15968879c0d5942464b"),
    "geolocation" : [ 
        {
            "_id" : ObjectId("5e3ee3ee68879c0d5942465e"),
            "latitude" : 12.9718313,
            "longitude" : 77.593551,
            "country" : "India",
            "city" : "Chennai",
            "zipcode" : "560001",
            "streetName" : "Sidney Road",
            "countryCode" : "in",
            "ip" : "116.75.115.248",
            "date" : ISODate("2020-02-08T16:38:06.584Z")
        }
    ]
}

0

尽管这个问题是在9.6年前提出的,但它对无数人产生了巨大帮助,我就是其中之一。谢谢大家的所有疑问,提示和答案。从这里的答案之一中进行选择。我发现以下方法也可以用于投影父文档中的其他字段。这可能对某人有所帮助。

对于以下文档,需要确定雇员(emp#7839)是否设置了2020年的休假历史记录。休假历史记录是作为上级Employee文档中的嵌入式文档实现的。

db.employees.find( {"leave_history.calendar_year": 2020}, 
    {leave_history: {$elemMatch: {calendar_year: 2020}},empno:true,ename:true}).pretty()


{
        "_id" : ObjectId("5e907ad23997181dde06e8fc"),
        "empno" : 7839,
        "ename" : "KING",
        "mgrno" : 0,
        "hiredate" : "1990-05-09",
        "sal" : 100000,
        "deptno" : {
                "_id" : ObjectId("5e9065f53997181dde06e8f8")
        },
        "username" : "none",
        "password" : "none",
        "is_admin" : "N",
        "is_approver" : "Y",
        "is_manager" : "Y",
        "user_role" : "AP",
        "admin_approval_received" : "Y",
        "active" : "Y",
        "created_date" : "2020-04-10",
        "updated_date" : "2020-04-10",
        "application_usage_log" : [
                {
                        "logged_in_as" : "AP",
                        "log_in_date" : "2020-04-10"
                },
                {
                        "logged_in_as" : "EM",
                        "log_in_date" : ISODate("2020-04-16T07:28:11.959Z")
                }
        ],
        "leave_history" : [
                {
                        "calendar_year" : 2020,
                        "pl_used" : 0,
                        "cl_used" : 0,
                        "sl_used" : 0
                },
                {
                        "calendar_year" : 2021,
                        "pl_used" : 0,
                        "cl_used" : 0,
                        "sl_used" : 0
                }
        ]
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.