公告推出MongoDB 8.0,史上最快的MongoDB! 阅读更多 >>推出MongoDB 8.0,史上最快的MongoDB! >>

在使用 MongoDB 中的数据时,您可能需要快速执行复杂的操作,包含多个操作阶段以收集项目的指标。生成报告和显示有用的元数据只是 MongoDB 聚合操作可以证明非常有用、强大和灵活的两个主要用例。


目录

什么是聚合?

在编程中,我们经常对一个项目集合执行一系列操作。以下是一个 JavaScript 示例

let numbers = [{val: 1}, {val: 2}, {val: 3}, {val: 4}];
numbers = numbers
    .map(obj => obj.val) // [1, 2, 3, 4]
    .reduce((prev, curr) => prev + curr, 0) // 10

在这个示例中,我们对数字数组执行了两个操作

  • 首先,map():我们将对象转换为它们的数值。

  • 其次,reduce():我们将输出合并为单个数字——数字的总和。

聚合操作处理数据记录并返回计算结果。

我们不仅可以在客户端使用 JavaScript 聚合数据,还可以使用 MongoDB 在服务器上对存储在数据库中的集合执行操作,然后在将结果返回给客户端之前执行这些操作。

单用途聚合

MongoDB 提供了两种执行聚合的方法。最简单的是 单用途聚合

单用途聚合操作是一系列辅助方法,应用于集合以计算结果。这些辅助方法允许简单访问常见的聚合过程。

其中提供的方法有


让我们使用一个名为“sales”的集合,该集合存储购买信息

{
    _id: 5bd761dcae323e45a93ccfea,
    saleDate: 2017-06-22T09:54:14.185+00:00,
    items: [
      {
        "name": "printer paper",
        "price": 17.3,
        // ...
      },
    ],
    storeLocation: "Denver",
    customer: {
        age: 40,
        satisfaction: 5,
       // ...
    },
    couponUsed: false,
    purchaseMethod: "In store"
}

如果我们想确定不同的购买方法,我们可以在 Node.js 脚本中调用 distinct()

const collection = client.db("sample_supplies").collection("sales");
const distinctPurchaseMethods = await collection.distinct("purchaseMethod");

distinctPurchaseMethods 是一个数组,包含“sales”集合中存储的所有唯一的购买方法。

["In store", "Online", "Phone"]

如果我们想查看总共完成了多少次销售,我们可以运行

const totalNumberOfSales = await collection.countDocuments();

countDocuments() 函数会汇总集合中的文档总数,并返回这个数字供我们使用。如果我们需要基于上述辅助方法之一对集合进行汇总,则可以使用单用途汇总。

我该如何使用 MongoDB 聚合数据?

当您需要进行更复杂的汇总时,可以使用 MongoDB 汇总管道(查看我们的 更详细的教程)。汇总管道是一系列阶段,可以对文档进行查询、筛选、修改和处理。它是一个图灵完备的实现,可以用作(相当低效的)编程语言。

在我们深入研究代码之前,让我们了解汇总管道本身做什么以及它是如何工作的。在汇总管道中,您在一个“阶段”中列出一系列指令。对于每个定义的阶段,MongoDB 按顺序依次执行它们,以给出您可以使用的最终输出。让我们看看 aggregate 命令的一个示例用法

collection.aggregate([
   { $match: { status: "A" } },
   { $group: { _id: "$cust_id", total: { $sum: "$amount" } } }
])

在这个示例中,我们运行了一个名为 $match 的阶段。一旦该阶段运行,它将输出传递给 $group 阶段。

$match 允许我们取一个项目集合,并且只接收具有 status 值为 A 的项目。

之后,我们使用 $group 来根据 cust_id 字段对文档进行分组。作为 $group 阶段的一部分,我们计算每个 groupamount 字段的总和。

除了 $sum 之外,MongoDB 还提供了许多其他运算符,您可以在汇总中使用。

聚合管道方法

让我们看看我们之前使用的同一销售集合,例如。下面是这个集合中的一个文档

{
  "_id": "5bd761dcae323e45a93ccffb",
  "items": [
    {
      "name": "printer paper",
      "tags": [
        "office"
      ],
      "price": 17.3,
      "quantity": 1
    },
    {
      "name": "binder",
      "tags": [
        "school"
      ],
      "price": 23.36,
      "quantity": 3
    }
  ],
  "couponUsed": false,
  "purchaseMethod": "In store"
}

鉴于我们有一个每个交易的已售物品列表,我们可以使用汇总管道计算所有购买物品的平均成本。

我们可以先使用 $set 在每个文档中添加一个字段。结合使用 $sum,我们能够在每个文档中添加一个名为 itemsTotal 的字段。

{ '$set': { 'itemsTotal': { '$sum': '$items.price' } } }

现在管道中的文档已经转换,包含一个名为 itemsTotal 的新属性。

[
 {
   "_id": "5bd761dcae323e45a93ccffb",
   "items": [
     // ...
   ],
   "itemsTotal": 360.33,
   "couponUsed": false,
   "purchaseMethod": "In store"
 }
]

接下来,我们可以将 $set 阶段的文档传递给一个 $group 阶段。在 $group 中,我们可以使用 $avg 运算符来计算所有文档的平均交易价格。

{ '$group': {
    'averageTransactionPrice': { '$avg': '$itemsTotal' },
    '_id': null
} }

完成此阶段后,我们将剩下单个文档,提供最终输出

[{
 "_id": null,
 "averageTransactionPrice": 620.511328
}]

输出告诉我们所有交易的平均价格是 $620.511328。


这个汇总的最终代码在 Node.js 中看起来可能如下所示

const aggCursor = collection.aggregate([
       { '$set': { 'itemsTotal': { '$sum': '$items.price' } } },
       { '$group': { 'averageTransactionPrice': { '$avg': '$itemsTotal' }, '_id': null } }
]);

findAndModify 命令

不仅仅是 aggregate 函数可以利用汇总语法的好处。从 MongoDB 4.2 开始,许多命令支持使用汇总管道更新文档。

让我们看看这样一个命令: updateMany

我们可能想将itemsTotal作为一个永久字段添加到我们的文档中,以便在这些操作中更快地读取。

让我们使用updateMany和聚合管道来添加一个名为itemsTotal的新字段。

await collection.updateMany({}, [
   { '$set': { 'itemsTotal': { '$sum': '$items.price' } } },
])

如您所见,我们已经从上一个示例中重新使用了$set阶段。现在,如果我们检查我们的集合,我们可以在每个文档中看到这个新字段。

  {
    "_id": "5bd761dcae323e45a93ccffb",
    "items": [
      {
        "name": "printer paper",
        "price": 17.3,
        // ...
      }
    ],
    "itemsTotal": 360.33,
    "couponUsed": false,
    "purchaseMethod": "In store"
  }

MongoDB 聚合速度有多快?

虽然我们的示例在适当的上下文中是真实和有用的,但它们相对较小。我们只使用了聚合管道中的两个阶段。

但这并不是聚合管道的全部潜力——远非如此。

聚合管道允许您执行复杂操作,这将允许您对集合的任何范围有深入了解。有几十个管道阶段以及广泛的操作,您可以使用这些操作构建您所想象的任何数据分析。

虽然聚合管道非常强大,但与我们在自己身上执行这些类型分析相比,它的性能如何呢?

让我们使用之前的聚合查询示例

const { performance } = require('perf_hooks');
const startTime = performance.now();
const totalAvg = collection.aggregate([
   {
       '$set': {
           'itemsTotal': {
               '$sum': '$items.price'
           }
       }
   }, {
       '$group': {
           '_id': null,
           'total': {
               '$avg': '$itemsTotal'
           }
       }
   }
]);
await totalAvg.toArray()
const endTime = performance.now();
console.log("Aggregation took:", endTime - startTime);

在我们的MongoDB示例中,我们使用了两个阶段:一个用于添加一个itemsTotal字段,另一个用于计算所有文档中itemsTotal的平均值。

为了在Node.js中匹配这种行为,我们将使用Array.prototype.mapArray.prototype.reduce作为相关替代品

const { performance } = require('perf_hooks');
const startTime = performance.now();
const allItems = await collection.find({}).toArray();
const itemsSum = allItems
   .map(item => {
       item.itemsTotal = item.items.reduce((p, c) => p + parseFloat(c.price), 0);
       return item;
   })
   .reduce((p, item) => {
       return p + item.itemsTotal;
   }, 0);
const itemAvg = itemsSum / allItems.length;

const endTime = performance.now();
console.log("Manual took:", endTime - startTime);

在5000个文档的集合上运行上述每个代码片段产生了以下计时结果

聚合耗时103.46毫秒。

手动迭代游标耗时881.32毫秒。

这个差异超过8.5倍!虽然这里的差异可能是毫秒级的,但我们使用的是一个极其小的集合大小。不难想象,如果我们的集合包含一百万或更多的文档,时间差异将非常巨大。记住,聚合管道在MongoDB服务器上运行,可以在运行前进行优化,而当你迭代游标以在客户端处理数据时,你会因为从该游标中检索数据页而增加大量的延迟。最佳方法可能是两者的结合。

结论

聚合管道使我们能够在这个示例中做很多事情,从确定集合中有多少文档,到能够对该集合执行复杂操作,再到收集多个数据点的平均值并修改数据库中的集合。

虽然我们今天学到了很多关于聚合管道的知识,但这只是开始。聚合管道非常强大,包含许多深入元素。如果您想了解更多关于管道及其用法的信息,您可以阅读我们的文档

如果您需要一本书,您始终可以参考Practical MongoDB Aggregations

MongoDB Atlas 还允许您通过 聚合管道构建器 创建和运行聚合管道。企业高级和本地用户也可以使用 Compass。

这使得您可以将完成的管道导出为支持的驱动语言之一。

常见问题解答

什么是聚合数据?

聚合数据是通过将来自多个来源的数值或非数值数据组合而形成的高级数据。

什么是数据聚合?

数据聚合是将大量数据进行高级审查的过程。

什么是聚合器?

聚合器是从不同来源收集信息并在一个地方整合的组织、网站或软件应用程序。

准备好开始了吗?

启动新集群或零停机迁移到 MongoDB Atlas。