公告介绍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阶段的一部分,我们计算每个group中所有amount字段的和。

除了$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);

将上面的每个代码片段运行在一个包含 5,000 个文档的集合上,产生了以下计时结果

聚合花费了 103.46 毫秒。

手动通过游标迭代花费了 881.32 毫秒。

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

结论

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

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

如果您需要书籍,您始终可以参考《实用 MongoDB 聚合》

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

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

常见问题解答

什么是聚合数据?

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

什么是数据聚合?

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

什么是聚合器?

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

准备好开始了吗?

启动新的集群或无缝迁移到 MongoDB Atlas。