如何使用MongoDB过滤子文档中的数组

136
我有一个子文档中的数组,像这样。
{
    "_id" : ObjectId("512e28984815cbfcb21646a7"),
    "list" : [
        {
            "a" : 1
        },
        {
            "a" : 2
        },
        {
            "a" : 3
        },
        {
            "a" : 4
        },
        {
            "a" : 5
        }
    ]
}

我能过滤子文档,只展示大于3的吗?
我的期望结果如下:
{
    "_id" : ObjectId("512e28984815cbfcb21646a7"),
    "list" : [
        {
            "a" : 4
        },
        {
            "a" : 5
        }
    ]
}

我尝试使用$elemMatch,但返回数组中第一个匹配的元素。
我的查询:
db.test.find( { _id" : ObjectId("512e28984815cbfcb21646a7") }, { 
    list: { 
        $elemMatch: 
            { a: { $gt:3 } 
            } 
    } 
} )

结果返回数组中的一个元素。
{ "_id" : ObjectId("512e28984815cbfcb21646a7"), "list" : [ { "a" : 4 } ] }

我尝试使用$match和聚合函数,但不起作用。
db.test.aggregate({$match:{_id:ObjectId("512e28984815cbfcb21646a7"), 'list.a':{$gte:5}  }})

它会返回数组中的所有元素。
{
    "_id" : ObjectId("512e28984815cbfcb21646a7"),
    "list" : [
        {
            "a" : 1
        },
        {
            "a" : 2
        },
        {
            "a" : 3
        },
        {
            "a" : 4
        },
        {
            "a" : 5
        }
    ]
}

我可以筛选数组中的元素,以获得预期的结果吗?
3个回答

214
使用aggregate是正确的方法,但在应用$match之前,您需要对list数组进行$unwind操作,以便可以过滤单个元素,然后使用$group将其重新组合起来。
db.test.aggregate([
    { $match: {_id: ObjectId("512e28984815cbfcb21646a7")}},
    { $unwind: '$list'},
    { $match: {'list.a': {$gt: 3}}},
    { $group: {_id: '$_id', list: {$push: '$list.a'}}}
])

输出:

{
  "result": [
    {
      "_id": ObjectId("512e28984815cbfcb21646a7"),
      "list": [
        4,
        5
      ]
    }
  ],
  "ok": 1
}

MongoDB 3.2 更新

从3.2版本开始,您可以使用新的$filter聚合操作符,在$project期间只包含您想要的list元素,以更高效地完成此操作。

db.test.aggregate([
    { $match: {_id: ObjectId("512e28984815cbfcb21646a7")}},
    { $project: {
        list: {$filter: {
            input: '$list',
            as: 'item',
            cond: {$gt: ['$$item.a', 3]}
        }}
    }}
])

$and: 获取0-5之间的数据:

cond: { 
    $and: [
        { $gt: [ "$$item.a", 0 ] },
        { $lt: [ "$$item.a", 5 ] }
]}

聚合操作是修改文档还是只执行选择? - Cherif
3
aggregate 操作不会影响文档本身。 - JohnnyHK
1
如果您想将该查询的结果发布给客户端,您会如何处理? - samson
1
这个解决方案是否利用了数组元素内部的索引(如果数组元素本身是一个文档)?假设在数组元素中的某个路径上有一个索引。 - kartik
1
@JohnnyHK 最后一个条件(闭括号)有个拼写错误。 - Cache
显示剩余2条评论

43

如果需要多个匹配的子文档,则上述解决方案效果最佳。 $elemMatch非常有用,如果需要将单个匹配的子文档作为输出。

db.test.find({list: {$elemMatch: {a: 1}}}, {'list.$': 1})

结果:

{
  "_id": ObjectId("..."),
  "list": [{a: 1}]
}

1
这对我来说是最好的解决方案。但第二个参数仅适用于原始类型。由于数组中有一个对象,我的解决方案是:ShoppingCartModel.findOne({"_id": ctx.params.idCart, "active" : true, products: {$elemMatch : {"products.active" : true}}})(我只过滤了 active: true 的购物车和产品) - Ulises Layera

26

使用$filter聚合

根据指定条件选择要返回的数组子集。返回一个只有与条件匹配的元素的数组,返回的元素按原始顺序排列。

db.test.aggregate([
    {$match: {"list.a": {$gt:3}}}, // <-- match only the document which have a matching element
    {$project: {
        list: {$filter: {
            input: "$list",
            as: "list",
            cond: {$gt: ["$$list.a", 3]} //<-- filter sub-array based on condition
        }}
    }}
]);

7
请注意,如果您使用的是Atlas,则免费版无法使用$filter - Seth Falco
@SethFalco,在2022年10月似乎不再是这种情况了。 - cprcrack

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