在node.js mongodb中求和嵌套数组

时间:2018-11-05 20:16:41

标签: mongodb mongodb-query aggregation-framework

我在mongodb中有一个看起来像这样的模式。

first_level:[{
    first_item  : String,
    second_level:[{
        second_item: String,
        third_level:[{
            third_item :String,
            forth_level :[{//4th level
                    price               : Number, // 5th level
                    sales_date          : Date, 
                    quantity_sold       : Number
                }]
        }]
    }]
}]

1)。我想根据中的匹配条件添加quantity_sold first_item,second_item,third_item和sales_date

2)。我还想查找特定日期所有已售出数量的平均值。

3)。我还想查找在特定日期所有已售出数量的平均值,并有相应的价格。

我对如何解决这个问题感到非常困惑,我来自sql  背景,所以这很令人困惑

1 个答案:

答案 0 :(得分:1)

让我们从一个基本的免责声明开始,在这里Find in Double Nested Array MongoDB已经回答了解决问题的主体。 “记录” Double 也适用于 Triple Quadrupal ANY 嵌套级别,基本上相同的原则总是

任何答案的另一个要点也是不要嵌套数组,因为该答案也对此进行了解释(并且我已经多次重复了 次) ),无论您出于什么原因而想“嵌套” ,实际上都没有给您带来您所认为的好处。实际上,“嵌套” 确实使生活变得更加困难。

嵌套问题

从“关系”模型对数据结构进行任何转换的主要误解几乎总是被解释为每个关联模型的“添加嵌套数组级别” 。您在此处呈现的内容也不例外,因为它很可能是“ normalized” ,因此每个子数组都包含与其父项相关的项。

MongoDB是基于“文档”的数据库,因此它几乎可以使您执行此操作,或者实际上可以执行您基本上想要的任何数据结构内容。但是,这并不意味着以这种形式的数据易于使用,或者对于实际用途而言确实是实用的。

让我们用一些实际数据填写该模式以演示:

{
  "_id": 1,
  "first_level": [
    {
      "first_item": "A",
      "second_level": [
        {
          "second_item": "A",
          "third_level": [
            { 
              "third_item": "A",
              "forth_level": [
                { 
                  "price": 1,
                  "sales_date": new Date("2018-10-31"),
                  "quantity": 1
                },
                { 
                  "price": 1,
                  "sales_date": new Date("2018-11-01"),
                  "quantity": 1
                },
                { 
                  "price": 1,
                  "sales_date": new Date("2018-11-02"),
                  "quantity": 1
                },
              ]
            },
            { 
              "third_item": "B",
              "forth_level": [
                { 
                  "price": 1,
                  "sales_date": new Date("2018-10-31"),
                  "quantity": 1
                },
              ]
            }
          ]
        },
        {
          "second_item": "A",
          "third_level": [
            { 
              "third_item": "B",
              "forth_level": [
                { 
                  "price": 1,
                  "sales_date": new Date("2018-11-03"),
                  "quantity": 1
                },
              ]
            }
          ]
        }
      ]
    },
    {
      "first_item": "A",
      "second_level": [
        {
          "second_item": "B",
          "third_level": [
            { 
              "third_item": "A",
              "forth_level": [
                { 
                  "price": 1,
                  "sales_date": new Date("2018-11-03"),
                  "quantity": 1
                },
              ]
            }
          ]
        }
      ]
    }
  ]
},
{
  "_id": 2,
  "first_level": [
    {
      "first_item": "A",
      "second_level": [
        {
          "second_item": "A",
          "third_level": [
            { 
              "third_item": "A",
              "forth_level": [
                { 
                  "price": 2,
                  "sales_date": new Date("2018-11-03"),
                  "quantity": 1
                },
                { 
                  "price": 1,
                  "sales_date": new Date("2018-10-31"),
                  "quantity": 1
                },
                { 
                  "price": 1,
                  "sales_date": new Date("2018-11-03"),
                  "quantity": 1
                }
              ]
            }
          ]
        }
      ]
    }
  ]
},
{
  "_id": 3,
  "first_level": [
    {
      "first_item": "A",
      "second_level": [
        {
          "second_item": "B",
          "third_level": [
            { 
              "third_item": "A",
              "forth_level": [
                { 
                  "price": 1,
                  "sales_date": new Date("2018-11-03"),
                  "quantity": 1
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

与问题的结构“有点”不同,但出于演示目的,它具有我们需要研究的内容。主要在文档中有一个数组,其中的项带有子数组,而子数组又具有子数组,依此类推。当然,这里的“规范化” 是通过每个“级别”上的标识符作为“项目类型”或您实际拥有的任何东西来实现的。

核心问题是您只想从这些嵌套数组中获取“一些”数据,而MongoDB确实只想返回“文档”,这意味着您需要进行一些操作才能达到这些匹配条件“子项目”。

即使在“正确” 的问题上,选择与所有这些“子条件”都匹配的文档也需要大量使用$elemMatch,以便获得正确的条件组合每个级别的数组元素。由于需要"Dot Notation",因此无法直接使用multiple conditions。如果没有$elemMatch语句,您将不会获得确切的“组合”,而只会获得在 any 数组元素上条件为真的文档。

实际上,“过滤出数组内容” 实际上是其他区别的一部分:

db.collection.aggregate([
  { "$match": {
    "first_level": {
      "$elemMatch": {
        "first_item": "A",
        "second_level": {
          "$elemMatch": {
            "second_item": "A",
            "third_level": {
              "$elemMatch": {
                "third_item": "A",
                "forth_level": {
                  "$elemMatch": {
                    "sales_date": {
                      "$gte": new Date("2018-11-01"),
                      "$lt": new Date("2018-12-01")
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }},
  { "$addFields": {
    "first_level": {
      "$filter": {
        "input": {
          "$map": {
            "input": "$first_level",
            "in": {
              "first_item": "$$this.first_item",
              "second_level": {
                "$filter": {
                  "input": {
                    "$map": {
                      "input": "$$this.second_level",
                      "in": {
                        "second_item": "$$this.second_item",
                        "third_level": {
                          "$filter": {
                            "input": {
                              "$map": {
                                "input": "$$this.third_level",
                                 "in": {
                                   "third_item": "$$this.third_item",
                                   "forth_level": {
                                     "$filter": {
                                       "input": "$$this.forth_level",
                                       "cond": {
                                         "$and": [
                                           { "$gte": [ "$$this.sales_date", new Date("2018-11-01") ] },
                                           { "$lt": [ "$$this.sales_date", new Date("2018-12-01") ] }
                                         ]
                                       }
                                     }
                                   }
                                 } 
                              }
                            },
                            "cond": {
                              "$and": [
                                { "$eq": [ "$$this.third_item", "A" ] },
                                { "$gt": [ { "$size": "$$this.forth_level" }, 0 ] }
                              ]
                            }
                          }
                        }
                      }
                    }
                  },
                  "cond": {
                    "$and": [
                      { "$eq": [ "$$this.second_item", "A" ] },
                      { "$gt": [ { "$size": "$$this.third_level" }, 0 ] }
                    ]
                  }
                }
              }
            }
          }
        },
        "cond": {
          "$and": [
            { "$eq": [ "$$this.first_item", "A" ] },
            { "$gt": [ { "$size": "$$this.second_level" }, 0 ] }
          ]
        } 
      }
    }
  }},
  { "$unwind": "$first_level" },
  { "$unwind": "$first_level.second_level" },
  { "$unwind": "$first_level.second_level.third_level" },
  { "$unwind": "$first_level.second_level.third_level.forth_level" },
  { "$group": {
    "_id": {
      "date": "$first_level.second_level.third_level.forth_level.sales_date",
      "price": "$first_level.second_level.third_level.forth_level.price",
    },
    "quantity_sold": {
      "$avg": "$first_level.second_level.third_level.forth_level.quantity"
    } 
  }},
  { "$group": {
    "_id": "$_id.date",
    "prices": {
      "$push": {
        "price": "$_id.price",
        "quanity_sold": "$quantity_sold"
      }
    },
    "quanity_sold": { "$avg": "$quantity_sold" }
  }}
])

最好将其描述为“混乱”和“涉及”。我们最初使用$elemMatch进行文档选择的查询不仅耗费了很多精力,而且还为每个数组级别进行了后续的$filter$map处理。如前所述,无论实际上有多少个级别,这都是这种模式。

您可以交替执行$unwind$match的组合,而不是对数组进行过滤,但这会在删除不需要的内容之前给$unwind造成额外的开销,因此在现代通常,最好先从数组中$filter开始使用MongoDB版本。

这里的终点是您想要$group通过数组中实际存在的元素,因此最终您需要在此之前$unwind数组的每个级别。

然后,通常使用sales_dateprice属性进行 first 累加,然后将后续阶段添加到$push您要在每个日期内累积平均值的不同price值,以为单位。

注意:日期的实际处理方式在实际使用中可能会有所不同,具体取决于您存储日期的粒度。在此示例中,日期都已经四舍五入到每个“天”的开始。如果您实际上需要累积实际的“日期时间”值,那么您可能真的想要这样或类似的构造:

{ "$group": {
  "_id": {
    "date": {
      "$dateFromParts": {
        "year": { "$year": "$first_level.second_level.third_level.forth_level.sales_date" },
        "month": { "$month": "$first_level.second_level.third_level.forth_level.sales_date" },
        "day": { "$dayOfMonth": "$first_level.second_level.third_level.forth_level.sales_date" }
      }
    }.
    "price": "$first_level.second_level.third_level.forth_level.price"
  }
  ...
}}

使用$dateFromParts和其他date aggregation operators提取“日期”信息,并以该形式显示日期以进行累积。

开始非正规化

从上面的“混乱”中应该清楚的是,使用嵌套数组并不是一件容易的事。在MongoDB 3.6之前的版本中,通常甚至无法原子地更新这样的结构,即使您甚至从未更新过它们或基本上没有替换整个数组,它们仍然不容易查询。这就是您所显示的。

必须在父文档中具有数组内容的位置,通常建议“拼合” “非规范化” 这样的结构。这可能与关系思维相反,但实际上出于性能原因,这是处理此类数据的最佳方法:

{
  "_id": 1,
  "data": [
    {
      "first_item": "A",
      "second_item": "A",
      "third_item": "A",
      "price": 1,
      "sales_date": new Date("2018-10-31"),
      "quantity": 1
    },

    { 
      "first_item": "A",
      "second_item": "A",
      "third_item": "A",
      "price": 1,
      "sales_date": new Date("2018-11-01"),
      "quantity": 1
    },
    { 
      "first_item": "A",
      "second_item": "A",
      "third_item": "A",
      "price": 1,
      "sales_date": new Date("2018-11-02"),
      "quantity": 1
    },
    { 
      "first_item": "A",
      "second_item": "A",
      "third_item": "B",
      "price": 1,
      "sales_date": new Date("2018-10-31"),
      "quantity": 1
    },
    {
     "first_item": "A",
     "second_item": "A",
     "third_item": "B",
     "price": 1,
     "sales_date": new Date("2018-11-03"),
     "quantity": 1
    },
    {
      "first_item": "A",
      "second_item": "B",
      "third_item": "A",
      "price": 1,
      "sales_date": new Date("2018-11-03"),
      "quantity": 1
     },
  ]
},
{
  "_id": 2,
  "data": [
    {
      "first_item": "A",
      "second_item": "A",
      "third_item": "A",
      "price": 2,
      "sales_date": new Date("2018-11-03"),
      "quantity": 1
    },
    { 
      "first_item": "A",
      "second_item": "A",
      "third_item": "A",
      "price": 1,
      "sales_date": new Date("2018-10-31"),
      "quantity": 1
    },
    { 
      "first_item": "A",
      "second_item": "A",
      "third_item": "A",
      "price": 1,
      "sales_date": new Date("2018-11-03"),
      "quantity": 1
    }
  ]
},
{
  "_id": 3,
  "data": [
    {
      "first_item": "A",
      "second_item": "B",
      "third_item": "A",
      "price": 1,
      "sales_date": new Date("2018-11-03"),
      "quantity": 1
     }
  ]
}

与原始显示的所有数据相同,但实际上我们只是将所有内容放入每个父文档中的单个扁平化数组中,而不是嵌套。当然,这意味着对各个数据点进行重复,但是查询复杂性和性能上的差异应该是显而易见的:

db.collection.aggregate([
  { "$match": {
    "data": {
      "$elemMatch": {
        "first_item": "A",
        "second_item": "A",
        "third_item": "A",
        "sales_date": {
          "$gte": new Date("2018-11-01"),
          "$lt": new Date("2018-12-01")
        }
      }
    }
  }},
  { "$addFields": {
    "data": {
      "$filter": {
        "input": "$data",
         "cond": {
           "$and": [
             { "$eq": [ "$$this.first_item", "A" ] },
             { "$eq": [ "$$this.second_item", "A" ] },
             { "$eq": [ "$$this.third_item", "A" ] },
             { "$gte": [ "$$this.sales_date", new Date("2018-11-01") ] },
             { "$lt": [ "$$this.sales_date", new Date("2018-12-01") ] }
           ]
         }
      }
    }
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": {
      "date": "$data.sales_date",
      "price": "$data.price",
    },
    "quantity_sold": { "$avg": "$data.quantity" }
  }},
  { "$group": {
    "_id": "$_id.date",
    "prices": {
      "$push": {
        "price": "$_id.price",
        "quantity_sold": "$quantity_sold"
      }
    },
    "quantity_sold": { "$avg": "$quantity_sold" }
  }}
])

现在,与嵌套$elemMatch调用(和类似$filter表达式)一样,所有内容都更加清晰易读,并且处理起来非常简单。另一个优点是,您实际上甚至可以为查询中使用的数组索引元素的键。这是嵌套模型的一个约束,在该模型中,MongoDB根本不允许在数组中的 array键上使用"Multikey indexing" 。对于单个数组,这是允许的,可以用来提高性能。

“数组内容过滤” 之后的所有内容都完全相同,不同的是它只是像"data.sales_date"这样的路径名,而不是来自{以前的结构。

何时不嵌入

最后,另一个大的误解是,所有关系需要翻译为嵌入数组中。这确实不是MongoDB的意图,并且只意味着在一次文档检索而不是“联接”的情况下,将“相关”数据保留在同一文档中的数组中。

此处经典的“订单/详细信息”模型通常适用于在现代世界中要在同一“屏幕”上显示“订单”的“标题”的详细信息,例如客户地址,订单总数等。 “订单”上不同订单项的详细信息。

在RDBMS诞生之初,典型的80字符乘25行的屏幕仅在一个屏幕上具有这样的“标题”信息,然后所购买的所有产品的详细信息行都在另一个屏幕上。因此自然有某种程度的常识将它们存储在单独的表中。随着人们对此类“屏幕”的更多了解,您通常希望看到整个内容,或者至少要看到“标头”以及此类“订单”的头几行。

因此,将这种安排放入数组很有意义,因为MongoDB立即返回一个包含相关数据的“文档”。不需要对单独的渲染屏幕进行单独的请求,也不需要对此类数据进行“联接”,因为它已经是“预先联接”的。

考虑是否需要-也称为“完全”非规范化

因此,如果您几乎大部分时间都不知道实际上对处理此类数组中的大多数数据感兴趣,那么将所有这些信息简单地单独放入一个集合中通常更有意义。另一个属性以识别“父母”,如果偶尔需要这种“加入”:

"first_level.second_level.third_level.forth_level.sales_date"

还是相同的数据,但是这一次是在完全独立的文档中,最好是在实际需要其他用途的情况下引用父级。请注意,这里的聚合完全不与父数据相关,并且还可以通过简单地将其存储在单独的集合中来明确带来额外性能和降低的复杂性的地方:

{
  "_id": 1,
  "parent_id": 1,
  "first_item": "A",
  "second_item": "A",
  "third_item": "A",
  "price": 1,
  "sales_date": new Date("2018-10-31"),
  "quantity": 1
},
{ 
  "_id": 2,
  "parent_id": 1,
  "first_item": "A",
  "second_item": "A",
  "third_item": "A",
  "price": 1,
  "sales_date": new Date("2018-11-01"),
  "quantity": 1
},
{ 
  "_id": 3,
  "parent_id": 1,
  "first_item": "A",
  "second_item": "A",
  "third_item": "A",
  "price": 1,
  "sales_date": new Date("2018-11-02"),
  "quantity": 1
},
{ 
  "_id": 4,
  "parent_id": 1,
  "first_item": "A",
  "second_item": "A",
  "third_item": "B",
  "price": 1,
  "sales_date": new Date("2018-10-31"),
  "quantity": 1
},
{
  "_id": 5,
  "parent_id": 1,
  "first_item": "A",
  "second_item": "A",
  "third_item": "B",
  "price": 1,
  "sales_date": new Date("2018-11-03"),
  "quantity": 1
},
{
  "_id": 6,
  "parent_id": 1,
  "first_item": "A",
  "second_item": "B",
  "third_item": "A",
  "price": 1,
  "sales_date": new Date("2018-11-03"),
  "quantity": 1
},
{
  "_id": 7,
  "parent_id": 2,
  "first_item": "A",
  "second_item": "A",
  "third_item": "A",
  "price": 2,
  "sales_date": new Date("2018-11-03"),
  "quantity": 1
},
{ 
  "_id": 8,
  "parent_id": 2,
  "first_item": "A",
  "second_item": "A",
  "third_item": "A",
  "price": 1,
  "sales_date": new Date("2018-10-31"),
  "quantity": 1
},
{ 
  "_id": 9,
  "parent_id": 2,
  "first_item": "A",
  "second_item": "A",
  "third_item": "A",
  "price": 1,
  "sales_date": new Date("2018-11-03"),
  "quantity": 1
},
{
  "_id": 10,
  "parent_id": 3,
  "first_item": "A",
  "second_item": "B",
  "third_item": "A",
  "price": 1,
  "sales_date": new Date("2018-11-03"),
  "quantity": 1
}

由于所有内容都已经是文档,因此无需“过滤数组” 或具有其他任何复杂性。您要做的就是选择匹配的文档并汇总结果,并且始终执行与前两个步骤完全相同的最终步骤。

为了仅获得最终结果,它的效果远优于以上任何一种方法。该查询实际上只涉及“详细”数据,因此,最佳做法是将细节与父级完全分开,因为它总是会提供最佳的性能优势。

这里的总体要点是,应用程序其余部分 Never 的实际访问模式需要返回整个数组内容,因此无论如何都不应将其嵌入。似乎大多数“写入”操作同样也永远不需要接触相关的父对象,这是该方法起作用与否的另一个决定因素。

结论

通常的信息是,作为一般规则,您永远不应嵌套数组。最多应该在相关的父文档中保留一个带有部分非规范化数据的“奇异”数组,并且在其余访问模式实际上根本不使用父级和子级的情况下,则应该真正分离数据。

“大”变化是,您认为规范化数据实际上很好的所有原因,最终成为了此类嵌入式文档系统的敌人。避免使用“联接”总是好的,但是创建复杂的嵌套结构以显示“联接”数据的外观也永远不会对您有所帮助。

处理“思维”就是规范化的成本通常最终要花费额外的存储空间以及维护最终存储中重复和非规范化数据的费用。

还要注意,以上所有形式都返回相同的结果集。简洁的示例数据仅包含单个项目,或者最多在有多个价格点的情况下,“平均”仍为db.collection.aggregate([ { "$match": { "first_item": "A", "second_item": "A", "third_item": "A", "sales_date": { "$gte": new Date("2018-11-01"), "$lt": new Date("2018-12-01") } }}, { "$group": { "_id": { "date": "$sales_date", "price": "$price" }, "quantity_sold": { "$avg": "$quantity" } }}, { "$group": { "_id": "$_id.date", "prices": { "$push": { "price": "$_id.price", "quantity_sold": "$quantity_sold" } }, "quantity_sold": { "$avg": "$quantity_sold" } }} ]) ,这是很不错的,因为无论如何这就是所有值。但是解释这一点的内容已经很长了,因此实际上只是“通过示例”:

1