【问题标题】:How to find an element in one of several MongoDBs subdocuments如何在几个 MongoDB 子文档之一中查找元素
【发布时间】:2015-05-26 12:54:06
【问题描述】:

我有一个 mongodb 集合,其中包含具有以下形式的子文档的文档:

    'Store': {   'cupboard1': {   'Cheese': 21,
                              'Humous': 25,
                              'Natchos': 10,
                              'Olives': 10,
                              'stockItems': 66},
                  'cupboard2': {  'Cheese': 11,
                              'Humous': 9,
                              'Olives': 2,
                              'Sausage': 3,
                              'stockItems': 25},
                  'whole':  {  'Chris': 32,
                              'Olives': 11,
                              'Sausage': 3,
                              'Humous': 34,
                              'Natchos': 10,
                              'stockItems': 91}

我想构建一些查询,这些查询依赖于根据食品名称查找文档(使用 python3/Pymongo)。 我可以看到我最初可以对“整个”子文档执行搜索,以获取匹配文档的数据。但是,我如何编写查询来查找可以在哪些橱柜中找到物品的详细信息? 另外,有没有更直接的方法可以找到橱柜?即,如果我知道我想找到香肠,但不知道它可能在哪个橱柜中找到?

【问题讨论】:

  • 我没有检查过,但我假设你可以使用带有光标的javascript函数来做到这一点。在这个函数中你可以迭代bson文档属性。参考这个页面docs.mongodb.org/manual/reference/method/cursor.forEach
  • 感谢您的建议。这似乎是一个很好的线索。我会看看。我应该补充一点,我使用的是 python/pymongo 而不是 shell(我会更新问题)——但我认为我应该能够对光标对象上的 pymongo 做同样的事情。

标签: mongodb pymongo


【解决方案1】:

我认为这里真正的问题是当前的数据结构不支持您正在尝试做的事情。有更好的方法可以做到这一点,最重要的是减少任何初始查询的负载,以便在给定的橱柜中找到“可能”包含所需项目的文档。

考虑“搜索”文档的基本前提,该文档可能在文档的“橱柜”之一中包含“香肠”。您的观察肯定是正确的,在这种结构中,最好搜索“整体”以测试是否存在。但请考虑执行此操作的查询:

collection.find({ "Store.whole.Sausage": { "$exists": True } })

这不是很好。它不理想的原因是因为您正在测试文档中是否存在“键”,这意味着不能使用“索引”并且需要“扫描”整个集合才能获得基本的结果水平。

即使一旦获得,确定“哪个”橱柜包含该项目也是一个代码问题,用于迭代对象属性并找到匹配项。在单个文档上执行此操作而不是推迟到服务器通常是有意义的,但一般来说,使用 mapReduce 的操作当然可以在服务器上运行代码并返回与呈现的文档不同的结果(作为外壳示例):

db.collection.mapReduce(
    function () {
      var Store = this.Store,
          id = this._id

      Object.keys(Store)
        .filter(function(key) {
          return key != "whole";
        })
        .forEach(function(key) {
          Object.keys( Store[key] )
            .forEach(function(el) {
              if ( el == "Sausage" )
                emit(id, {
                  cupboards: [
                    {
                      cupboard: parseInt(key.match(/\d+$/)[0]),
                      item: el,
                      qty: Store[key][el]
                    }
                  ],
                  totalQty: Store[key][el]
                });
            });
        });
    },
    function (key,values) {

      var result = { cupboards: [], totalQty: 0 };

      values.forEach(function(el) {
        el.cupboards.forEach(function(item) {
          result.cupbards.push(item);
        });
        result.totalQty += el.totalQty;
      });

      return result;

    },
    { 
        "query": { "Store.whole.Sausage": { "$exists": true } },
        "out": { "inline": 1 }
    }
)

这会返回如下内容:

{
    "results" : [
        {
            "_id" : ObjectId("5563db1c22cfcc577e5d7450"),
            "value" : {
                "cupboards" : [
                    {
                        "cupboard" : 2,
                        "item" : "Sausage",
                        "qty" : 3
                    }
                ],
                "totalQty" : 3
            }
        }
    ]
}

在客户端代码中基本上可以遵循相同的方法,您可以在其中检查文档的内容以查找匹配项。但正如我所说,这里真正的问题是初始“查询”不是最优的,是对集合的“蛮力”检查。

更好的情况是像这样构造数据:

{
    "cupboards": [
        { "cupboard": 1, "item": "Cheese", "qty": 21 },
        { "cupboard": 1, "item": "Humous", "qty": 25 },
        { "cupboard": 1, "item": "Nachos", "qty": 10 },
        { "cupboard": 1, "item": "Olives", "qty": 10 },
        { "cupboard": 2, "item": "Cheese", "qty": 11 },
        { "cupboard": 2, "item": "Humous", "qty": 9 },
        { "cupboard": 2, "item": "Olives", "qty": 2 },
        { "cupboard": 2, "item": "Sausage", "qty": 3 }
    ]
}

现在“项目”是一个“数据点”,可以对其进行索引,以便在不扫描整个集合的情况下仅获取与所需项目匹配的那些文档:

collection.find({ "cupboards.item": "Sausage" })

您仍然可以在代码中“过滤”数组内容以找到您的匹配项,或者使用.aggregate() 执行类似的操作:

collection.aggregate([
    { "$match": { "cupboards.item": "Sausage" }},
    { "$unwind": "$cupboards" },
    { "$match": { "cupboards.item": "Sausage" }},
    { "$group": {
        "_id": "$_id",
        "cupboards": { 
            "$push": {
                "cupboard":"$cupboards.cupboard",
                "item": "$cupboards.item",
                "qty": "$cupboards.qty"
            }
        },
        "totalQty": { "$sum": "$cupboards.qty" }
    }}
])

产生与上述相同的基本结果,但更简单,速度更快:

{
    "_id" : ObjectId("5563e80065536add0d04619c"),
    "cupboards" : [
            {
                    "cupboard" : 2,
                    "item" : "Sausage",
                    "qty" : 3
            }
    ],
    "totalQty" : 3
}

所以这里的真正意义是“避免”在存储的文档中使用实际上是“数据点”的东西作为“键名”。键名未编入索引,因此无法进行有效搜索。 “数据”可以被索引,这是搜索的有效方法。


关于修订结构的注释以供参考。除了这里的一般“大修”之外,一个很大的区别是省略了文档中最初显示的“总计”字段。遗漏的一个重要原因是,即使在原始形式中,在添加和更新其他密钥的同时维护这样的“总数”也是一个可怕的前提。

基本上没有办法在不加载/检查/重写“整个”文档的情况下自动更新所有值并保持“总计”同步。任何形式的单一“快速”更新都是不可能的。

虽然在文档和组件中维护“总计”通常是一个“高尚的想法”,但对于多个“总计”而言,开销是相当可观的。因此,在大多数情况下,“快速写入”通常比读取所需的额外计算开销更受欢迎。因此,通常最好遵循该模型,除非您发现在您的特定情况下,您可以忍受处理多个更新以获得更好的读取操作性能的额外成本。

【讨论】:

  • 感谢@user3561036 提供如此详细的回答。我确实想知道文档结构是否需要一些更改。我想需要一些时间才能停止将现实世界模型(食品是橱柜的子项)考虑到一个更扁平的数据模型,其中位置是食品的一个属性。
  • @ChemLynx 我认为我在这里提出的主要观点是“逻辑”方法是“橱柜”确实是可能的橱柜的“集合”或“阵列”,其内容确实是另一个收藏。将单独的“橱柜”作为主对象的不同属性并不是一个干净的对象结构,因为属性本​​身在对象之间是不同的。 “数组”没有嵌套并且“橱柜”是橱柜中项目的“属性”的唯一原因是 MongoDB 不能直观地处理嵌套数组以进行更新。使显示的表单最有效。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-07-25
  • 2016-08-25
  • 2016-04-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-29
相关资源
最近更新 更多