MongoDB高级查询 - 使用另一个字段的值

3
我正在评估MongoDB,并想知道它在查询方面的能力如何。
关于我的数据集,我可能需要使用一个字段的值与另一个字段的值进行比较。最好的解释方法是举例说明。
在以下JSON中,我希望返回至少有一个年龄小于30岁且所在国家人口超过1亿的人的文档。:
{
  people: [
    { name: "Feyyaz", age: 28, country: "Turkiye" },
    { name: "Joseph", age: 25, country: "USA" },
    ...
  ],
  countries: [
    { name: "Turkiye", population: 75000000 },
    { name: "USA", population: 300000000 },
    ...
  ]
}

注意:这个例子完全是我编造的,因为我的真实世界的例子要复杂得多。而改变结构应该是最后的选择。


请问您能否提供一份代码尝试?这里是最佳起点:MongoDB refdoc。尝试编写代码可以帮助您更准确地提出问题,我们不会为您编写代码。 - dgiugg
@dgiugg 并不是每个人都知道如何做,而且问题明确说明这是一次评估。当你看到这个问题时,很明显有一个明确的问题需要解决,对于新手来说这是一个难以用代码解决的问题。 - Neil Lunn
@dgiugg,请看下面Neil的回答。作为一个评估产品的人,我认为这很难尝试。尽量给出建设性的答案! - Feyyaz
3个回答

3

如果您能使用Python完成此任务,那么可以考虑使用查询语言ObjectPath。这将使您只需要一行代码即可完成任务:

$.people[@.age<30 and $.countries[@.name is @@.country].population > 100000000]

除了 "@@" 尚未实现 - 如果您想使用它,可以在 github 页面上编写一个功能请求

声明:计划在不久的将来将这种语言与 MongoDB 集成,以便利用 MongoDB 的分布式能力。


1
使用.find()的标准查询操作无法像您所要求的那样将两个字段匹配。您可以使用标准匹配条件获得“接近”的结果,但实际上让数组元素进行比较则需要更高级一点。
您正在寻找的“高级瑞士军刀”以MongoDB聚合框架的形式呈现。它不仅可以“聚合”数据,还可以作为一般文档操作和评估的工具:
db.pop.aggregate([

  // Match possible documents to reduce work
  { "$match": {
    "people.age": { "$lt": 30 },
    "countries.population": { "$gt": 100000000 }
  }},

  // Test the conditions against the arrays      
  { "$project": {
    "people": 1,
    "countries": 1,
    "match": {
      "$anyElementTrue": {
        "$map": {
          "input": "$people",
          "as": "p",
          "in": {
            "$anyElementTrue": {
              "$map": {
                "input": "$countries",
                "as": "c",
                "in": {
                  "$and": [
                    { "$lt": [ "$$p.age",30 ] },
                    { "$gt": [ "$$c.population",100000000 ] },
                    { "$eq": [ "$$p.country", "$$c.name" ] }
                  ]
                }
              }
            }
          }
        }
      }
    }
  }},

  // Filter any documents that did not match
  { "$match": { "match": true }}
])

如果你想将它们“过滤”成匹配的结果,那么你可以稍微改变一下做法。我会将$project分阶段进行,但你也可以在一个阶段中完成:
db.pop.aggregate([

  // Match possible documents to reduce work
  { "$match": {
    "people.age": { "$lt": 30 },
    "countries.population": { "$gt": 100000000 }
  }},

  // Filter the people array for matches
  { "$project": {
    "people": {
      "$setDifference": [
        { "$map": {
          "input": "$people",
          "as": "p",
          "in": {
            "$cond": [
              { "$and": [
                { "$lt": [ "$$p.age", 30 ] },
                {
                  "$anyElementTrue": {
                    "$map": {
                      "input": "$countries",
                      "as": "c",
                      "in": {
                        "$and": [
                          { "$gt": [ "$$c.population", 100000000 ] },
                          { "$eq": [ "$$p.country", "$$c.name" ] }
                        ]
                      }
                    }
                  }
                }
              ]},
              "$$p",
              false
            ]
          }
        }},
        [false]
      ]
    },
    "countries": 1
  }},

  // Discard any document that did not meet conditions
  { "$match": { "people": { "$ne": false } }},

  // Filter the countries to matching people
  { "$project": {
    "people": 1,
    "countries": {
      "$setDifference": [
        { "$map": {
          "input": "$countries",
          "as": "c",
          "in": {
            "$cond": [
              { "$and": [
                { "$gt": [ "$$c.population", 100000000 ] },
                {
                  "$anyElementTrue": {
                    "$map": {
                      "input": "$people",
                      "as": "p",
                      "in": {
                        "$eq": [ "$$p.country", "$$c.name" ]
                      }
                    }                    
                  }
                }
              ]},
              "$$c",
              false
            ]
          }
        }},
        [false]
      ]
    }
  }}
])

在第二种情况下,您将得到过滤掉不匹配数组元素的文档,如下所示:
{
    "_id" : ObjectId("53c8f1645117367f5ff2036c"),
    "people" : [
            {
                    "name" : "Joseph",
                    "age" : 25,
                    "country" : "USA"
            }
    ],
    "countries" : [
            {
                    "name" : "USA",
                    "population" : 300000000
            }
    ]
}

非常强大的东西。

还可以在文档中查看聚合框架运算符和其他聚合示例。

您也可以使用mapReduce来做类似的事情,但通常更喜欢使用聚合框架,因为它是一种本地代码实现,而MongoDB mapReduce则依赖于JavaScript解释运行。


谢谢你的回答。这意味着这种查询是可能的,但需要付出很多努力和难以阅读的脚本 :)。顺便说一下,它看起来是正确的,但在try.mongodb.org上无法工作。可能是语法问题。如果你能修复它就太好了。 - Feyyaz
@FeyyazE 语法仅适用于MongoDB 2.6及以上版本。在早期版本中仍然可以实现,但是列表会更加混乱。您正在“评估”,因此请“评估”。下载、安装并尝试最新版本的MongoDB 2.6.3。 - Neil Lunn
@FeyyazE 在发布之前,我自己运行了代码,所以我知道它是有效的。我在控制台上发布的结果是正确的。你删除了注释吗?尽管那应该仍然可以工作。检查你的安装。否则你似乎已经改变了一些东西。 - Neil Lunn
我已经安装了v2.6.3,但我刚意识到try.mongodb.org不支持所有命令,或者它不是最新的。在我的电脑上可以工作,谢谢。 - Feyyaz

0

关于NeilLunn答案中FeyyazE的评论,实际上您也可以使用标准的JavaScript和真正经典易读的函数,像这样:

function test1 (field) {return field <= 30;}
function test2 (field) {return field >= 100000000;}

var fct = function (array1, field1, pivot1, array2, field2, pivot2) {
    for (var key in array1) {
        if (test1(array1[key][field1])) {
            for (var key2 in array2) {
                if (array2[key2][pivot2] == array1[key][pivot1] && test2(array2[key2][field2])) {
                    return true;
                }
            }
        }
    }
    return false;
}

db.test.find({$where: "fct(
    this.people,
    'age',
    'country',
    this.countries,
    'population',
    'name'
)"});

但是这将需要很长时间才能让Mongo进行评估。我在shell中尝试了一个小的100K文档集合,它花费了... 3秒钟!所以也许你更喜欢付出努力和难以阅读的脚本...


1
我认为这里没有人谈论一个文档,而是数以万计的文档。 - Neil Lunn

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