在使用 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 按顺序依次执行它们,以给出您可以使用的最终输出。让我们看看 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 } }
]);
不仅仅是 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"
}
虽然我们的示例在适当的上下文中是真实和有用的,但它们相对较小。我们只使用了聚合管道中的两个阶段。
但这并不是聚合管道的全部潜力——远非如此。
聚合管道允许您执行复杂操作,这将允许您对集合的任何范围有深入了解。有几十个管道阶段以及广泛的操作,您可以使用这些操作构建您所想象的任何数据分析。
虽然聚合管道非常强大,但与我们在自己身上执行这些类型分析相比,它的性能如何呢?
让我们使用之前的聚合查询示例
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.map
和Array.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。
这使得您可以将完成的管道导出为支持的驱动语言之一。
聚合数据是通过将来自多个来源的数值或非数值数据组合而形成的高级数据。
数据聚合是将大量数据进行高级审查的过程。
聚合器是从不同来源收集信息并在一个地方整合的组织、网站或软件应用程序。