我已经在之前的文章中介绍了 MongoDB 5.0 上的一些新功能:重新分片和时间序列集合。如果您错过了它们,请查看:
MongoDB 5.0 时间序列集合
MongoDB 5.0 中的重新分片
在本文中,我想介绍另一个新功能:窗口函数。
窗口函数在关系数据库中非常流行,它们允许在排序的文档中运行窗口,从而在窗口的每个步骤上产生计算。典型用例是计算滚动平均值、相关分数或累积总数。即使使用旧版本的 MongoDB 或使用窗口函数不可用的数据库,您也可以获得相同的结果。但这是以更复杂为代价的,因为通常需要多个查询,并且还需要在某个地方保存临时数据。
相反,窗口函数允许您运行单个查询并以更高效和优雅的方式获得预期结果。
让我们看看该功能在 MongoDB 5.0 上是如何工作的。
窗口函数
MongoDB 5.0 提供了一个新的聚合阶段 $setWindowFields。这是提供窗口功能功能的一种。
以下是阶段的语法:
{
$setWindowFields: {
partitionBy: <expression>,
sortBy: {
<sort field 1>: <sort order>,
<sort field 2>: <sort order>,
...,
<sort field n>: <sort order>
},
output: {
<output field 1>: {
<window operator>: <window operator parameters>,
window: {
documents: [ <lower boundary>, <upper boundary> ],
range: [ <lower boundary>, <upper boundary> ],
unit: <time unit>
}
},
<output field 2>: { ... },
...
<output field n>: { ... }
}
}
}
- partitionBy(可选):用于对文档进行分组的一些表达式。如果默认省略,则所有文档都被分组到一个分区中
- sortBy(在某些情况下需要):对文档进行排序。使用 $sort 语法
- 输出(必需):指定要附加到结果集的文档。基本上,这是提供窗口函数结果的参数
- window(可选):定义包含的窗口边界以及边界应如何用于计算窗口函数结果
好吧,这些定义可能看起来很神秘,但几个简单的例子将阐明如何使用它们。
测试数据集
我有一个运行MongoDB 5.0 的 Percona 服务器,我从意大利获得了一些关于 COVID-19 感染、住院和其他信息的公共数据。可通过以下链接获取每日和每个区域的数据:https 😕/github.com/pcm-dpc/COVID-19/tree/master/dati-regioni 。
我只加载了跨越 2021 年和 2022 年的几个月的数据。数据用意大利语标记,所以我创建了一个类似的缩减集合,只是为了满足本文的需要。
以下是文件样本:
> db.covid.find({"region":"Lombardia"}).sort({"date":1}).limit(5)
{ "_id" : ObjectId("62ab5f7d017d030e4cb314e9"), "region" : "Lombardia", "total_cases" : 884125, "date" : ISODate("2021-10-01T15:00:00Z") }
{ "_id" : ObjectId("62ab5f7d017d030e4cb314fe"), "region" : "Lombardia", "total_cases" : 884486, "date" : ISODate("2021-10-02T15:00:00Z") }
{ "_id" : ObjectId("62ab5f7d017d030e4cb31516"), "region" : "Lombardia", "total_cases" : 884814, "date" : ISODate("2021-10-03T15:00:00Z") }
{ "_id" : ObjectId("62ab5f7d017d030e4cb31529"), "region" : "Lombardia", "total_cases" : 884920, "date" : ISODate("2021-10-04T15:00:00Z") }
{ "_id" : ObjectId("62ab5f7d017d030e4cb3153d"), "region" : "Lombardia", "total_cases" : 885208, "date" : ISODate("2021-10-05T15:00:00Z") }
每份文件都包含从大流行开始到特定意大利地区的每日 COVID 感染总数。
计算每日新增病例
让我们创建我们的第一个窗口函数。
由于我们在集合中只有总病例数,因此我们想计算每天的新病例数。通过这种方式,我们可以了解大流行的状况是在恶化还是在改善。
您可以通过发出以下聚合管道来实现:
> db.covid.aggregate( [
{ $setWindowFields: {
partitionBy : "$region",
sortBy: { date: 1 },
output: {
previous: {
$push: "$total_cases",
window: {
range: [-1, -1],
unit: "day"
}
}
}
}
}
,
{ $unwind:"$previous"},
{ $addFields: {
new_cases: {
$subtract: ["$total_cases","$previous"]
}
}
},
{ $match: { "region": "Lombardia" } },
{ $project: { _id:0, region:1, date:1, new_cases: 1} }
] )
{ "region" : "Lombardia", "date" : ISODate("2021-10-02T15:00:00Z"), "new_cases" : 361 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-03T15:00:00Z"), "new_cases" : 328 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-04T15:00:00Z"), "new_cases" : 106 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-05T15:00:00Z"), "new_cases" : 288 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-06T15:00:00Z"), "new_cases" : 449 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-07T15:00:00Z"), "new_cases" : 295 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-08T15:00:00Z"), "new_cases" : 293 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-09T15:00:00Z"), "new_cases" : 284 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-10T15:00:00Z"), "new_cases" : 278 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-11T15:00:00Z"), "new_cases" : 87 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-12T15:00:00Z"), "new_cases" : 306 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-13T15:00:00Z"), "new_cases" : 307 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-14T15:00:00Z"), "new_cases" : 273 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-15T15:00:00Z"), "new_cases" : 288 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-16T15:00:00Z"), "new_cases" : 432 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-17T15:00:00Z"), "new_cases" : 297 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-18T15:00:00Z"), "new_cases" : 112 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-19T15:00:00Z"), "new_cases" : 412 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-20T15:00:00Z"), "new_cases" : 457 }
{ "region" : "Lombardia", "date" : ISODate("2021-10-21T15:00:00Z"), "new_cases" : 383 }
管道还包含使输出更具可读性的阶段。无论如何,让我们关注 $setWindowFields。
在第一阶段,我们定义窗口函数,以便为每个文档创建一个包含前一天的总病例数的新字段。该字段显然被命名为previous。
然后我们将在接下来的阶段使用这些信息来简单地计算“今天”和“昨天”的总病例数之间的差异。然后我们得到每天的增长。
看看窗口函数是如何创建的。我们使用 $push 用 total_cases 的值填充新字段。在窗口文档中,我们将范围定义为[-1,-1]。这些数字代表窗口的下边界和上边界,它们都对应于窗口中的前一个 (-1) 文档。它只涉及一个文件:昨天。在这种情况下,sortBy 的使用是相关的,因为它告诉 MongoDB 以哪个顺序考虑窗口中的文档。将范围定义为 [-1,-1] 以获取昨天的数据的技巧是可能的,因为文档已正确排序。
计算移动平均线
现在让我们计算移动平均线。我们将考虑上周的数据来计算每日新增病例的平均值。这种参数在大流行高峰期非常流行,引发了围绕预测的大量讨论并解决了政府的决定。嗯,这是一个简化。还有其他相关参数,但移动平均线就是其中之一。
要计算移动平均线,我们需要在前面的示例中计算的每日新增病例。我们可以以不同的方式重用这些值,例如在前面的管道中添加另一个“$setWindowField”阶段,在现有文档上添加 new_cases 字段,或者像我一样使用 $out 阶段为简单起见创建另一个集合:
> db.covid.aggregate( [ { $setWindowFields: { partitionBy : "$region", sortBy: { date: 1 }, output: { previous: { $push: "$total_cases", window: { range: [-1, -1], unit: "day" } } } } }, { $unwind:"$previous"}, { $addFields: { new_cases: { $subtract: ["$total_cases","$previous"] } } }, { $project: { region:1, date:1, new_cases: 1} }, { $out: "covid_daily" } ] )
现在我们可以计算 covid_daily 集合的移动平均值。让我们使用以下聚合来完成:
> db.covid_daily.aggregate([
{ $setWindowFields: {
partitionBy : "$region",
sortBy : { date: 1 },
output: {
moving_average: {
$avg: "$new_cases",
window: {
range: [-6, 0],
unit: "day"
}
}
}
}
},
{ $project: { _id:0 } }
])
{ "region" : "Abruzzo", "date" : ISODate("2021-10-02T15:00:00Z"), "new_cases" : 49, "moving_average" : 49 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-03T15:00:00Z"), "new_cases" : 36, "moving_average" : 42.5 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-04T15:00:00Z"), "new_cases" : 14, "moving_average" : 33 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-05T15:00:00Z"), "new_cases" : 35, "moving_average" : 33.5 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-06T15:00:00Z"), "new_cases" : 61, "moving_average" : 39 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-07T15:00:00Z"), "new_cases" : 54, "moving_average" : 41.5 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-08T15:00:00Z"), "new_cases" : 27, "moving_average" : 39.42857142857143 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-09T15:00:00Z"), "new_cases" : 48, "moving_average" : 39.285714285714285 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-10T15:00:00Z"), "new_cases" : 19, "moving_average" : 36.857142857142854 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-11T15:00:00Z"), "new_cases" : 6, "moving_average" : 35.714285714285715 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-12T15:00:00Z"), "new_cases" : 55, "moving_average" : 38.57142857142857 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-13T15:00:00Z"), "new_cases" : 56, "moving_average" : 37.857142857142854 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-14T15:00:00Z"), "new_cases" : 45, "moving_average" : 36.57142857142857 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-15T15:00:00Z"), "new_cases" : 41, "moving_average" : 38.57142857142857 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-16T15:00:00Z"), "new_cases" : 26, "moving_average" : 35.42857142857143 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-17T15:00:00Z"), "new_cases" : 39, "moving_average" : 38.285714285714285 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-18T15:00:00Z"), "new_cases" : 3, "moving_average" : 37.857142857142854 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-19T15:00:00Z"), "new_cases" : 45, "moving_average" : 36.42857142857143 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-20T15:00:00Z"), "new_cases" : 54, "moving_average" : 36.142857142857146 }
{ "region" : "Abruzzo", "date" : ISODate("2021-10-21T15:00:00Z"), "new_cases" : 72, "moving_average" : 40 }
请注意,我们已将范围边界定义为 [-6,0],以便跨越当前文档的上周文档。
关于窗口函数的注意事项
我们在窗口定义中使用了单位:“天”,但此选项字段也可以有其他值,如年、季度、月、周、日、小时等。
有多种运算符可以与 $setWindowFields 一起使用:$avg、$count、$first、$last、$max、$min、$derivative、$sum、$rank 和许多其他您可以查看文档的运算符。
关于窗口函数的使用有一些限制。如果您遇到其中一些,请查看官方文档。
结论
新的窗口函数是部署在 MongoDB 5.0 上的一个非常好的特性。它可以使许多开发人员的生活更轻松。
要获取更多详细信息并检查您可以查看的限制,请查看以下页面:
https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/
原文标题:Window Functions in MongoDB 5.0
原文作者:Corrado Pandiani
原文地址:https://www.percona.com/blog/window-functions-in-mongodb-5-0/
原文标题:Window Functions in MongoDB 5.0
原文作者:Corrado Pandiani
原文地址:https://www.percona.com/blog/window-functions-in-mongodb-5-0/




