查找数组字段不为空的 MongoDB 记录

Posted

技术标签:

【中文标题】查找数组字段不为空的 MongoDB 记录【英文标题】:Find MongoDB records where array field is not empty 【发布时间】:2018-02-24 00:44:48 【问题描述】:

我所有的记录都有一个名为“图片”的字段。该字段是一个字符串数组。

我现在想要这个数组不为空的最新 10 条记录。

我已经用谷歌搜索了,但奇怪的是我没有找到太多关于这个的东西。 我已经阅读了 $where 选项,但我想知道这对原生函数有多慢,以及是否有更好的解决方案。

即使那样,这也行不通:

ME.find($where: 'this.pictures.length > 0').sort('-created').limit(10).execFind()

什么都不返回。离开 this.pictures 不带长度位确实有效,但当然它也会返回空记录。

【问题讨论】:

【参考方案1】:

如果您还有没有密钥的文档,您可以使用:

ME.find( pictures:  $exists: true, $not: $size: 0  )

如果涉及$size,MongoDB 不会使用索引,所以这里有一个更好的解决方案:

ME.find( pictures:  $exists: true, $ne: []  )

如果您的属性可能包含无效值(如null boolean 或其他),那么您可以按照建议的in this answer 添加额外的检查using $types

使用 mongo >= 3.2:

ME.find( pictures:  $exists: true, $type: 'array', $ne: []  )

使用 mongo

ME.find( pictures:  $exists: true, $type: 4, $ne: []  )

从MongoDB 2.6版本开始,你可以和算子$gt对比,但这可能会导致意想不到的结果(你可以找到详细解释in this answer):

ME.find( pictures:  $gt: []  )

【讨论】:

对我来说这是正确的方法,因为它确保数组存在且不为空。 小心,ME.find( pictures: $gt: [] ) 是危险的,即使在较新的 MongoDB 版本中也是如此。如果您的列表字段上有一个索引,并且在查询期间使用了该索引,您将得到意想不到的结果。例如:db.doc.find('nums': $gt: [] ).hint( _id: 1 ).count() 返回正确的数字,而db.doc.find('nums': $gt: [] ).hint( nums: 1 ).count() 返回0 在下面查看我的详细回答,了解为什么这可能不适合您:***.com/a/42601244/1579058 @wojcikstefan 的评论需要被投票,以防止人们使用最后一个建议,在某些情况下确实不会返回匹配的文档。 使用 $exists 返回图片为空、未定义等的文档。这是better solution【参考方案2】:

在看了一些之后,尤其是在 mongodb 文档中,以及一些令人费解的地方,这就是答案:

ME.find(pictures: $exists: true, $not: $size: 0)

【讨论】:

这不起作用。我不知道这是否以前有效,但这也会返回没有“图片”键的对象。 令人难以置信的是,这个答案有 63 个赞成票,而事实上 @rdsoze 所说的是真的 - 查询还将返回 没有 具有 pictures 字段的记录。 小心,如果涉及 $size link,mongoDB 不会使用索引。最好包含 $ne:[] 和可能的 $ne:null。 @rdsoze 问题的第一行指出“我的所有记录都有一个名为“图片”的字段。该字段是一个数组”。更重要的是,这是一个非常现实和常见的场景。这个答案没有错,它完全按照书面形式适用于问题,批评或否决它不能解决不同问题的事实是愚蠢的。 @Cec 所有文档都说,如果您在查询中使用 $size,它不会使用任何索引来为您提供更快的结果。因此,如果您在该字段上有索引并且想要使用它,请坚持使用 $ne:[] 等其他方法,如果这对您有用,那将使用您的索引。【参考方案3】:

这也可能对您有用:

ME.find('pictures.0': $exists: true);

【讨论】:

不错!这也可以让您检查最小尺寸。你知道数组是否总是按顺序索引吗?是否会出现pictures.2 存在但pictures.1 不存在的情况? $exists 运算符是布尔值,而不是偏移量。 @tenbatsu 应该使用 true 而不是 1 @anushr Would there ever be a case where pictures.2 exists but pictures.1 does not? 是的,这种情况可能会发生。 @TheBndr 只有当pictures 是子文档而不是数组时才会发生这种情况。例如pictures: '2': 123 这很好,也很直观,但如果性能很重要,请注意 - 即使您在 pictures 上有索引,它也会执行完整的集合扫描。【参考方案4】:

查询时您关心两件事 - 准确性和性能。考虑到这一点,我在 MongoDB v3.0.14 中测试了几种不同的方法。

TL;DR db.doc.find( nums: $gt: -Infinity ) 是最快和最可靠的(至少在我测试的 MongoDB 版本中)。

编辑:这不再适用于 MongoDB v3.6!请参阅此帖子下的 cmets 以获得潜在的解决方案。

设置

我插入了 1k 个不带列表字段的文档、1k 个带有空列表的文档和 5 个带有非空列表的文档。

for (var i = 0; i < 1000; i++)  db.doc.insert(); 
for (var i = 0; i < 1000; i++)  db.doc.insert( nums: [] ); 
for (var i = 0; i < 5; i++)  db.doc.insert( nums: [1, 2, 3] ); 
db.doc.createIndex( nums: 1 );

我认识到这不足以像我在下面的测试中那样认真对待性能,但它足以展示各种查询的正确性和所选查询计划的行为。

测试

db.doc.find('nums': '$exists': true) 返回错误的结果(对于我们想要完成的任务)。

MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums': '$exists': true).count()
1005

--

db.doc.find('nums.0': '$exists': true) 返回正确的结果,但使用完整的集合扫描也很慢(请注意解释中的 COLLSCAN 阶段)。

MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums.0': '$exists': true).count()
5
MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums.0': '$exists': true).explain()

  "queryPlanner": 
    "plannerVersion": 1,
    "namespace": "test.doc",
    "indexFilterSet": false,
    "parsedQuery": 
      "nums.0": 
        "$exists": true
      
    ,
    "winningPlan": 
      "stage": "COLLSCAN",
      "filter": 
        "nums.0": 
          "$exists": true
        
      ,
      "direction": "forward"
    ,
    "rejectedPlans": [ ]
  ,
  "serverInfo": 
    "host": "MacBook-Pro",
    "port": 27017,
    "version": "3.0.14",
    "gitVersion": "08352afcca24bfc145240a0fac9d28b978ab77f3"
  ,
  "ok": 1

--

db.doc.find('nums': $exists: true, $gt: '$size': 0 ) 返回错误结果。这是因为无效的索引扫描没有推进任何文档。如果没有索引,它可能会准确但速度很慢。

MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $exists: true, $gt:  '$size': 0 ).count()
0
MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $exists: true, $gt:  '$size': 0 ).explain('executionStats').executionStats.executionStages

  "stage": "KEEP_MUTATIONS",
  "nReturned": 0,
  "executionTimeMillisEstimate": 0,
  "works": 2,
  "advanced": 0,
  "needTime": 0,
  "needFetch": 0,
  "saveState": 0,
  "restoreState": 0,
  "isEOF": 1,
  "invalidates": 0,
  "inputStage": 
    "stage": "FETCH",
    "filter": 
      "$and": [
        
          "nums": 
            "$gt": 
              "$size": 0
            
          
        ,
        
          "nums": 
            "$exists": true
          
        
      ]
    ,
    "nReturned": 0,
    "executionTimeMillisEstimate": 0,
    "works": 1,
    "advanced": 0,
    "needTime": 0,
    "needFetch": 0,
    "saveState": 0,
    "restoreState": 0,
    "isEOF": 1,
    "invalidates": 0,
    "docsExamined": 0,
    "alreadyHasObj": 0,
    "inputStage": 
      "stage": "IXSCAN",
      "nReturned": 0,
      "executionTimeMillisEstimate": 0,
      "works": 1,
      "advanced": 0,
      "needTime": 0,
      "needFetch": 0,
      "saveState": 0,
      "restoreState": 0,
      "isEOF": 1,
      "invalidates": 0,
      "keyPattern": 
        "nums": 1
      ,
      "indexName": "nums_1",
      "isMultiKey": true,
      "direction": "forward",
      "indexBounds": 
        "nums": [
          "( $size: 0.0 , [])"
        ]
      ,
      "keysExamined": 0,
      "dupsTested": 0,
      "dupsDropped": 0,
      "seenInvalidated": 0,
      "matchTested": 0
    
  

--

db.doc.find('nums': $exists: true, $not: '$size': 0 ) 返回正确的结果,但性能很差。从技术上讲,它会进行索引扫描,但它仍然会推进所有文档,然后必须过滤它们)。

MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $exists: true, $not:  '$size': 0 ).count()
5
MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $exists: true, $not:  '$size': 0 ).explain('executionStats').executionStats.executionStages

  "stage": "KEEP_MUTATIONS",
  "nReturned": 5,
  "executionTimeMillisEstimate": 0,
  "works": 2016,
  "advanced": 5,
  "needTime": 2010,
  "needFetch": 0,
  "saveState": 15,
  "restoreState": 15,
  "isEOF": 1,
  "invalidates": 0,
  "inputStage": 
    "stage": "FETCH",
    "filter": 
      "$and": [
        
          "nums": 
            "$exists": true
          
        ,
        
          "$not": 
            "nums": 
              "$size": 0
            
          
        
      ]
    ,
    "nReturned": 5,
    "executionTimeMillisEstimate": 0,
    "works": 2016,
    "advanced": 5,
    "needTime": 2010,
    "needFetch": 0,
    "saveState": 15,
    "restoreState": 15,
    "isEOF": 1,
    "invalidates": 0,
    "docsExamined": 2005,
    "alreadyHasObj": 0,
    "inputStage": 
      "stage": "IXSCAN",
      "nReturned": 2005,
      "executionTimeMillisEstimate": 0,
      "works": 2015,
      "advanced": 2005,
      "needTime": 10,
      "needFetch": 0,
      "saveState": 15,
      "restoreState": 15,
      "isEOF": 1,
      "invalidates": 0,
      "keyPattern": 
        "nums": 1
      ,
      "indexName": "nums_1",
      "isMultiKey": true,
      "direction": "forward",
      "indexBounds": 
        "nums": [
          "[MinKey, MaxKey]"
        ]
      ,
      "keysExamined": 2015,
      "dupsTested": 2015,
      "dupsDropped": 10,
      "seenInvalidated": 0,
      "matchTested": 0
    
  

--

db.doc.find('nums': $exists: true, $ne: [] ) 返回正确结果,速度稍快,但性能仍不理想。它使用 IXSCAN,它只推进具有现有列表字段的文档,但必须一一过滤掉空列表。

MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $exists: true, $ne: [] ).count()
5
MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $exists: true, $ne: [] ).explain('executionStats').executionStats.executionStages

  "stage": "KEEP_MUTATIONS",
  "nReturned": 5,
  "executionTimeMillisEstimate": 0,
  "works": 1018,
  "advanced": 5,
  "needTime": 1011,
  "needFetch": 0,
  "saveState": 15,
  "restoreState": 15,
  "isEOF": 1,
  "invalidates": 0,
  "inputStage": 
    "stage": "FETCH",
    "filter": 
      "$and": [
        
          "$not": 
            "nums": 
              "$eq": [ ]
            
          
        ,
        
          "nums": 
            "$exists": true
          
        
      ]
    ,
    "nReturned": 5,
    "executionTimeMillisEstimate": 0,
    "works": 1017,
    "advanced": 5,
    "needTime": 1011,
    "needFetch": 0,
    "saveState": 15,
    "restoreState": 15,
    "isEOF": 1,
    "invalidates": 0,
    "docsExamined": 1005,
    "alreadyHasObj": 0,
    "inputStage": 
      "stage": "IXSCAN",
      "nReturned": 1005,
      "executionTimeMillisEstimate": 0,
      "works": 1016,
      "advanced": 1005,
      "needTime": 11,
      "needFetch": 0,
      "saveState": 15,
      "restoreState": 15,
      "isEOF": 1,
      "invalidates": 0,
      "keyPattern": 
        "nums": 1
      ,
      "indexName": "nums_1",
      "isMultiKey": true,
      "direction": "forward",
      "indexBounds": 
        "nums": [
          "[MinKey, undefined)",
          "(undefined, [])",
          "([], MaxKey]"
        ]
      ,
      "keysExamined": 1016,
      "dupsTested": 1015,
      "dupsDropped": 10,
      "seenInvalidated": 0,
      "matchTested": 0
    
  

--

db.doc.find('nums': $gt: [] ) 很危险,因为取决于所使用的索引,它可能会产生意想不到的结果。这是因为无效的索引扫描不推进任何文档。

MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $gt: [] ).count()
0
MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $gt: [] ).hint( nums: 1 ).count()
0
MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $gt: [] ).hint( _id: 1 ).count()
5

MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $gt: [] ).explain('executionStats').executionStats.executionStages

  "stage": "KEEP_MUTATIONS",
  "nReturned": 0,
  "executionTimeMillisEstimate": 0,
  "works": 1,
  "advanced": 0,
  "needTime": 0,
  "needFetch": 0,
  "saveState": 0,
  "restoreState": 0,
  "isEOF": 1,
  "invalidates": 0,
  "inputStage": 
    "stage": "FETCH",
    "filter": 
      "nums": 
        "$gt": [ ]
      
    ,
    "nReturned": 0,
    "executionTimeMillisEstimate": 0,
    "works": 1,
    "advanced": 0,
    "needTime": 0,
    "needFetch": 0,
    "saveState": 0,
    "restoreState": 0,
    "isEOF": 1,
    "invalidates": 0,
    "docsExamined": 0,
    "alreadyHasObj": 0,
    "inputStage": 
      "stage": "IXSCAN",
      "nReturned": 0,
      "executionTimeMillisEstimate": 0,
      "works": 1,
      "advanced": 0,
      "needTime": 0,
      "needFetch": 0,
      "saveState": 0,
      "restoreState": 0,
      "isEOF": 1,
      "invalidates": 0,
      "keyPattern": 
        "nums": 1
      ,
      "indexName": "nums_1",
      "isMultiKey": true,
      "direction": "forward",
      "indexBounds": 
        "nums": [
          "([], BinData(0, ))"
        ]
      ,
      "keysExamined": 0,
      "dupsTested": 0,
      "dupsDropped": 0,
      "seenInvalidated": 0,
      "matchTested": 0
    
  

--

db.doc.find('nums.0’: $gt: -Infinity ) 返回正确的结果,但性能不佳(使用完整的集合扫描)。

MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums.0':  $gt: -Infinity ).count()
5
MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums.0':  $gt: -Infinity ).explain('executionStats').executionStats.executionStages

  "stage": "COLLSCAN",
  "filter": 
    "nums.0": 
      "$gt": -Infinity
    
  ,
  "nReturned": 5,
  "executionTimeMillisEstimate": 0,
  "works": 2007,
  "advanced": 5,
  "needTime": 2001,
  "needFetch": 0,
  "saveState": 15,
  "restoreState": 15,
  "isEOF": 1,
  "invalidates": 0,
  "direction": "forward",
  "docsExamined": 2005

--

db.doc.find('nums': $gt: -Infinity ) 令人惊讶的是,这非常好用!它给出了正确的结果,而且速度很快,从索引扫描阶段推进了 5 个文档。

MacBook-Pro(mongod-3.0.14) test> db.doc.find('nums':  $gt: -Infinity ).explain('executionStats').executionStats.executionStages

  "stage": "FETCH",
  "nReturned": 5,
  "executionTimeMillisEstimate": 0,
  "works": 16,
  "advanced": 5,
  "needTime": 10,
  "needFetch": 0,
  "saveState": 0,
  "restoreState": 0,
  "isEOF": 1,
  "invalidates": 0,
  "docsExamined": 5,
  "alreadyHasObj": 0,
  "inputStage": 
    "stage": "IXSCAN",
    "nReturned": 5,
    "executionTimeMillisEstimate": 0,
    "works": 15,
    "advanced": 5,
    "needTime": 10,
    "needFetch": 0,
    "saveState": 0,
    "restoreState": 0,
    "isEOF": 1,
    "invalidates": 0,
    "keyPattern": 
      "nums": 1
    ,
    "indexName": "nums_1",
    "isMultiKey": true,
    "direction": "forward",
    "indexBounds": 
      "nums": [
        "(-inf.0, inf.0]"
      ]
    ,
    "keysExamined": 15,
    "dupsTested": 15,
    "dupsDropped": 10,
    "seenInvalidated": 0,
    "matchTested": 0
  

【讨论】:

感谢您非常详细的回答@wojcikstefan。不幸的是,您建议的解决方案似乎不适用于我的情况。我有一个包含 2m 个文档的 MongoDB 3.6.4 集合,其中大多数有一个 seen_events String 数组,它也被索引。使用 $gt: -Infinity 搜索,我立即得到 0 个文档。使用 $exists: true, $ne: [] ,我得到的文档更有可能是 1,2m,在 FETCH 阶段浪费了大量时间:gist.github.com/N-Coder/b9e89a925e895c605d84bfeed648d82c 看来你是对的@Ncode - 这不再适用于 MongoDB v3.6 :( 我玩了几分钟,这是我发现的:1. db.test_collection.find("seen_events.0": $exists: true) 是不好,因为它使用集合扫描。 2. db.test_collection.find(seen_events: $exists: true, $ne: []) 不好,因为它的 IXSCAN 匹配所有文档,然后在慢速 FETCH 阶段执行过滤。 3. db.test_collection.find(seen_events: $exists: true, $not: $size: 0) 也是如此。 4. 所有其他查询都返回无效结果。 @NCode 找到了解决方案!如果您确定所有非空 seen_events 都包含字符串,则可以使用:db.test_collection.find(seen_events: $gt: '').count()。要确认它表现良好,请查看db.test_collection.find(seen_events: $gt: '').explain(true).executionStats。您可能可以通过模式验证强制看到的事件是字符串:docs.mongodb.com/manual/core/schema-validation 谢谢!所有现有值都是字符串,所以我会尝试一下。在 MongoDB bugtracker 中也有一个 bug 讨论这个问题:jira.mongodb.org/browse/SERVER-26655 使用 mongodb shell 并解释这种方法给了我最有效的查询,干得好,谢谢!到目前为止,这是我想出如何使用 c# 驱动程序表达它的最佳方法: var builder = Builders.Filter; var filter = builder.Gt("myarray", Decimal128.NegativeInfinity); myObjects = col.findOne(filter).ToList();【参考方案5】:

从 2.6 版本开始,另一种方法是将字段与空数组进行比较:

ME.find(pictures: $gt: [])

在 shell 中测试它:

> db.ME.insert([
pictures: [1,2,3],
pictures: [],
pictures: [''],
pictures: [0],
pictures: 1,
foobar: 1
])

> db.ME.find(pictures: $gt: [])
 "_id": ObjectId("54d4d9ff96340090b6c1c4a7"), "pictures": [ 1, 2, 3 ] 
 "_id": ObjectId("54d4d9ff96340090b6c1c4a9"), "pictures": [ "" ] 
 "_id": ObjectId("54d4d9ff96340090b6c1c4aa"), "pictures": [ 0 ] 

因此它正确地包含了pictures 至少有一个数组元素的文档,并排除了pictures 为空数组、不是数组或缺失的文档。

【讨论】:

小心如果您尝试使用索引,此答案可能会给您带来麻烦。执行 db.ME.createIndex( pictures: 1 ) 然后 db.ME.find(pictures: $gt: []) 将返回零结果,至少在 MongoDB v3.0.14 中 @wojcikstefan 很好。需要重新审视这一点。【参考方案6】:

仅检索“图片”为数组且不为空的所有文档

ME.find(pictures: $type: 'array', $ne: [])

如果使用 3.2 之前的 MongoDb 版本,请使用 $type: 4 而不是 $type: 'array'。请注意,此解决方案甚至没有使用$size,因此索引没有问题(“查询不能对查询的 $size 部分使用索引”)

其他解决方案,包括这些(接受的答案):

ME.find( 图片: $exists: true, $not: $size: 0 ); ME.find( 图片: $exists: true, $ne: [] )

错误,因为它们返回文档,例如,'pictures' 是 nullundefined、0 等。

【讨论】:

【参考方案7】:

您可以使用以下任何方法来实现此目的。 两者还注意不为其中没有请求的键的对象返回结果:

db.video.find(pictures: $exists: true, $gt: $size: 0)
db.video.find(comments: $exists: true, $not: $size: 0)

【讨论】:

【参考方案8】:

使用$elemMatch 运算符:根据文档

$elemMatch 运算符匹配包含数组字段且至少有一个元素匹配所有指定查询条件的文档。

$elemMatches 确保该值是一个数组并且它不为空。所以查询会是这样的

ME.find( pictures: $elemMatch: $exists: true )

PS 此代码的变体可在 MongoDB 大学的 M121 课程中找到。

【讨论】:

【参考方案9】:
db.find( pictures:  $elemMatch:  $exists: true   )

$elemMatch 匹配包含数组字段且至少有一个元素与指定查询匹配的文档。

因此,您将所有数组与至少一个元素进行匹配。

【讨论】:

【参考方案10】:

您还可以在 Mongo 运算符 $exists 上使用辅助方法 Exists

ME.find()
    .exists('pictures')
    .where('pictures').ne([])
    .sort('-created')
    .limit(10)
    .exec(function(err, results)
        ...
    );

【讨论】:

【参考方案11】:
 $where: "this.pictures.length > 1" 

使用 $where 并传递 this.field_name.length ,它返回数组字段的大小并通过与数字比较来检查它。如果任何数组的值大于数组大小必须至少为 1。所以所有数组字段的长度都大于 1,这意味着它在该数组中有一些数据

【讨论】:

【参考方案12】:

这也有效:

db.getCollection('collectionName').find('arrayName': $elemMatch:)

【讨论】:

【参考方案13】:
ME.find(pictures: $exists: true) 

就这么简单,这对我有用。

【讨论】:

以上是关于查找数组字段不为空的 MongoDB 记录的主要内容,如果未能解决你的问题,请参考以下文章

查找数组字段不为空的 MongoDB 记录

查找数组字段不为空的 MongoDB 记录

查找数组字段不为空的 MongoDB 记录

用PHP查询mongo数据时,条件是某个字段(A为数组)不为空,但是有的记录中并没有字段A,这个条件怎么写?

mysql查找字段空不为空的方法总结

使用 MongoDB 检查字段是不是存在