查询 MongoDB 记录中数组字段不为空的数据

763

我的所有记录都有一个名为"pictures"的字段。该字段是一个字符串数组。

现在,我想要最新的10个记录,其中此数组不为空。

我已经在Google上搜索了一些资料,但奇怪的是我没有找到太多关于这个的内容。 我已经研究了$where选项,但我想知道它与本地函数相比速度有多慢,是否有更好的解决方案。

即使如此,那也行不通:

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

不返回任何内容。如果省略长度字段,则this.pictures仍然有效,但当然也会返回空记录。

13个回答

1341
如果您还有没有密钥的文档,您可以使用以下方法:
ME.find({ pictures: { $exists: true, $not: {$size: 0} } })

如果使用了$size,MongoDB不会使用索引,因此这里有一个更好的解决方案:

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

如果您的属性可能包含无效值(如null boolean或其他值),那么可以添加一个额外的检查,使用建议中提到的$type操作符,如此答案所示:

对于mongo版本>= 3.2:

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

使用Mongo版本< 3.2:

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

自MongoDB 2.6版本以来,您可以使用运算符$gt进行比较,但这可能会导致意外的结果(您可以在此答案中找到详细说明):

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

7
对我来说,那是正确的方式,因为它确保该数组存在且不为空。 - lrepolho
69
小心,即使在新版的MongoDB中,ME.find({ pictures: { $gt: [] } })也是危险的。如果你在列表字段上建立了索引并且该索引在查询过程中被利用,你将会得到意想不到的结果。例如:db.doc.find({'nums': { $gt: [] }}).hint({ _id: 1 }).count() 返回正确的数量,而 db.doc.find({'nums': { $gt: [] }}).hint({ nums: 1 }).count() 返回 0 - wojcikstefan
3
请看下面的详细答案以了解为什么这可能不适用于您:https://dev59.com/eGUq5IYBdhLWcg3wF8rw#42601244。 - wojcikstefan
6
需要点赞@wojcikstefan的评论,以防止人们使用最后一个建议,因为在某些情况下,它确实无法返回匹配的文档。请注意不要改变原来的意思并尽量使翻译通俗易懂。 - Thomas Jung
2
使用 $exists 可以返回图片为 null、undefined 等的文档。这里有一个更好的解决方案:https://dev59.com/eGUq5IYBdhLWcg3wF8rw#52465830 - ingdc
显示剩余6条评论

213

经过进一步查找,特别是在mongodb文档中,认真拼凑一些碎片,这就是答案:

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

34
无法运行。我不知道之前是否能运行,但这也会返回没有“pictures”键的对象。 - rdsoze
20
这个答案竟然获得了63个赞,实际上@rdsoze所说的是真的——这个查询也会返回没有“图片”字段的记录。 - Dan Dascalescu
5
注意,如果涉及$size操作符,MongoDB将不使用索引链接。最好包含{$ne:[]},可能还要包含{$ne:null}。 - Levente Dobson
20
问题的第一行指出:“我的所有记录都有一个名为“pictures”的字段。此字段是一个数组”。而且,这是一个非常现实和常见的情况。这个答案并不是错误的,它完全可以解决问题,并且批评或者对其进行投票降低评分,因为它不能解决另一个不同的问题是愚蠢的。 - Mark Amery
@LeventeDobson 这是否意味着我可以定义一个索引,仅查找存在且不为空的字段的文档? - Cec
2
@Cec 所有的文档都说,如果在查询中使用 $size,它不会使用任何索引来提供更快的结果。因此,如果您在该字段上有索引并且想要使用它,请坚持使用其他方法,如 {$ne:[]},如果这对您有效,它将使用您的索引。 - Levente Dobson

138

这对你也可能有用:

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

2
太好了!这还能让你检查最小尺寸。你知道数组是否总是按顺序索引的吗?有没有可能出现pictures.2存在而pictures.1不存在的情况呢? - anushr
2
$exists 运算符是一个布尔值,而不是偏移量。@tenbatsu 应该使用 true 而不是 1 - awhie29urh2
3
可能会出现这样的情况,即pictures.2存在而pictures.1不存在。 - The Bndr
2
@TheBndr 这只有在 pictures 是一个子文档而不是数组时才会发生。例如:pictures: {'2': 123} - JohnnyHK
10
这很好理解,但如果性能很重要,要小心 - 即使你在“pictures”上有索引,它仍会执行完整的集合扫描。 - wojcikstefan

42

在查询时,您关心两件事情-准确性和性能。 出于这个目的,我在MongoDB v3.0.14中测试了几种不同的方法。

简而言之,db.doc.find({ nums: { $gt: -Infinity }})是最快且最可靠的方法(至少在我测试的MongoDB版本中)。

编辑:这在MongoDB v3.6中已不再起作用!请参见本帖子下面的评论以获取潜在解决方案。

设置

我插入了1000个没有列表字段的文档,1000个带有空列表的文档以及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。 不幸的是,您提出的解决方案似乎在我的情况下无法工作。 我有一个MongoDB 3.6.4集合,其中包含200万个文档,其中大多数文档都具有“seen_events”字符串数组,该数组也已索引。 使用“{ $gt:-Infinity }”进行搜索,我立即获得0个文档。 使用“{ $exists:true,$ne:[] }”我可以获得更可能的120万个文档,但在FETCH阶段浪费了很多时间: https://gist.github.com/N-Coder/b9e89a925e895c605d84bfeed648d82c - NCode
看起来你是对的 @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. 所有其他查询都返回无效结果。
- wojcikstefan
3
@NCode找到了解决方案! 如果您确信所有非空的 seen_events 都包含字符串,那么您可以使用此命令:db.test_collection.find({seen_events:{$gt:''}})。count()。要确认其表现良好,请查看 db.test_collection.find({seen_events:{$gt:''}})。explain(true)。executionStats。通过模式验证,您可能可以强制执行将已看活动转换为字符串:https://docs.mongodb.com/manual/core/schema-validation/。 - wojcikstefan
谢谢!所有现有的值都是字符串,所以我会尝试一下。 在MongoDB bugtracker中也有一个讨论此问题的错误报告: https://jira.mongodb.org/browse/SERVER-26655 - NCode
使用mongodb shell并解释这种方法给了我最有效的查询,做得好,谢谢!以下是我迄今为止使用c#驱动程序找到的最佳表达方式:var builder = Builders<MyClass>.Filter; var filter = builder.Gt("myarray", Decimal128.NegativeInfinity); myObjects = col.findOne(filter).ToList(); - DavidJBerman

32

从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为空数组、不是数组或缺失的文档。


11
小心,如果您尝试使用索引,这个答案可能会给您带来麻烦。在MongoDB v3.0.14中,执行db.ME.createIndex({ pictures: 1 })然后执行db.ME.find({pictures: {$gt: []}})将返回零结果。 - wojcikstefan
@wojcikstefan 很好的发现。需要重新审视一下这个问题。 - JohnnyHK

12

检索所有且仅包含非空数组'pictures'的文档

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

如果使用MongoDb版本3.2之前的版本,请使用$type: 4代替$type: 'array'。请注意,此解决方案甚至不使用$size,因此索引没有问题(“查询无法在查询的$size部分使用索引”)。
其他解决方案,包括这些(接受的答案):
```ME.find({ pictures: { $exists: true, $not: {$size: 0} } }); ME.find({ pictures: { $exists: true, $ne: [] } })```
是错误的,因为它们返回文档,即使例如“pictures”为nullundefined、0等。

9
db.find({ pictures: { $elemMatch: { $exists: true } } })

$elemMatch 可以匹配包含至少一个符合指定查询条件的数组字段的文档。

因此,您可以匹配所有至少有一个元素的数组。


7
您可以使用以下任何一种方法来实现此目的。
两者还会注意不返回未在对象中请求的键的结果:
db.video.find({pictures: {$exists: true, $gt: {$size: 0}}})
db.video.find({comments: {$exists: true, $not: {$size: 0}}})

5
使用 $elemMatch 运算符:根据文档,$elemMatch 运算符匹配包含数组字段的文档,并且该数组至少有一个元素满足所有指定的查询条件。使用 $elemMatches 可以确保值为数组并且不为空。因此查询语句如下所示:ME.find({ pictures: { $elemMatch: {$exists: true }}})。PS:在 MongoDB University 的 M121 课程中也有这段代码的变体。

0
{ $where: "this.pictures.length > 1" }

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


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