当在 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);
将上面的每个代码片段运行在一个包含 5,000 个文档的集合上,产生了以下计时结果
聚合花费了 103.46 毫秒。
手动通过游标迭代花费了 881.32 毫秒。
这个差异超过 8.5 倍!虽然这里的差异可能是毫秒级别的,但我们使用的是一个非常小的集合大小。不难想象,如果我们的集合包含一百万或更多的文档,时间差异将多么巨大。请记住,聚合管道在 MongoDB 服务器上运行,可以在运行之前进行优化,而当你通过游标在客户端迭代处理数据时,由于从该游标获取数据页,会增加很多延迟。最佳方法可能是两者的结合。
聚合管道使我们能够在本例中完成很多事情,从确定集合中有多少文档,能够对该集合执行复杂操作,到跨多个数据点收集平均值并修改数据库中的集合。
虽然我们今天学到了很多关于聚合管道的知识,但这只是开始。聚合管道非常强大,包含许多深入的内容。如果您想了解更多关于管道及其用法的信息,您可以阅读我们的文档。
如果您需要书籍,您始终可以参考《实用 MongoDB 聚合》。
MongoDB Atlas 还允许您通过聚合管道构建器创建和运行聚合管道。企业高级和本地用户也可以使用 Compass。
这使得您可以将完成的管道导出为支持的驱动程序语言之一。
聚合数据是通过组合多个来源的数值或非数值数据形成的高级数据。
数据聚合是将大量数据组合起来进行高级审查的过程。
聚合器是组织、网站或软件应用程序,它们从不同的来源收集信息并在一个地方进行整合。