如何在MongoDB中查找数组的最后一个元素?

6

我想找出最后一个元素等于某个值的数组文档。

可以通过特定的数组位置访问数组元素:

// i.e. comments[0].by == "Abe"
db.example.find( { "comments.0.by" : "Abe" } )

但是如何使用最后一个项目作为搜索条件呢?例如:

db.example.find( { "comments.last.by" : "Abe" } )

顺便说一下,我正在使用 PHP。


1
你想要查找数组中最后一个元素等于某个值的文档吗? - Sergio Tulentsev
那么你就做不到了,除非你将数组长度存储在一个单独的字段中。但这仍然需要两个查询。如果有人能证明我错了,我会很高兴 :) - Sergio Tulentsev
5个回答

11

我知道这个问题很老了,但在回答一个类似的新问题后,我在谷歌上找到了它。所以我认为这个问题值得同等对待。

你可以通过使用聚合来避免使用$where带来的性能问题:

db.example.aggregate([

    // Use an index, which $where cannot to narrow down
    {$match: { "comments.by": "Abe" }},

    // De-normalize the Array
    {$unwind: "$comments"},

    // The order of the array is maintained, so just look for the $last by _id
    {$group: { _id: "$_id", comments: {$last: "$comment"} }},

    // Match only where that $last comment by `by.Abe`
    {$match: { "comments.by": "Abe" }},

    // Retain the original _id order
    {$sort: { _id: 1 }}

])

考虑到我们已经能够缩小只针对首先有 "Abe" 评论的文档进行测试,因此应该比 $where 更高效。正如警告的那样,$where将测试集合中的每个文档,并且即使存在可用的索引也永远不会使用。

当然,您也可以使用此处描述的技术来保留原始文档,这样一切都像find()一样工作。

这只是让任何人思考的食物。


现代MongoDB版本的更新

现代版本已添加了$redact管道表达式以及$arrayElemAt (后者从3.2版本开始,因此这是最低版本)这两者的组合将允许一个逻辑表达式检查数组的最后一个元素而无需处理$unwind阶段:

db.example.aggregate([
  { "$match": { "comments.by": "Abe" }},
  { "$redact": {
    "$cond": {
      "if": {
        "$eq": [
          { "$arrayElemAt": [ "$comments.by", -1 ] },
          "Abe"
        ]
      },
      "then": "$$KEEP",
      "else": "$$PRUNE"
    }
  }}
])
这里的逻辑是通过比较完成的,其中$arrayElemAt获取了数组-1的最后一个索引,这个索引通过$map转换成了仅包含"by"属性值的数组。这使得单个值可以与所需参数"Abe"进行比较。

甚至更加现代的方法是在MongoDB 3.6及以上版本中使用$expr

db.example.find({
  "comments.by": "Abe",
  "$expr": {
    "$eq": [
      { "$arrayElemAt": [ "$comments.by", -1 ] },
      "Abe"
    ]
  }
})

这将是在数组内匹配最后一个元素的最有效解决方案,预计实际上会在大多数情况下甚至特别是在这里代替使用$where


7

使用这种模式设计,无法一次完成此操作。您可以将长度存储起来并执行两个查询,或者另外在另一个字段中存储最后一条评论:

{
    '_id': 'foo';
    'comments' [
        { 'value': 'comment #1', 'by': 'Ford' },
        { 'value': 'comment #2', 'by': 'Arthur' },
        { 'value': 'comment #3', 'by': 'Zaphod' }
    ],
    'last_comment': {
        'value': 'comment #3', 'by': 'Zaphod'
    }
}

当然,您会复制一些数据,但是至少可以使用$set将此数据与comment$push一起设置。
$comment = array(
    'value' => 'comment #3',
    'by' => 'Zaphod',
);

$collection->update(
    array( '_id' => 'foo' ),
    array(
        '$set' => array( 'last_comment' => $comment ),
        '$push' => array( 'comments' => $comment )
    )
);

现在找到最后一个变得很容易了!

5

您可以使用$where操作符来实现:

db.example.find({ $where: 
    'this.comments.length && this.comments[this.comments.length-1].by === "Abe"' 
})

$where通常会导致较慢的性能。不过,您可以通过在查询中包含"comments.by": "Abe"来帮助改善性能:

db.example.find({
    "comments.by": "Abe",
    $where: 'this.comments.length && this.comments[this.comments.length-1].by === "Abe"' 
})

通过这种方式,$where 只需要针对包含 Abe 评论的文档进行评估,新术语将能够使用 "comments.by" 上的索引。


@fnaquira 我知道这已经过时了,但是在发现这个问题后,我认为它值得一个新的(而且更好的)答案。我已经提供了答案。 - Neil Lunn

3

我只是在做:

db.products.find({'statusHistory.status':'AVAILABLE'},{'statusHistory': {$slice: -1}})

这将返回具有数组中最后一个statusHistory元素包含属性status='AVAILABLE'products


0

我不确定为什么我的上面的回答被删除了。我正在重新发布它。我相信在不更改模式的情况下,您应该能够以这种方式完成。

db.example.find({ "comments:{$slice:-1}.by" : "Abe" } 

// ... 或者

db.example.find({ "comments.by" : "Abe" }

默认情况下,它会取数组中的最后一个元素。


1
这两个都是错误的。$slice 是一个投影操作符,而不是查询操作符,第二个查询会查看所有评论,而不仅仅是最后一个。 - JohnnyHK
一些方式,ashwin的答案帮助了我,因为根据MongoDB文档,$slice确实可以在查询中使用:http://docs.mongodb.org/manual/reference/operator/projection/slice/ - Jefferson Sofarelli

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