如何在MongoDB中使用聚合
作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。
介绍
MongoDB 是一个数据库管理系统,允许您将大量数据存储在文档中,这些文档保存在称为集合的更大结构中。 您可以对集合运行查询以检索与给定条件匹配的文档子集,但 MongoDB 的查询机制不允许您对返回的数据进行分组或转换。 这意味着仅使用 MongoDB 的查询机制执行有意义的数据分析的选项是有限的。
与许多其他数据库系统一样,MongoDB 允许您执行各种 聚合操作 。 这些允许您以多种方式处理数据记录,例如对数据进行分组、将数据排序为特定顺序或重组返回的文档,以及像使用查询一样过滤数据。
MongoDB 通过 聚合管道 提供聚合操作——一系列按顺序处理数据文档的操作。 在本教程中,您将通过示例学习如何使用聚合管道的最常见功能。 您将过滤、排序、分组和转换文档,然后将所有这些功能一起使用以形成多阶段处理管道。
先决条件
要遵循本教程,您将需要:
- 具有
sudo
权限的常规非 root 用户和配置了 UFW 的防火墙的服务器。 本教程使用运行 Ubuntu 20.04 的服务器进行了验证,您可以按照此 Ubuntu 20.04 初始服务器设置教程来准备您的服务器。 - MongoDB 安装在您的服务器上。 要进行此设置,请按照我们关于 如何在 Ubuntu 20.04 上安装 MongoDB 的教程进行操作。
- 通过启用身份验证和创建管理用户来保护服务器的 MongoDB 实例。 要像这样保护 MongoDB,请按照我们关于 如何在 Ubuntu 20.04 上保护 MongoDB 的教程进行操作。
- 熟悉查询 MongoDB 集合和过滤结果。 要了解如何使用 MongoDB 查询,请按照教程 如何在 MongoDB 中创建查询。
注意:有关如何配置服务器、安装和安全 MongoDB 安装的链接教程参考 Ubuntu 20.04。 本教程专注于 MongoDB 本身,而不是底层操作系统。 只要启用了身份验证,它通常适用于任何 MongoDB 安装,无论操作系统如何。
了解聚合管道
使用数据库管理系统时,任何时候想要从数据库中检索数据都必须执行称为 查询 的操作。 但是,查询只返回数据库中已经存在的数据。 为了分析您的数据以查找有关数据的模式或其他信息(而不是数据本身),您通常需要执行另一种称为 聚合 的操作。
聚合对来自多个来源的数据进行分组,然后以某种方式处理该数据以返回单个结果。 在关系数据库中,数据库管理系统通常会从同一个表中的多行中提取数据以执行聚合函数。 但是,在像 MongoDB 这样的面向文档的数据库中,数据库将从同一个集合中的多个文档中提取数据。
MongoDB使您能够通过称为聚合管道的机制执行聚合操作。 这些构建为一系列顺序的声明性数据处理操作,称为 stages。 每个阶段都会在文档通过管道时检查和转换文档,将转换后的结果输入后续阶段以进行进一步处理。 来自选定集合的文档进入管道并经过每个阶段,其中一个阶段的输出形成下一个阶段的输入,最终结果出现在管道的末端。
将这个过程想象成蔬菜通过餐厅厨房的流水线可能会有所帮助。 在这个类比中,蔬菜经过一组站,每个站负责一个动作:清洗、去皮、切碎、烹饪和电镀。 同样,进入聚合管道的数据必须经过多个阶段,每个阶段负责特定的操作。
阶段可以对数据执行操作,例如:
- filtering:这类似于查询,其中通过一组标准缩小文档列表
- 排序:您可以根据所选字段重新排序文档
- 转换:更改文档结构的能力意味着您可以删除或重命名某些字段,或者可能重命名或组合嵌入文档中的字段以提高可读性
- grouping:也可以将多个文档一起处理,形成一个汇总结果
流水线阶段不需要生成与接收到的相同数量的文档。 继续我们的厨房类比,想象一个切碎站将整个蔬菜切成多片,或者一个质量控制站拒绝有缺陷的蔬菜并只将少数健康蔬菜传递到下一个站。 同样,阶段可以生成新文档或从输入管道开始的集合中过滤掉现有文档。 此外,同一阶段可以在聚合管道中出现多次,一个接一个地应用多个操作。
在以下步骤中,您将准备一个测试数据库作为示例数据集。 然后,您将学习如何单独使用一些最常见的聚合管道阶段。 最后,您将把这些阶段组合在一起形成一个完整的示例管道。
第 1 步——准备测试数据
为了了解聚合管道如何工作以及如何使用它们,此步骤概述了如何打开 MongoDB shell 以连接到本地安装的 MongoDB 实例。 它还解释了如何创建示例集合并将一些示例文档插入其中。 本指南将使用此示例数据来说明 MongoDB 的不同类型的聚合阶段。
要创建此示例集合,请以您的管理用户身份连接到 MongoDB shell。 本教程遵循先决条件 MongoDB 安全教程 的约定,并假设此管理用户名为 AdminSammy,其身份验证数据库为 admin
。 如果不同,请务必在以下命令中更改这些详细信息以反映您自己的设置:
mongo -u AdminSammy -p --authenticationDatabase admin
输入您在安装期间设置的密码以访问 shell。 提供密码后,您的提示符将变为大于号 (>
)。
注意: 在新连接时,MongoDB shell 默认会自动连接到 test
数据库。 您可以安全地使用此数据库来试验 MongoDB 和 MongoDB shell。
或者,您也可以切换到另一个数据库来运行本教程中给出的所有示例命令。 要切换到另一个数据库,请运行 use
命令,后跟数据库名称:
use database_name
要了解聚合管道的工作原理,您将需要一个包含多个不同类型字段的文档集合,您可以通过不同方式过滤、排序、分组和汇总。 本指南将使用一个样本集来描述世界上人口最多的 20 个城市。 这些文档将与以下描述东京市的示例文档具有相同的格式:
东京文件
{ "name": "Tokyo", "country": "Japan", "continent": "Asia", "population": 37.400 }
本文档包含以下信息:
name
:城市名称。country
:城市所在的国家。continent
:城市所在的大陆。population
:城市人口,百万。
在 MongoDB shell 中运行以下 insertMany()
方法,同时创建一个名为 cities
的集合,并将 20 个示例文档插入其中。 这些文件描述了世界上人口最多的 20 个城市:
db.cities.insertMany([ {"name": "Seoul", "country": "South Korea", "continent": "Asia", "population": 25.674 }, {"name": "Mumbai", "country": "India", "continent": "Asia", "population": 19.980 }, {"name": "Lagos", "country": "Nigeria", "continent": "Africa", "population": 13.463 }, {"name": "Beijing", "country": "China", "continent": "Asia", "population": 19.618 }, {"name": "Shanghai", "country": "China", "continent": "Asia", "population": 25.582 }, {"name": "Osaka", "country": "Japan", "continent": "Asia", "population": 19.281 }, {"name": "Cairo", "country": "Egypt", "continent": "Africa", "population": 20.076 }, {"name": "Tokyo", "country": "Japan", "continent": "Asia", "population": 37.400 }, {"name": "Karachi", "country": "Pakistan", "continent": "Asia", "population": 15.400 }, {"name": "Dhaka", "country": "Bangladesh", "continent": "Asia", "population": 19.578 }, {"name": "Rio de Janeiro", "country": "Brazil", "continent": "South America", "population": 13.293 }, {"name": "São Paulo", "country": "Brazil", "continent": "South America", "population": 21.650 }, {"name": "Mexico City", "country": "Mexico", "continent": "North America", "population": 21.581 }, {"name": "Delhi", "country": "India", "continent": "Asia", "population": 28.514 }, {"name": "Buenos Aires", "country": "Argentina", "continent": "South America", "population": 14.967 }, {"name": "Kolkata", "country": "India", "continent": "Asia", "population": 14.681 }, {"name": "New York", "country": "United States", "continent": "North America", "population": 18.819 }, {"name": "Manila", "country": "Philippines", "continent": "Asia", "population": 13.482 }, {"name": "Chongqing", "country": "China", "continent": "Asia", "population": 14.838 }, {"name": "Istanbul", "country": "Turkey", "continent": "Europe", "population": 14.751 } ])
输出将包含分配给新插入对象的对象标识符列表。
Output{ "acknowledged" : true, "insertedIds" : [ ObjectId("612d1e835ebee16872a109a4"), ObjectId("612d1e835ebee16872a109a5"), ObjectId("612d1e835ebee16872a109a6"), ObjectId("612d1e835ebee16872a109a7"), ObjectId("612d1e835ebee16872a109a8"), ObjectId("612d1e835ebee16872a109a9"), ObjectId("612d1e835ebee16872a109aa"), ObjectId("612d1e835ebee16872a109ab"), ObjectId("612d1e835ebee16872a109ac"), ObjectId("612d1e835ebee16872a109ad"), ObjectId("612d1e835ebee16872a109ae"), ObjectId("612d1e835ebee16872a109af"), ObjectId("612d1e835ebee16872a109b0"), ObjectId("612d1e835ebee16872a109b1"), ObjectId("612d1e835ebee16872a109b2"), ObjectId("612d1e835ebee16872a109b3"), ObjectId("612d1e835ebee16872a109b4"), ObjectId("612d1e835ebee16872a109b5"), ObjectId("612d1e835ebee16872a109b6"), ObjectId("612d1e835ebee16872a109b7") ] }
您可以通过在不带参数的 cities
集合上运行 find()
方法来验证文档是否已正确插入。 这将检索集合中的所有文档:
db.cities.find()
Output{ "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } . . .
有了样本数据,您可以继续下一步,了解如何使用 $match
阶段构建聚合管道。
第 2 步 — 使用 $match
聚合阶段
要创建聚合管道,可以使用 MongoDB 的 aggregate()
方法。 此方法使用的语法与用于查询集合中数据的 find()
方法非常相似,但 aggregate()
接受一个或多个阶段名称作为参数。 此步骤重点介绍如何使用 $match
聚合阶段。
无论您是要进行简单的文档结构处理、汇总还是复杂的转换,您通常都希望将分析重点放在与特定标准匹配的文档选择上。 $match
可用于在管道的任何给定步骤缩小文档列表,并可用于确保所有后续操作将在有限的条目列表上执行。
例如,运行以下操作。 这将使用单个 $match
阶段构建一个聚合管道,而无需任何特定的过滤查询:
db.cities.aggregate([ { $match: { } } ])
在 cities
集合上执行的 aggregate()
方法指示 MongoDB 运行作为方法参数传递的聚合管道。 因为聚合管道是多步骤过程,所以参数是阶段列表,因此使用方括号 []
表示多个元素的数组。
该数组中的每个元素都是描述处理阶段的对象。 这里的阶段写为{ $match: { } }
。 在这个描述处理阶段的文档中,键$match
指的是阶段类型,值{ }
描述它的参数。 在我们的示例中,$match
阶段使用空查询文档作为其参数,并且是整个处理管道中的唯一阶段。
请记住,$match
缩小了集合中的文档列表。 在没有应用过滤参数的情况下,MongoDB 将返回集合中所有城市的列表:
Output{ "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("612d1e835ebee16872a109a5"), "name" : "Mumbai", "country" : "India", "continent" : "Asia", "population" : 19.98 } { "_id" : ObjectId("612d1e835ebee16872a109a6"), "name" : "Lagos", "country" : "Nigeria", "continent" : "Africa", "population" : 13.463 } { "_id" : ObjectId("612d1e835ebee16872a109a7"), "name" : "Beijing", "country" : "China", "continent" : "Asia", "population" : 19.618 } { "_id" : ObjectId("612d1e835ebee16872a109a8"), "name" : "Shanghai", "country" : "China", "continent" : "Asia", "population" : 25.582 } . . .
接下来,再次运行 aggregate()
方法,但这次包含一个查询文档作为 $match
阶段的参数。 任何有效的查询文件都可以在这里使用。
您可以认为使用 $match
阶段等同于使用 find()
查询集合,如先决条件部分中列出的 如何在 MongoDB 中创建查询教程中所述。 最大的不同是,$match
可以在聚合管道中多次使用,允许您查询在管道中较早处理和转换的文档。 您将在本指南后面部分了解有关在同一聚合管道中多次使用同一阶段的更多信息。
运行以下 aggregate()
方法。 此示例包括一个 $match
阶段,用于仅选择北美的城市:
db.cities.aggregate([ { $match: { "continent": "North America" } } ])
这次 { "continent": "North America" }
查询文档作为参数出现在 $match
阶段。 因此,MongoDB 从北美返回两个城市:
Output{ "_id" : ObjectId("612d1e835ebee16872a109b0"), "name" : "Mexico City", "country" : "Mexico", "continent" : "North America", "population" : 21.581 } { "_id" : ObjectId("612d1e835ebee16872a109b4"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }
此命令返回与以下命令相同的输出,而是使用 find()
方法查询数据库:
db.cities.find({ "continent": "North America" })
前面的 aggregate()
方法只返回两个城市,所以没有太多可以试验的地方。 要返回更多结果,请更改此命令,使其返回北美和亚洲的城市:
db.cities.aggregate([ { $match: { "continent": { $in: ["North America", "Asia"] } } } ])
请注意,查询文档语法再次与使用 find()
方法检索相同数据的方式相同。 这次 MongoDB 返回 14 个不同的城市:
Output{ "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("612d1e835ebee16872a109a5"), "name" : "Mumbai", "country" : "India", "continent" : "Asia", "population" : 19.98 } . . .
至此,您已经了解了如何执行聚合管道并使用 $match
阶段来缩小集合的文档范围。 继续阅读以了解如何通过使用 $sort
阶段对结果进行排序并将多个阶段组合在一起来构建更复杂的管道。
第 3 步 — 使用 $sort
聚合阶段
$match
阶段对于缩小移动到下一个聚合阶段的文档列表很有用。 但是,$match
在数据通过管道时不会更改或转换数据。
查询数据库时,通常会在检索结果时期望某个顺序。 使用标准查询机制,您可以通过将 sort()
方法附加到 find()
查询的末尾来指定文档顺序。 例如,要检索集合中的每个城市并按人口降序对它们进行排序,您可以运行如下操作:
db.cities.find().sort({ "population": -1 })
MongoDB 将返回每个城市,从东京开始,然后是德里、首尔等:
Output{ "_id" : ObjectId("612d1e835ebee16872a109ab"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("612d1e835ebee16872a109b1"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } . . .
您也可以通过包含 $sort
阶段来对聚合管道中的文档进行排序。 为了说明这一点,运行以下 aggregate()
方法。 这遵循与前面使用 $match
阶段的示例类似的语法:
db.cities.aggregate([ { $sort: { "population": -1 } } ])
同样,聚合管道中使用的阶段列表作为阶段定义数组在一对方括号 ([]
) 之间传递。 此示例的阶段定义仅包含一个 $sort
阶段作为键,其值是包含排序参数的文档。 任何有效的排序文档都可以在这里使用。
MongoDB 将返回与之前的 find()
操作相同的结果集,因为使用仅具有排序阶段的聚合管道等效于应用排序顺序的标准查询:
Output{ "_id" : ObjectId("612d1e835ebee16872a109ab"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("612d1e835ebee16872a109b1"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } . . .
假设您想检索按人口升序排序的北美城市。 为此,您可以一个接一个地应用两个处理阶段:第一个使用过滤 $match
阶段缩小结果集,然后第二个使用 $sort
应用所需的排序阶段:
db.cities.aggregate([ { $match: { "continent": "North America" } }, { $sort: { "population": 1 } } ])
请注意命令语法中的两个单独阶段,在阶段数组中用逗号分隔。
这一次,MongoDB 将返回代表纽约和墨西哥城的文档,这两个城市是北美仅有的两个城市,从人口较少的纽约开始:
Output{ "_id" : ObjectId("612d1e835ebee16872a109b4"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 } { "_id" : ObjectId("612d1e835ebee16872a109b0"), "name" : "Mexico City", "country" : "Mexico", "continent" : "North America", "population" : 21.581 }
为了获得这些结果,MongoDB 首先将文档集合通过 $match
阶段,根据查询条件过滤文档,然后将结果转发到负责对结果进行排序的下一个阶段。 就像 $match
阶段一样,$sort
可以在聚合管道中出现多次,并且可以按您可能需要的任何字段对文档进行排序,包括在聚合期间只会出现在文档结构中的字段。
注意: 在聚合管道的开始运行过滤和排序阶段时,在任何投影、分组或其他转换阶段之前,MongoDB 将使用索引来最大化性能,就像使用标准查询一样。 您可以在我们的 如何在 MongoDB 中使用索引的指南中了解有关索引的更多信息。
第 4 步 — 使用 $group
聚合阶段
$group
聚合阶段负责对文档进行分组和汇总。 它接收多个文档,并根据分组表达式值将它们排列成几个单独的批次,并为每个不同的批次输出一个文档。 输出文档包含有关该组的信息,并且可以包含其他计算字段,例如来自该组的文档列表的总和或平均值。
为了说明,运行以下 aggregate()
方法。 这包括一个 $group
阶段,它将按每个城市所在的大陆对生成的文档进行分组:
db.cities.aggregate([ { $group: { "_id": "$continent" } } ])
在 MongoDB 中,每个文档都必须有一个 _id
字段用作主键。 回想第 1 步,用于创建样本集合的 insertMany()
方法没有在任何样本文档中包含此字段。 这是因为 MongoDB 会自动创建该字段并以 ObjectId
字段的形式生成唯一标识号。 但是,对于聚合管道中的 $group
阶段,您需要使用有效表达式指定 _id
字段。
但是,此 aggregate()
方法确实指定了 _id
值; 即,在 cities
集合中每个文档的 continent
字段中找到的每个值。 任何时候您想像这样在聚合管道中引用字段的值时,您必须在字段名称前加上美元符号 ($
)。 在 MongoDB 中,这被称为 字段路径 ,因为它将操作定向到适当的字段,在那里它可以找到要在管道阶段使用的值。
在此示例中,"$continent"
告诉 MongoDB 从原始文档中获取 continent
字段,并使用其值在聚合管道中构造表达式值。 MongoDB 将为该分组表达式的每个唯一值输出一个文档:
Output{ "_id" : "Africa" } { "_id" : "Asia" } { "_id" : "South America" } { "_id" : "Europe" } { "_id" : "North America" }
此示例为集合中表示的五大洲中的每一个输出一个文档。 默认情况下,分组阶段不包括原始文档中的任何其他字段,因为它不知道如何或从哪个文档获取其他值。
但是,您可以在分组表达式中指定多个单字段值。 以下示例方法将根据 continent
和 country
文档中的值对文档进行分组:
db.cities.aggregate([ { $group: { "_id": { "continent": "$continent", "country": "$country" } } } ])
请注意,此示例的分组表达式的 _id
字段使用嵌入式文档,该文档内部又包含两个字段:一个用于大陆名称,另一个用于国家/地区名称。 这两个字段都使用字段路径美元符号表示法引用原始文档中的字段。
这次 MongoDB 返回 14 个结果,因为集合中有 14 个不同的国家/地区对:
Output{ "_id" : { "continent" : "Europe", "country" : "Turkey" } } { "_id" : { "continent" : "South America", "country" : "Argentina" } } { "_id" : { "continent" : "Asia", "country" : "Bangladesh" } } { "_id" : { "continent" : "Asia", "country" : "Philippines" } } { "_id" : { "continent" : "Asia", "country" : "South Korea" } } { "_id" : { "continent" : "Asia", "country" : "Japan" } } { "_id" : { "continent" : "Asia", "country" : "China" } } { "_id" : { "continent" : "North America", "country" : "United States" } } { "_id" : { "continent" : "North America", "country" : "Mexico" } } { "_id" : { "continent" : "Africa", "country" : "Nigeria" } } { "_id" : { "continent" : "Asia", "country" : "India" } } { "_id" : { "continent" : "Asia", "country" : "Pakistan" } } { "_id" : { "continent" : "Africa", "country" : "Egypt" } } { "_id" : { "continent" : "South America", "country" : "Brazil" } }
这些结果没有以任何有意义的方式排序。 随着您越来越多地使用数据,您可能会遇到想要执行更复杂数据分析的情况。 为此,MongoDB 提供了许多 累加器运算符 ,它们允许您找到有关数据的更详细的详细信息。 累加器操作符,有时简称为 accumulator,是一种特殊类型的操作,它在通过聚合管道时保持其值或状态,例如多个值的总和或平均值.
为了说明,运行以下 aggregate()
方法。 此方法的 $group
阶段创建所需的 _id
分组表达式以及三个额外的计算字段。 这些计算字段都包含一个累加器运算符及其值。 以下是这些计算字段的细分:
highest_population
:此字段包含组中的最大人口值。$max
累加器运算符计算组中所有文档的"$population"
的最大值。first_city
:包含组中第一个城市的名称。$first
累加器运算符从出现在组中的第一个文档中获取"$name"
的值。 请注意,由于文档列表现在是无序的,这不会自动使其成为人口最多的城市,而是 MongoDB 在每个组中找到的第一个城市。cities_in_top_20
:保存每个大陆-国家对集合中的城市数量。 为此,$sum
累加器运算符用于计算列表中所有对的总和。 在此示例中,每个文档的总和为一个,并且不引用源文档中的特定字段。
您可以根据用例的需要添加任意数量的额外计算字段,但现在运行此示例查询:
db.cities.aggregate([ { $group: { "_id": { "continent": "$continent", "country": "$country" }, "highest_population": { $max: "$population" }, "first_city": { $first: "$name" }, "cities_in_top_20": { $sum: 1 } } } ])
MongoDB 返回以下 14 个文档,每个文档对应于分组表达式定义的每个唯一组:
Output{ "_id" : { "continent" : "North America", "country" : "United States" }, "highest_population" : 18.819, "first_city" : "New York", "cities_in_top_20" : 1 } { "_id" : { "continent" : "Asia", "country" : "Philippines" }, "highest_population" : 13.482, "first_city" : "Manila", "cities_in_top_20" : 1 } { "_id" : { "continent" : "North America", "country" : "Mexico" }, "highest_population" : 21.581, "first_city" : "Mexico City", "cities_in_top_20" : 1 } { "_id" : { "continent" : "Africa", "country" : "Nigeria" }, "highest_population" : 13.463, "first_city" : "Lagos", "cities_in_top_20" : 1 } { "_id" : { "continent" : "Asia", "country" : "India" }, "highest_population" : 28.514, "first_city" : "Mumbai", "cities_in_top_20" : 3 } { "_id" : { "continent" : "Asia", "country" : "Pakistan" }, "highest_population" : 15.4, "first_city" : "Karachi", "cities_in_top_20" : 1 } { "_id" : { "continent" : "Africa", "country" : "Egypt" }, "highest_population" : 20.076, "first_city" : "Cairo", "cities_in_top_20" : 1 } { "_id" : { "continent" : "South America", "country" : "Brazil" }, "highest_population" : 21.65, "first_city" : "Rio de Janeiro", "cities_in_top_20" : 2 } { "_id" : { "continent" : "Europe", "country" : "Turkey" }, "highest_population" : 14.751, "first_city" : "Istanbul", "cities_in_top_20" : 1 } { "_id" : { "continent" : "Asia", "country" : "Bangladesh" }, "highest_population" : 19.578, "first_city" : "Dhaka", "cities_in_top_20" : 1 } { "_id" : { "continent" : "South America", "country" : "Argentina" }, "highest_population" : 14.967, "first_city" : "Buenos Aires", "cities_in_top_20" : 1 } { "_id" : { "continent" : "Asia", "country" : "South Korea" }, "highest_population" : 25.674, "first_city" : "Seoul", "cities_in_top_20" : 1 } { "_id" : { "continent" : "Asia", "country" : "Japan" }, "highest_population" : 37.4, "first_city" : "Osaka", "cities_in_top_20" : 2 } { "_id" : { "continent" : "Asia", "country" : "China" }, "highest_population" : 25.582, "first_city" : "Beijing", "cities_in_top_20" : 3 }
返回文档中的字段名称对应于分组阶段文档中的计算字段名称。 为了更仔细地检查结果,让我们将焦点缩小到单个文档:
代表日本的摘要文件
{ "_id" : { "continent" : "Asia", "country" : "Japan" }, "highest_population" : 37.4, "first_city" : "Osaka", "cities_in_top_20" : 2 }
_id
字段保存日本和亚洲的分组表达式值。 cities_in_top_20
字段显示有两个日本城市在 20 个人口最多的城市列表中。 回想第 1 步,您只添加了两个代表日本城市(东京和大阪)的文档,因此该值是正确的。 highest_population
对应的是东京的人口,确实是两者中较高的人口。
然而,first_city
显示的是大阪而不是东京,正如人们所预料的那样。 那是因为分组阶段使用了一个未按人口排序的源文档列表,因此无法保证“第一”在该场景中的逻辑含义。 Osaka 首先由管道处理,因此出现在 first_city
字段值中。
您将在第 6 步中学习如何通过战略性地结合排序和分组阶段来改变这种情况。 不过,现在,您可以继续执行第 5 步,该步骤概述了如何使用投影转换管道中的文档结构。
注意: 除了这一步中描述的三个之外,MongoDB 中还有几个累加器运算符可用于各种聚合。 要了解有关分组和累加器运算符的更多信息,我们鼓励您关注 官方 MongoDB 文档 。
第 5 步 — 使用 $project
聚合阶段
使用聚合管道时,有时您可能只想返回文档集合的多个字段中的一部分,或者稍微更改结构以将某些字段移动到嵌入文档中。 您可以使用此策略来编辑不应包含在报告中的字段,或者以适合某些应用程序要求的格式准备结果。
例如,假设您想检索样本集合中每个城市的人口,但您希望结果采用以下格式:
所需的文档结构
{ "location" : { "country" : "South Korea", "continent" : "Asia" }, "name" : "Seoul", "population" : 25.674 }
location
字段包含国家和大陆对,城市名称和人口分别显示在 name
和 population
字段中,文档标识符 _id
不会出现在输出的文档中。
您可以使用 $project
阶段在聚合管道中构建新的文档结构,从而改变结果文档在结果集中的显示方式。
为了说明,运行以下包含 $project
阶段的 aggregate()
方法:
db.cities.aggregate([ { $project: { "_id": 0, "location": { "country": "$country", "continent": "$continent", }, "name": "$name", "population": "$population" } } ])
此 $project
阶段的值是描述输出结构的 projection 文档。 这些投影文档遵循与查询中使用的相同的格式,构造为包含投影或排除投影。 投影文档按键对应输入文档进入$project
阶段的按键。
当投影文档包含以 1
作为其值的键时,它描述了将包含在结果中的字段列表。 另一方面,如果投影键设置为 0
,则投影文档将描述将从结果中排除的字段列表。
在聚合管道中,投影还可以包括额外的计算字段。 在这种情况下,投影会自动变为包含投影,并且只有 _id
字段可以通过将 "_id": 0
附加到投影文档来抑制。 计算字段对其值使用美元符号字段路径表示法,并且可以引用输入文档中的值。
在此示例中,使用 "_id": 0
抑制文档标识符,name
和 population
是引用 name
和 population
的计算字段分别来自输入文档的字段。 location
字段成为嵌入文档,其中包含两个附加键:country
和 continent
,指的是输入文档中的字段。
使用此投影阶段,MongoDB 将返回以下文档:
Output{ "location" : { "country" : "South Korea", "continent" : "Asia" }, "name" : "Seoul", "population" : 25.674 } { "location" : { "country" : "India", "continent" : "Asia" }, "name" : "Mumbai", "population" : 19.98 } { "location" : { "country" : "Nigeria", "continent" : "Africa" }, "name" : "Lagos", "population" : 13.463 } { "location" : { "country" : "China", "continent" : "Asia" }, "name" : "Beijing", "population" : 19.618 } { "location" : { "country" : "China", "continent" : "Asia" }, "name" : "Shanghai", "population" : 25.582 } { "location" : { "country" : "Japan", "continent" : "Asia" }, "name" : "Osaka", "population" : 19.281 } { "location" : { "country" : "Egypt", "continent" : "Africa" }, "name" : "Cairo", "population" : 20.076 } { "location" : { "country" : "Japan", "continent" : "Asia" }, "name" : "Tokyo", "population" : 37.4 } { "location" : { "country" : "Pakistan", "continent" : "Asia" }, "name" : "Karachi", "population" : 15.4 } { "location" : { "country" : "Bangladesh", "continent" : "Asia" }, "name" : "Dhaka", "population" : 19.578 } { "location" : { "country" : "Brazil", "continent" : "South America" }, "name" : "Rio de Janeiro", "population" : 13.293 } { "location" : { "country" : "Brazil", "continent" : "South America" }, "name" : "São Paulo", "population" : 21.65 } { "location" : { "country" : "Mexico", "continent" : "North America" }, "name" : "Mexico City", "population" : 21.581 } { "location" : { "country" : "India", "continent" : "Asia" }, "name" : "Delhi", "population" : 28.514 } { "location" : { "country" : "Argentina", "continent" : "South America" }, "name" : "Buenos Aires", "population" : 14.967 } { "location" : { "country" : "India", "continent" : "Asia" }, "name" : "Kolkata", "population" : 14.681 } { "location" : { "country" : "United States", "continent" : "North America" }, "name" : "New York", "population" : 18.819 } { "location" : { "country" : "Philippines", "continent" : "Asia" }, "name" : "Manila", "population" : 13.482 } { "location" : { "country" : "China", "continent" : "Asia" }, "name" : "Chongqing", "population" : 14.838 } { "location" : { "country" : "Turkey", "continent" : "Europe" }, "name" : "Istanbul", "population" : 14.751 }
现在,每个文档都遵循通过投影阶段转换的新格式。
既然您已经学习了如何使用 $project
阶段为通过聚合管道的文档构建新的文档结构,您已经准备好将本指南中涵盖的所有管道阶段组合到单个聚合管道中.
第 6 步 - 将所有阶段放在一起
您现在已准备好将您在前面的步骤中练习使用的所有阶段连接在一起,以形成一个功能齐全的聚合管道,用于过滤和转换文档。
假设手头的任务是为亚洲和北美的每个国家/地区找到人口最多的城市,并返回其名称和人口。 结果应按人口最多排序,首先返回城市最大的国家,并且您只对人口最多的城市超过 2000 万人口门槛的国家感兴趣。 最后,您的目标文档结构应复制以下内容:
示例文档
{ "location" : { "country" : "Japan", "continent" : "Asia" }, "most_populated_city" : { "name" : "Tokyo", "population" : 37.4 } }
为了说明如何检索满足这些要求的数据集,此步骤概述了如何构建适当的聚合管道。
首先运行以下查询,该查询过滤来自 cities
集合的初始文档,因此结果集将仅包含亚洲和北美的国家/地区。 尽管以后可以缩小文档选择范围,但提前这样做会优化管道的效率。 毕竟,限制早期处理的文档数量将最大限度地减少后期所需的处理量:
db.cities.aggregate([ { $match: { "continent": { $in: ["North America", "Asia"] } } } ])
此管道的 $match
阶段将仅查找北美和亚洲的城市,代表这些城市的文档将以其完整的原始结构和默认顺序返回:
Output{ "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("612d1e835ebee16872a109a5"), "name" : "Mumbai", "country" : "India", "continent" : "Asia", "population" : 19.98 } { "_id" : ObjectId("612d1e835ebee16872a109a7"), "name" : "Beijing", "country" : "China", "continent" : "Asia", "population" : 19.618 } { "_id" : ObjectId("612d1e835ebee16872a109a8"), "name" : "Shanghai", "country" : "China", "continent" : "Asia", "population" : 25.582 } . . .
在第 4 步中,您了解到如果您需要访问组中第一个文档中的字段,则将文档的无序列表传递到分组阶段可能会产生意想不到的结果。 您必须这样做才能稍后找到人口最多的城市的名称,所以要解决这个问题,您可以按照 $match
阶段和 [X207X ] 阶段:
db.cities.aggregate([ { $match: { "continent": { $in: ["North America", "Asia"] } } }, { $sort: { "population": -1 } } ])
此 aggregate()
方法中的第二个管道阶段告诉 MongoDB 按人口按降序对文档进行排序,如 { "population": -1 }
排序文档所示。
返回的文档再次具有相同的结构,但这次东京排在首位,因为它的人口最多:
Output{ "_id" : ObjectId("612d1e835ebee16872a109ab"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } . . .
您现在拥有按来自预期大陆的人口排序的城市列表,因此此方案的下一个必要操作是按国家/地区对城市进行分组,从每个组中仅选择人口最多的城市。 为此,将 $group
阶段添加到管道:
db.cities.aggregate([ { $match: { "continent": { $in: ["North America", "Asia"] } } }, { $sort: { "population": -1 } }, { $group: { "_id": { "continent": "$continent", "country": "$country" }, "first_city": { $first: "$name" }, "highest_population": { $max: "$population" } } } ])
这个新阶段的分组表达式告诉 MongoDB 按独特的大陆和国家对分组城市。 对于每个组,两个计算值汇总了这些组。 highest_population
值使用 $max
累加器运算符来查找组中的最高人口。 first_city
获取文档组中第一个城市的名称。 由于之前应用的排序阶段,您可以确定第一个城市也是该组中人口最多的城市,并且它将与人口数值相匹配。
添加此 $group
阶段会更改此方法返回的文档数量及其结构。 这一次,该方法只返回九个文档,因为之前过滤的城市列表中只有九个唯一的国家和大陆对。 每个文档对应于其中一对,由 _id
字段中的分组表达式值和两个计算字段组成:
Output{ "_id" : { "continent" : "North America", "country" : "United States" }, "first_city" : "New York", "highest_population" : 18.819 } { "_id" : { "continent" : "Asia", "country" : "China" }, "first_city" : "Shanghai", "highest_population" : 25.582 } { "_id" : { "continent" : "Asia", "country" : "Japan" }, "first_city" : "Tokyo", "highest_population" : 37.4 } { "_id" : { "continent" : "Asia", "country" : "South Korea" }, "first_city" : "Seoul", "highest_population" : 25.674 } { "_id" : { "continent" : "Asia", "country" : "Bangladesh" }, "first_city" : "Dhaka", "highest_population" : 19.578 } { "_id" : { "continent" : "Asia", "country" : "Philippines" }, "first_city" : "Manila", "highest_population" : 13.482 } { "_id" : { "continent" : "Asia", "country" : "India" }, "first_city" : "Delhi", "highest_population" : 28.514 } { "_id" : { "continent" : "Asia", "country" : "Pakistan" }, "first_city" : "Karachi", "highest_population" : 15.4 } { "_id" : { "continent" : "North America", "country" : "Mexico" }, "first_city" : "Mexico City", "highest_population" : 21.581 }
请注意,每个组的结果文档不是按人口值排序的。 纽约排在首位,但第二个城市——上海——拥有近 700 万人口。 此外,一些国家的人口最多的城市低于 2000 万的预期门槛。
请记住,过滤和排序阶段可以在管道中出现多次。 此外,对于每个聚合阶段,最后一个阶段的输出是下一个阶段的输入。 使用另一个 $match
阶段过滤组以仅包含城市满足最低人口 2000 万的国家:
db.cities.aggregate([ { $match: { "continent": { $in: ["North America", "Asia"] } } }, { $sort: { "population": -1 } }, { $group: { "_id": { "continent": "$continent", "country": "$country" }, "first_city": { $first: "$name" }, "highest_population": { $max: "$population" } } }, { $match: { "highest_population": { $gt: 20.0 } } } ])
此过滤 $match
阶段指的是来自分组阶段的文档中可用的 highest_population
字段,即使这样的字段不是原始文档结构的一部分。
这一次,输出中出现了五个国家:
Output{ "_id" : { "continent" : "Asia", "country" : "China" }, "first_city" : "Shanghai", "highest_population" : 25.582 } { "_id" : { "continent" : "Asia", "country" : "Japan" }, "first_city" : "Tokyo", "highest_population" : 37.4 } { "_id" : { "continent" : "Asia", "country" : "South Korea" }, "first_city" : "Seoul", "highest_population" : 25.674 } { "_id" : { "continent" : "Asia", "country" : "India" }, "first_city" : "Delhi", "highest_population" : 28.514 } { "_id" : { "continent" : "North America", "country" : "Mexico" }, "first_city" : "Mexico City", "highest_population" : 21.581 }
接下来,根据结果的 highest_population
值对结果进行排序。 为此,添加另一个 $sort
阶段:
db.cities.aggregate([ { $match: { "continent": { $in: ["North America", "Asia"] } } }, { $sort: { "population": -1 } }, { $group: { "_id": { "continent": "$continent", "country": "$country" }, "first_city": { $first: "$name" }, "highest_population": { $max: "$population" } } }, { $match: { "highest_population": { $gt: 20.0 } } }, { $sort: { "highest_population": -1 } } ])
文档结构没有改变,MongoDB 仍然返回与国家组对应的五个文档。 然而,这一次,日本首先出现,因为东京是数据集中人口最多的城市:
Output{ "_id" : { "continent" : "Asia", "country" : "Japan" }, "first_city" : "Tokyo", "highest_population" : 37.4 } { "_id" : { "continent" : "Asia", "country" : "India" }, "first_city" : "Delhi", "highest_population" : 28.514 } { "_id" : { "continent" : "Asia", "country" : "South Korea" }, "first_city" : "Seoul", "highest_population" : 25.674 } { "_id" : { "continent" : "Asia", "country" : "China" }, "first_city" : "Shanghai", "highest_population" : 25.582 } { "_id" : { "continent" : "North America", "country" : "Mexico" }, "first_city" : "Mexico City", "highest_population" : 21.581 }
最后一个要求是转换文档结构以匹配前面显示的示例。 为了您的评论,这里再次提供该示例:
示例文档
{ "location" : { "country" : "Japan", "continent" : "Asia" }, "most_populated_city" : { "name" : "Tokyo", "population" : 37.4 } }
此示例的 location
嵌入文档类似于 _id
分组表达式值,因为两者都包含 country
和 continent
字段。 人口最多的城市名称和人口作为嵌入文档嵌套在 most_populated_city
字段下。 这与所有计算字段都是顶级字段的分组结果不同。
要将结果转换为与此结构对齐,请将 $project
阶段添加到管道:
db.cities.aggregate([ { $match: { "continent": { $in: ["North America", "Asia"] } } }, { $sort: { "population": -1 } }, { $group: { "_id": { "continent": "$continent", "country": "$country" }, "first_city": { $first: "$name" }, "highest_population": { $max: "$population" } } }, { $match: { "highest_population": { $gt: 20.0 } } }, { $sort: { "highest_population": -1 } }, { $project: { "_id": 0, "location": { "country": "$_id.country", "continent": "$_id.continent", }, "most_populated_city": { "name": "$first_city", "population": "$highest_population" } } } ])
这个 $project
阶段首先抑制 _id
字段出现在输出中。 然后它创建一个 location
字段作为包含两个字段的嵌套文档:country
和 continent
。 使用美元符号表示法,这些字段中的每一个都引用来自输入文档的值。 "$_id.country"
从输入的 _id
嵌入文档内部的 country
字段中提取值,并且 $_id.continent
从其 continent
字段中提取值. most_populated_city
遵循类似的结构,将 name
和 population
字段嵌套在里面。 这些分别指的是顶级字段 first_city
和 highest_population
。
这个投影阶段有效地为输出构建了一个全新的结构,如下所示:
Output{ "location" : { "country" : "Japan", "continent" : "Asia" }, "most_populated_city" : { "name" : "Tokyo", "population" : 37.4 } } { "location" : { "country" : "India", "continent" : "Asia" }, "most_populated_city" : { "name" : "Delhi", "population" : 28.514 } } { "location" : { "country" : "South Korea", "continent" : "Asia" }, "most_populated_city" : { "name" : "Seoul", "population" : 25.674 } } { "location" : { "country" : "China", "continent" : "Asia" }, "most_populated_city" : { "name" : "Shanghai", "population" : 25.582 } } { "location" : { "country" : "Mexico", "continent" : "North America" }, "most_populated_city" : { "name" : "Mexico City", "population" : 21.581 } }
此输出满足此步骤开始时定义的所有要求:
- 它仅包括列表中来自亚洲和北美的城市。
- 对于每个国家和大陆对,选择一个城市,它是人口最多的城市。
- 列出了所选城市的名称和人口。
- 城市按人口最多到最少的顺序排列。
- 输出格式已更改以与示例文档对齐。
结论
在本文中,您熟悉了聚合管道,这是一种用于多步骤文档处理的 MongoDB 功能,包括过滤、排序、汇总和转换。 您已经使用 $match
、$sort
、$group
和 $project
聚合阶段一一并联合执行示例报告场景处理输入文档呈现汇总数据。 您还熟悉计算域和累加器运算符,以从文档组中查找最大值和总和。
本教程仅描述了 MongoDB 提供的用于处理和转换数据的聚合管道功能的一小部分。 有更多可用的处理阶段,本文中描述的每个阶段都可以以其他不同的方式使用。 我们鼓励您学习官方 官方 MongoDB 文档 以了解有关聚合管道的更多信息以及它们如何帮助您处理存储在数据库中的数据。