按字段存在进行MongoDB聚合

99

我很难相信这个问题还没有被问过和回答过,但我找不到任何线索。

我有一个MongoDB聚合查询,需要根据另一个字段的存在与否分组,该字段为布尔值。

例如,让我们从这个集合开始:

> db.test.find()
{ "_id" : ObjectId("53fbede62827b89e4f86c12e"),
  "field" : ObjectId("53fbede62827b89e4f86c12d"), "name" : "Erik" }
{ "_id" : ObjectId("53fbee002827b89e4f86c12f"), "name" : "Erik" }
{ "_id" : ObjectId("53fbee092827b89e4f86c131"),
  "field" : ObjectId("53fbee092827b89e4f86c130"), "name" : "John" }
{ "_id" : ObjectId("53fbee122827b89e4f86c132"), "name" : "Ben" }

有2个文档包含"field"字段,而另外2个则没有。 需要注意的是,每个"field"字段的值可能不同;我们只是想根据其存在与否分组(或者对我来说,非空也可以,因为我没有存储任何空值)。

我尝试使用 $project,但是在那里没有 $exists,而 $cond 和 $ifNull 也没有帮助我。即使不存在该字段,它似乎始终存在:

> db.test.aggregate(
  {$project:{fieldExists:{$cond:[{$eq:["$field", null]}, false, true]}}},
  {$group:{_id:"$fieldExists", count:{$sum:1}}}
)
{ "_id" : true, "count" : 4 }

我希望以下这个更简单的聚合查询能够运行,但是由于某种原因,$exists不能像这样被支持:

> db.test.aggregate({$group:{_id:{$exists:"$field"}, count:{$sum:1}}})
assert: command failed: {
  "errmsg" : "exception: invalid operator '$exists'",
  "code" : 15999,
  "ok" : 0
} : aggregate failed
Error: command failed: {
  "errmsg" : "exception: invalid operator '$exists'",
  "code" : 15999,
  "ok" : 0
} : aggregate failed
    at Error (<anonymous>)
    at doassert (src/mongo/shell/assert.js:11:14)
    at Function.assert.commandWorked (src/mongo/shell/assert.js:244:5)
    at DBCollection.aggregate (src/mongo/shell/collection.js:1149:12)
    at (shell):1:9
2014-08-25T19:19:42.344-0700 Error: command failed: {
  "errmsg" : "exception: invalid operator '$exists'",
  "code" : 15999,
  "ok" : 0
} : aggregate failed at src/mongo/shell/assert.js:13

有人知道如何从这样的集合中获取所需的结果吗?

期望结果:

{ "_id" : true, "count" : 2 }
{ "_id" : false, "count" : 2 }
10个回答

138

不错,聪明的解决方案!我没有意识到 BSON 定义了不同类型之间的比较。 - Erik Buchanan
12
这确实很聪明,我没有更好的解决方案,但感觉像是一种巧妙的方法。 - duozmo
11
如果要检查值是否不存在或者为空,请使用 { $lte: ["$field", null] } - Ricky Boyce
@RickyBoyce 谢谢你的提示。我一直在使用 $eq,但是无法理解为什么没有得到预期的结果。现在改用 $lte,一切都正常了。 - 010011100101
2
我不知道为什么这个没有提到,但对于我来说$match: { var_name: { $exists : true } }完全可以正常工作。 我还没有测试过对聚合进行分组的情况。 - nonNumericalFloat
在2023年使用了这个解决方案,下面涉及对null / undefined进行检查的解决方案似乎在聚合管道中无法工作。 - Griffin Baker

57

我通过检查未定义来解决了它

$ne : [$var_to_check, undefined]
或者
$ne:  [ { $type : "$var_to_check"}, 'missing'] }

如果变量已定义,这将返回true。


4
我强烈建议采用第二种方法,而不是标记为已回答的那种方法。其他方法似乎都有些投机取巧。https://docs.mongodb.com/manual/reference/operator/aggregation/type/ - Ahmadreza
1
第二种方法在聚合管道中起作用,感谢@Delcon。 - Pranjal Gupta
1
“undefined” 似乎已被弃用,因此第二个选项似乎是唯一有效的解决方案。 - Kipr

28

$exists 操作符是一个 "查询" 操作符,因此它基本上用于 "过滤" 结果而不是识别逻辑条件。

作为一个 "逻辑" 操作符,聚合框架支持 $ifNull 操作符。这将返回字段值(如果存在)或备用提供的值(如果不存在),否则评估为 null

db.test.aggregate([
    { "$group": {
        "_id": { "$ifNull": [ "$field", false ] },
        "count": { "$sum": 1 }
    }}
])

当然,即使如此,这也不是一个“真/假”比较,所以除非您实际上想返回该字段存在的实际值,否则您可能最好使用一个$cond语句,就像您已经使用的那样:
db.test.aggregate([
    { "$group": {
        "_id": { "$cond": [{ "$eq": [ "$field", null ] }, true, false ] },
        "count": { "$sum": 1 }
    }}
])

在处理使用 $unwind 会导致错误的不存在的数组字段时,$ifNull 可以非常有用。然后,您可以执行类似于返回单个元素或空数组的操作,以便在其余管道处理中不会出现问题。


2
正如我在原帖中指出的那样,你的解决方案给出了错误的结果:{"_id" : false, "count" : 4}。不过还是感谢你的回答。 - Erik Buchanan
21
实际上,$eq 始终返回 false,即使字段存在也是如此。但是如果使用 $gt,它就有效。使用 "_id": { "$cond": [{ "$gt": [ "$field", null ] }, true, false ] } 代替。 - Roman Blachman
2
$eq 不检查字段是否存在。感谢 @RomanBlachman 提供的 $gt 提示。 - joniba
1
使用$match有什么区别吗?因为我刚刚尝试了$match: { var_name: { $exists : true } },聚合运算正常工作。 - nonNumericalFloat

24

不知道过去的情况如何,但现在在2019年有一个清晰的解决方案。在聚合管道中执行以下操作:

$match: {"my_field": {$ne: null}}

很好的一件事是,在我的语言中,“ne”表示“不” :)


1
我认为在聚合管道中可能不起作用,最终使用了 $gt - whoami - fakeFaceTrueSoul
2
以上查询将返回那些 my_field 字段不存在的文档。这不是预期的结果。 - Bhupinder Bisht
3
“ne”表示“不等于”。 - egvo
2
我们是说“嗯”的骑士! - JBaczuk

10

一个语义透明的解决方案来检查字段是否存在且不为空:

{ $ne: [{ $ifNull: ["$field", null] }, null] }

要检查是否缺失,请将$ne替换为$eq


8

简而言之

{'$project': {
    'field_exists': {'$or': [
        {'$eq': ['$field', null]}, 
        {'$gt': ['$field', null]},
    ]},
}}

详细信息

$exists 表示该字段存在,即使它是 null 或任何其他空值。这就是为什么本页上的所有答案都是不正确的原因。

让我们进行一些测试。请检查以下内容:

// Let's take any collection that have docs
db.getCollection('collection').aggregate([
  // Get arbitrary doc, no matter which, we won't use it
  {"$limit": 1},
  // Project our own fields (just create them with $literal)
  {'$project': {
    '_id': 0,
    'null_field': {'$literal': null},
    'not_null_field': {'$literal': {}},
  }},
])

我们会得到这个:
{
    "null_field" : null,
    "not_null_field" : {}
}

那么让我们明确一下这个文档中存在哪些字段:

  1. null_field - 存在
  2. not_null_field - 存在
  3. non_existent_field - 不存在。

好的,现在是测试我之前提到过的项目阶段的时候了。让我们为我们感兴趣的每个字段添加它:

{'$project': {
    'null_field_exists': {'$or': [
        {'$eq': ['$null_field', null]}, 
        {'$gt': ['$null_field', null]},
    ]},
    'not_null_field_exists': {'$or': [
        {'$eq': ['$not_null_field', null]}, 
        {'$gt': ['$not_null_field', null]},
    ]},
    'non_existent_field_exists': {'$or': [
        {'$eq': ['$non_existent_field', null]}, 
        {'$gt': ['$non_existent_field', null]},
    ]},
}},

我们得到的是:
{
    "null_field_exists" : true,
    "not_null_field_exists" : true,
    "non_existent_field_exists" : false
}

正确!

另外需要注意的一点是,在比较时我们使用null,因为它是至少有价值的最小值(更小的只是不存在)。


6
在mongoose中,只有以下内容有效。
$ne:  [ { $type : "$var_to_check"}, 'missing'] }

5

我的回答如下:

{$match:{
    $and:[{
        name:{
            $exists:true
        }
    }, {
        $expr:{
            $eq:["$$id", "$_id"]
        }
    }]
}}

我在管道阶段中使用这个查找功能。 在这篇文章中,第一条规则是名称必须存在。第二条规则是这两个集合之间的关系。 我相信您可以根据您的问题进行修改。


4

我使用了$addFields$ifNull来解决它,然后通过检查其值是否为空来使用$match匹配所添加的字段。

collection.aggregate(
    [
       {
          $addFields:{
              fieldName:{
                 $ifNull:["$fieldToCheckIfExists", null]
              }
          }
       },
       {
          $match:{
              fieldName:{
                $ne: null
          }
       }
    ]

0
在 Group 中,如果你想要计算现有的字段数量,而不是像其他答案所示的那样进行分组,你可以使用:
{
  _id: "$groupField",
  qtyExists: {
    $sum: {
      $cond: ["$field", 1, 0]
    }
  },
  qtyNotExists: {
    $sum: {
      $cond: ["$field", 0, 1]
    }
  },
}

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