如何在 MongoDB 中使用聚合

作者选择了 开放式互联网 / 自由言论基金作为 写给捐赠计划的一部分接受捐款。

介绍

MongoDB 是一个数据库管理系统,允许您在被称为收藏的更大结构中存储大量数据的文档,您可以对收藏执行查询以获取符合特定条件的文件子集,但 MongoDB 的查询机制不允许您组合或转换返回的数据,这意味着您仅使用 MongoDB 的查询机制进行有意义数据分析的选项有限。

与许多其他数据库系统一样,MongoDB 允许您执行各种 _aggregation 操作,这些操作允许您以各种方式处理数据记录,例如将数据组合、将数据分类为特定的顺序,或重组返回的文档,以及过滤数据,就像一个查询一样。

MongoDB 通过 aggregation pipelines 提供聚合操作 - 一系列处理数据文档的操作. 在本教程中,您将通过示例学习如何使用聚合管道的最常见功能。

前提条件

要遵循本教程,您将需要:

  • 一个拥有sudo特权和与 UFW 配置的防火墙的常规非根用户的服务器. 本教程是通过使用运行 Ubuntu 20.04 的服务器进行验证的,您可以通过遵循此 Ubuntu 20.04 的初始服务器设置教程来准备您的服务器。
  • MongoDB 安装在您的服务器上。 要设置此项,请遵循我们关于 如何在 Ubuntu 20.04 上安装 MongoDB 的教程的教程。
  • 您的服务器的 MongoDB 实例通过允许身份验证和创建管理用户而得到保护。 要确保 MongoDB 像这样,请遵循我们的教程(How To Secure MongoDB

<$>[注] **注:**有关如何配置您的服务器,安装,然后保护MongoDB安装的链接教程参考Ubuntu 20.04.本教程专注于MongoDB本身,而不是潜在的操作系统。

了解聚合管道

当您使用数据库管理系统时,任何时候,您想要从数据库中获取数据时,都必须执行一个称为 query 的操作,但是,查询只会返回数据库中已经存在的数据,以便分析数据以找到数据的模式或其他信息,而不是数据本身,您通常需要执行另一种称为 aggregation 的操作。

在关系数据库中,数据库管理系统通常会从同一表中的多个行中提取数据,以执行汇总函数。

MongoDB 允许您通过称为 aggregation pipelines 的机制执行聚合操作. 这些都是以声明数据处理操作的连续序列构建的,称为 stages. 每个步骤检查和转换文件,随着它们通过管道,将转换的结果输入到后续步骤进行进一步处理。

它可能有助于思考这种过程,就像蔬菜在餐厅厨房中穿过一个组装线一样。在这种类比中,蔬菜穿过一组站点,每个站点都负责一个单一的操作:洗,切割,切割,烹饪和涂料。

阶段可以对数据执行操作,如:

  • 过滤:这类似于查询,其中文档列表通过一组标准 缩小
  • 分类:您可以根据所选的字段 重新排序文档* 转换:改变文档结构的能力意味着您可以删除或重命名某些字段,或者可能在嵌入文档中重命名或组字段以便可读性
  • 组合:您也可以一起处理多个文档以形成汇总的结果

管道步骤不需要生产相同数量的文件,他们收到的。继续我们的厨房模拟,想象一个切割站,采取整个蔬菜,并通过它们作为多个片段,或一个质量控制站,拒绝蔬菜有缺陷,并转移到下一个站只有一小部分健康的。

在以下步骤中,您将准备一个测试数据库作为示例数据集,然后您将学习如何单独使用一些最常见的聚合管道步骤。

步骤 1 – 准备测试数据

为了了解聚合管道如何工作以及如何使用它们,此步骤概述了如何打开 MongoDB 壳连接到本地安装的 MongoDB 实例。它还解释了如何创建样本集合并插入一些样本文档。

要创建此样本集合,请连接到 MongoDB 壳作为您的管理用户。本教程遵循前提教程(MongoDB 安全教程)(https://andsky.com/tech/tutorials/how-to-secure-mongodb-on-ubuntu-20-04)的惯例,并假定该管理用户的名称是 AdminSammy,其身份验证数据库是 admin

1mongo -u AdminSammy -p --authenticationDatabase admin

输入您在安装过程中设置的密码以获取存取壳. 提供密码后,您的提示将更改为大于符号(>)。

<$>[注] 注: 新连接时,MongoDB 壳会默认情况下自动连接到测试数据库,您可以安全地使用此数据库来实验 MongoDB 和 MongoDB 壳。

或者,您也可以切换到另一个数据库以运行本教程中提供的所有示例命令. 要切换到另一个数据库,请运行使用命令,然后是您的数据库名称:

1use database_name

美元

要了解聚合管道是如何工作的,您需要一系列具有不同类型的多个字段的文件,您可以以不同的方式过滤、分类、组合和总结。本指南将使用描述世界上人口最多的20个城市的样本集合。

1[label The Tokyo document]
2{
3    "name": "Tokyo",
4    "country": "Japan",
5    "continent": "Asia",
6    "population": 37.400
7}

本文件包含以下信息:

  • 名称:城市的名称
  • 国家:城市所在的国家
  • 大陆:城市所在的大陆
  • 人口:城市的人口,以百万计。

在 MongoDB 壳中运行以下insertMany()方法,同时创建一个名为cities的集合,并将二十个样本文档插入其中。

 1db.cities.insertMany([
 2    {"name": "Seoul", "country": "South Korea", "continent": "Asia", "population": 25.674 },
 3    {"name": "Mumbai", "country": "India", "continent": "Asia", "population": 19.980 },
 4    {"name": "Lagos", "country": "Nigeria", "continent": "Africa", "population": 13.463 },
 5    {"name": "Beijing", "country": "China", "continent": "Asia", "population": 19.618 },
 6    {"name": "Shanghai", "country": "China", "continent": "Asia", "population": 25.582 },
 7    {"name": "Osaka", "country": "Japan", "continent": "Asia", "population": 19.281 },
 8    {"name": "Cairo", "country": "Egypt", "continent": "Africa", "population": 20.076 },
 9    {"name": "Tokyo", "country": "Japan", "continent": "Asia", "population": 37.400 },
10    {"name": "Karachi", "country": "Pakistan", "continent": "Asia", "population": 15.400 },
11    {"name": "Dhaka", "country": "Bangladesh", "continent": "Asia", "population": 19.578 },
12    {"name": "Rio de Janeiro", "country": "Brazil", "continent": "South America", "population": 13.293 },
13    {"name": "São Paulo", "country": "Brazil", "continent": "South America", "population": 21.650 },
14    {"name": "Mexico City", "country": "Mexico", "continent": "North America", "population": 21.581 },
15    {"name": "Delhi", "country": "India", "continent": "Asia", "population": 28.514 },
16    {"name": "Buenos Aires", "country": "Argentina", "continent": "South America", "population": 14.967 },
17    {"name": "Kolkata", "country": "India", "continent": "Asia", "population": 14.681 },
18    {"name": "New York", "country": "United States", "continent": "North America", "population": 18.819 },
19    {"name": "Manila", "country": "Philippines", "continent": "Asia", "population": 13.482 },
20    {"name": "Chongqing", "country": "China", "continent": "Asia", "population": 14.838 },
21    {"name": "Istanbul", "country": "Turkey", "continent": "Europe", "population": 14.751 }
22])

输出将包含新插入对象分配的对象标识符列表。

 1[secondary_label Output]
 2{
 3        "acknowledged" : true,
 4        "insertedIds" : [
 5                ObjectId("612d1e835ebee16872a109a4"),
 6                ObjectId("612d1e835ebee16872a109a5"),
 7                ObjectId("612d1e835ebee16872a109a6"),
 8                ObjectId("612d1e835ebee16872a109a7"),
 9                ObjectId("612d1e835ebee16872a109a8"),
10                ObjectId("612d1e835ebee16872a109a9"),
11                ObjectId("612d1e835ebee16872a109aa"),
12                ObjectId("612d1e835ebee16872a109ab"),
13                ObjectId("612d1e835ebee16872a109ac"),
14                ObjectId("612d1e835ebee16872a109ad"),
15                ObjectId("612d1e835ebee16872a109ae"),
16                ObjectId("612d1e835ebee16872a109af"),
17                ObjectId("612d1e835ebee16872a109b0"),
18                ObjectId("612d1e835ebee16872a109b1"),
19                ObjectId("612d1e835ebee16872a109b2"),
20                ObjectId("612d1e835ebee16872a109b3"),
21                ObjectId("612d1e835ebee16872a109b4"),
22                ObjectId("612d1e835ebee16872a109b5"),
23                ObjectId("612d1e835ebee16872a109b6"),
24                ObjectId("612d1e835ebee16872a109b7")
25        ]
26}

您可以通过在城市集合中运行find()方法来验证文档是否正确插入,而没有参数。

1db.cities.find()
1[secondary_label Output]
2{ "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
3. . .

有了样本数据,您可以继续到下一步,了解如何使用$match阶段构建聚合管道。

步骤 2 — 使用$match聚合阶段

要创建一个聚合管道,可以使用 MongoDB 的聚合() 方法. 此方法使用一个类似于用于查询集合中的数据的find() 方法的语法,但聚合() 接受一个或多个阶段名称作为参数。

无论您是要进行轻型文档结构处理、总结或复杂转换,您通常都希望将分析集中在只针对特定标准的文档选择上。

举个例子,运行以下操作:此操作将使用单个$match阶段构建一个聚合管道,而无需任何特定的过滤查询:

1db.cities.aggregate([
2    { $match: { } }
3])

城市集合中执行的聚合()方法指示 MongoDB 运行作为方法论点通过的聚合管道. 由于聚合管道是多步进程,所以论点是阶段列表,因此使用的方块[]表示多个元素的数组。

这个数组中的每个元素都是描述处理阶段的对象. 此处的阶段写作为 { $ match: { } }. 在描述处理阶段的本文中,关键 $ match 指的是阶段类型,而值 { } 描述了其参数。

请记住,‘$match’缩小了收藏中的文档列表,没有应用过滤参数,MongoDB 会返回收藏中的所有城市列表:

1[secondary_label Output]
2{ "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
3{ "_id" : ObjectId("612d1e835ebee16872a109a5"), "name" : "Mumbai", "country" : "India", "continent" : "Asia", "population" : 19.98 }
4{ "_id" : ObjectId("612d1e835ebee16872a109a6"), "name" : "Lagos", "country" : "Nigeria", "continent" : "Africa", "population" : 13.463 }
5{ "_id" : ObjectId("612d1e835ebee16872a109a7"), "name" : "Beijing", "country" : "China", "continent" : "Asia", "population" : 19.618 }
6{ "_id" : ObjectId("612d1e835ebee16872a109a8"), "name" : "Shanghai", "country" : "China", "continent" : "Asia", "population" : 25.582 }
7. . .

接下来,再次运行汇总()方法,但这次将查询文档作为一个参数添加到$match阶段。

您可以考虑使用$match阶段等同于使用find()来查询集合,如在如何在MongoDB中创建查询(https://andsky.com/tech/tutorials/how-to-create-queries-in-mongodb)教程中所述。最大的区别是$match可以在聚合管道中多次使用,允许您查询已在管道中早些时候处理和转换的文档。

运行以下汇总()方法. 此示例包含一个$匹配阶段,仅选择来自北美的城市:

1db.cities.aggregate([
2    { $match: { "continent": "North America" } }
3])

此时,大陆:北美 }的查询文档作为$match阶段的参数出现,因此,MongoDB返回了来自北美的两个城市:

1[secondary_label Output]
2{ "_id" : ObjectId("612d1e835ebee16872a109b0"), "name" : "Mexico City", "country" : "Mexico", "continent" : "North America", "population" : 21.581 }
3{ "_id" : ObjectId("612d1e835ebee16872a109b4"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

此命令返回与下列命令相同的输出,该命令使用find()方法来查询数据库:

1db.cities.find({ "continent": "North America" })

以前的汇总()方法只返回两个城市,所以没有太多的实验可用. 要返回更多的结果,请更改此命令以返回来自北美和亚洲的城市:

1db.cities.aggregate([
2    { $match: { "continent": { $in: ["North America", "Asia"] } } }
3])

请注意,查询文档语法再次与您使用find()方法重新发送相同数据的方式相同。

1[secondary_label Output]
2{ "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
3{ "_id" : ObjectId("612d1e835ebee16872a109a5"), "name" : "Mumbai", "country" : "India", "continent" : "Asia", "population" : 19.98 }
4. . .

通过此,您已经学会了如何执行聚合管道,并使用$match阶段来缩小收藏的文档,继续阅读以了解如何使用$sort阶段来排序结果并将多个阶段结合起来来构建更复杂的管道。

步骤 3 — 使用$sort聚合阶段

$match阶段有助于缩小将文件列表移动到下一个汇总阶段,但$match不会改变或转换数据,因为它通过管道。

在查询数据库时,在检索结果时,通常会预期某个顺序。使用标准查询机制,您可以通过将sort()方法附加到find()查询的末尾,来指定文档顺序。

1db.cities.find().sort({ "population": -1 })

MongoDB将返回每个城市,从东京开始,接着是德里、首尔等:

1[secondary_label Output]
2{ "_id" : ObjectId("612d1e835ebee16872a109ab"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
3{ "_id" : ObjectId("612d1e835ebee16872a109b1"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
4{ "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
5. . .

您可以通过包括一个$sort阶段来对集合管道中的文档进行分类。 为了说明这一点,请运行以下aggregate()方法。

1db.cities.aggregate([
2    { $sort: { "population": -1 } }
3])

再一次,聚合管道中使用的阶段列表作为一对方块([])之间的一组阶段定义进行传递。本示例的阶段定义仅包含一个单个‘$sort’阶段作为密钥,其值为持有分类参数的文档。

MongoDB 将返回与之前的find()操作相同的结果设置,因为仅使用一个排序阶段的聚合管道相当于应用排序顺序的标准查询:

1[secondary_label Output]
2{ "_id" : ObjectId("612d1e835ebee16872a109ab"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
3{ "_id" : ObjectId("612d1e835ebee16872a109b1"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 }
4{ "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
5. . .

假设您只想从北美的城市获取按人口排序以上升顺序排序。 要做到这一点,您可以依次应用两个处理步骤:第一步是通过过滤$match阶段缩小结果,然后第二步是使用$sort步骤应用所需的排序:

1db.cities.aggregate([
2    { $match: { "continent": "North America" } },
3    { $sort: { "population": 1 } }
4])

注意命令语法中的两个单独的步骤,在步骤阵列中分开一个字节。

这一次,MongoDB将返回代表纽约和墨西哥城的文件,这两个城市是北美唯一的城市,从纽约开始,因为它人口较低:

1[secondary_label Output]
2{ "_id" : ObjectId("612d1e835ebee16872a109b4"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }
3{ "_id" : ObjectId("612d1e835ebee16872a109b0"), "name" : "Mexico City", "country" : "Mexico", "continent" : "North America", "population" : 21.581 }

为了获得这些结果,MongoDB 首先通过$match阶段通过文档收集,对查询标准进行过滤,然后将结果转发到负责分类结果的下一阶段,就像$match阶段一样,$sort可以在聚合管道中多次出现,并可以按您可能需要的任何字段排序文档,包括在聚合过程中只在文档结构中出现的字段。

<$>[注] **注:**在整合管道开始时运行过滤和排序步骤时,在任何投影,组合或其他转换步骤之前,MongoDB 将使用索引以最大限度地提高性能,就像标准查询一样。

步骤 4 — 使用$group聚合阶段

$group聚合阶段负责组合和总结文档. 它将多个文档列入并根据组合表达式值将其安排为几个单独的批次,并为每个单独的批次输出一个单一的文档。

为了说明,运行以下汇总()方法,其中包括一个$group阶段,该阶段将根据每个城市所在的大陆组合结果的文档:

1db.cities.aggregate([
2    { $group: { "_id": "$continent" } }
3])

在 MongoDB 中,每个文档都必须有一个_id字段以作为主要密钥使用。从步骤 1 记住,用于创建样本集合的insertMany()方法没有将此字段纳入任何样本文档中,这是因为 MongoDB 会自动创建此字段,并以ObjectId字段的形式生成独特的识别号。

然而,这种汇总()方法确实会指定一个_id值,即在城市集合中的每个文档的大陆字段中发现的每个值。 任何时候,您想要引用汇总管道中的一个字段的值,您必须以美元符号($)提前该字段的名称。 在 MongoDB,这被称为 field path,因为它将操作引导到相应的字段,在那里可以找到在管道阶段使用的值。

在本示例中,$大陆告诉 MongoDB 从原始文档中取大陆字段,并使用其值来构建汇总管道中的表达值。

1[secondary_label Output]
2{ "_id" : "Africa" }
3{ "_id" : "Asia" }
4{ "_id" : "South America" }
5{ "_id" : "Europe" }
6{ "_id" : "North America" }

此示例为收藏中代表的五个大洲输出一个单个文档. 默认情况下,组合阶段不包括来自原始文档的任何额外字段,因为它不知道如何或从哪个文档来源其他值。

但是,您可以在组合表达式中指定多个单域值. 以下示例方法将根据大陆国家文档中的值组合文档:

 1db.cities.aggregate([
 2    {
 3        $group: {
 4            "_id": {
 5                "continent": "$continent",
 6                "country": "$country"
 7            }
 8        }
 9    }
10])

请注意,本示例的_id字段使用嵌入式文档,其内有两个字段:一个用于大陆名称,另一个用于国家名称。

这一次,MongoDB 返回 14 个结果,因为收藏中有 14 个不同的国家和大陆对:

 1[secondary_label Output]
 2{ "_id" : { "continent" : "Europe", "country" : "Turkey" } }
 3{ "_id" : { "continent" : "South America", "country" : "Argentina" } }
 4{ "_id" : { "continent" : "Asia", "country" : "Bangladesh" } }
 5{ "_id" : { "continent" : "Asia", "country" : "Philippines" } }
 6{ "_id" : { "continent" : "Asia", "country" : "South Korea" } }
 7{ "_id" : { "continent" : "Asia", "country" : "Japan" } }
 8{ "_id" : { "continent" : "Asia", "country" : "China" } }
 9{ "_id" : { "continent" : "North America", "country" : "United States" } }
10{ "_id" : { "continent" : "North America", "country" : "Mexico" } }
11{ "_id" : { "continent" : "Africa", "country" : "Nigeria" } }
12{ "_id" : { "continent" : "Asia", "country" : "India" } }
13{ "_id" : { "continent" : "Asia", "country" : "Pakistan" } }
14{ "_id" : { "continent" : "Africa", "country" : "Egypt" } }
15{ "_id" : { "continent" : "South America", "country" : "Brazil" } }

这些结果没有以任何有意义的方式进行排序. 随着您越来越多地处理数据,您可能会遇到想要进行更复杂的数据分析的情况。 为此,MongoDB 提供了一些 _accumulator 操作员,允许您找到有关数据的更多细节。

为了说明,请运行以下汇总()方法。 该方法的$group阶段创建了所需的_id组合表达式以及三个额外的计算字段。

  • highest_population:此字段包含群组中最大的人口值。 $max 积累器操作员计算了群组中的所有文档中的 $population 的最大值。
  • first_city:包含群组中第一个城市的名称。 $first_' 积累器操作员从群组中出现的第一个文档中取出 $name_` 的值。 请注意,由于文档列表现在未分类,这并不意味着它自动成为群组中人口最高的城市,而是 MongoDB 在每个群组中找到的第一个城市。

您可以添加用于您的使用案例所需的更多额外的计算字段,但暂时运行此示例查询:

 1db.cities.aggregate([
 2    {
 3        $group: {
 4            "_id": {
 5                "continent": "$continent",
 6                "country": "$country"
 7            },
 8            "highest_population": { $max: "$population" },
 9            "first_city": { $first: "$name" },
10            "cities_in_top_20": { $sum: 1 }
11        }
12    }
13])

MongoDB 返回以下 14 个文档,每个由组合表达式定义的独特组为 1 个:

 1[secondary_label Output]
 2{ "_id" : { "continent" : "North America", "country" : "United States" }, "highest_population" : 18.819, "first_city" : "New York", "cities_in_top_20" : 1 }
 3{ "_id" : { "continent" : "Asia", "country" : "Philippines" }, "highest_population" : 13.482, "first_city" : "Manila", "cities_in_top_20" : 1 }
 4{ "_id" : { "continent" : "North America", "country" : "Mexico" }, "highest_population" : 21.581, "first_city" : "Mexico City", "cities_in_top_20" : 1 }
 5{ "_id" : { "continent" : "Africa", "country" : "Nigeria" }, "highest_population" : 13.463, "first_city" : "Lagos", "cities_in_top_20" : 1 }
 6{ "_id" : { "continent" : "Asia", "country" : "India" }, "highest_population" : 28.514, "first_city" : "Mumbai", "cities_in_top_20" : 3 }
 7{ "_id" : { "continent" : "Asia", "country" : "Pakistan" }, "highest_population" : 15.4, "first_city" : "Karachi", "cities_in_top_20" : 1 }
 8{ "_id" : { "continent" : "Africa", "country" : "Egypt" }, "highest_population" : 20.076, "first_city" : "Cairo", "cities_in_top_20" : 1 }
 9{ "_id" : { "continent" : "South America", "country" : "Brazil" }, "highest_population" : 21.65, "first_city" : "Rio de Janeiro", "cities_in_top_20" : 2 }
10{ "_id" : { "continent" : "Europe", "country" : "Turkey" }, "highest_population" : 14.751, "first_city" : "Istanbul", "cities_in_top_20" : 1 }
11{ "_id" : { "continent" : "Asia", "country" : "Bangladesh" }, "highest_population" : 19.578, "first_city" : "Dhaka", "cities_in_top_20" : 1 }
12{ "_id" : { "continent" : "South America", "country" : "Argentina" }, "highest_population" : 14.967, "first_city" : "Buenos Aires", "cities_in_top_20" : 1 }
13{ "_id" : { "continent" : "Asia", "country" : "South Korea" }, "highest_population" : 25.674, "first_city" : "Seoul", "cities_in_top_20" : 1 }
14{ "_id" : { "continent" : "Asia", "country" : "Japan" }, "highest_population" : 37.4, "first_city" : "Osaka", "cities_in_top_20" : 2 }
15{ "_id" : { "continent" : "Asia", "country" : "China" }, "highest_population" : 25.582, "first_city" : "Beijing", "cities_in_top_20" : 3 }

返回文档中的字段名称与组合阶段文档中的计算字段名称相匹配。

1[label A summarized document representing Japan]
2{ "_id" : { "continent" : "Asia", "country" : "Japan" }, "highest_population" : 37.4, "first_city" : "Osaka", "cities_in_top_20" : 2 }

「_id」字段包含日本和亚洲的组合表达式值。「cities_in_top_20」字段显示两个日本城市在人口最多的20个城市列表中。 请记住从步骤1中记住,您只添加了两个代表日本(东京和大阪)城市的文档,所以这个值是正确的。

然而,first_city显示了大阪,而不是东京,正如人们所期望的那样。 这是因为组合阶段使用了一份源文档列表,这些文档不是由人口订购的,所以在这种情况下它无法保证first的逻辑含义。

您将学习如何通过在步骤 6 中战略性地组合排序和组合步骤来改变这一点,但现在您可以继续到步骤 5 中,该步骤描述了如何使用预测来改变管道中的文档结构。

<$>[注] **注:**除了本步骤中描述的三种操作员外,MongoDB中还有几种可以用于各种聚合的蓄电器操作员。

步骤 5 — 使用$项目聚合阶段

在使用聚合管道时,有时您只需要返回文档集合中的几个多个字段,或者稍微更改结构,以便将某些字段移动到嵌入式文档中。

例如,假设您想获取样本收集中的每个城市的人口,但您希望结果以以下格式获取:

1[label Required document structure]
2{
3    "location" : {
4        "country" : "South Korea",
5        "continent" : "Asia"
6    },
7    "name" : "Seoul",
8    "population" : 25.674
9}

位置字段包含国家和大陆对,城市的名称和人口分别显示在名称人口字段中,并且文档标识符_id不会出现在输出文档中。

您可以使用$project步骤在聚合管道中构建新文档结构,从而改变结果集中的结果文档显示方式。

为了说明,运行以下汇总()方法,其中包括一个$项目步骤:

 1db.cities.aggregate([
 2    {
 3        $project: {
 4            "_id": 0,
 5            "location": {
 6                "country": "$country",
 7                "continent": "$continent",
 8            },
 9            "name": "$name",
10            "population": "$population"
11        }
12    }
13])

此「$project」阶段的值是描述输出结构的 projection 文档. 这些投影文档跟 在查询中使用的文档相同的格式,构建为包含投影或排除投影。

当投影文档中包含具有1值的键时,它描述了将被包含在结果中的字段列表.另一方面,如果投影钥匙设置为0,投影文档描述了将被排除在结果中的字段列表。

在聚合管道中,预测也可能包括额外的计算字段. 在这种情况下,预测会自动变成一个包含预测,只有_id字段可以通过将_id: 0`附加到预测文档来抑制。

在本示例中,文档标识符被压缩为_id:0,名称人口分别是指输入文档中的名称人口`字段的计算字段。

使用此投影步骤,MongoDB 将返回以下文档:

 1[secondary_label Output]
 2{ "location" : { "country" : "South Korea", "continent" : "Asia" }, "name" : "Seoul", "population" : 25.674 }
 3{ "location" : { "country" : "India", "continent" : "Asia" }, "name" : "Mumbai", "population" : 19.98 }
 4{ "location" : { "country" : "Nigeria", "continent" : "Africa" }, "name" : "Lagos", "population" : 13.463 }
 5{ "location" : { "country" : "China", "continent" : "Asia" }, "name" : "Beijing", "population" : 19.618 }
 6{ "location" : { "country" : "China", "continent" : "Asia" }, "name" : "Shanghai", "population" : 25.582 }
 7{ "location" : { "country" : "Japan", "continent" : "Asia" }, "name" : "Osaka", "population" : 19.281 }
 8{ "location" : { "country" : "Egypt", "continent" : "Africa" }, "name" : "Cairo", "population" : 20.076 }
 9{ "location" : { "country" : "Japan", "continent" : "Asia" }, "name" : "Tokyo", "population" : 37.4 }
10{ "location" : { "country" : "Pakistan", "continent" : "Asia" }, "name" : "Karachi", "population" : 15.4 }
11{ "location" : { "country" : "Bangladesh", "continent" : "Asia" }, "name" : "Dhaka", "population" : 19.578 }
12{ "location" : { "country" : "Brazil", "continent" : "South America" }, "name" : "Rio de Janeiro", "population" : 13.293 }
13{ "location" : { "country" : "Brazil", "continent" : "South America" }, "name" : "São Paulo", "population" : 21.65 }
14{ "location" : { "country" : "Mexico", "continent" : "North America" }, "name" : "Mexico City", "population" : 21.581 }
15{ "location" : { "country" : "India", "continent" : "Asia" }, "name" : "Delhi", "population" : 28.514 }
16{ "location" : { "country" : "Argentina", "continent" : "South America" }, "name" : "Buenos Aires", "population" : 14.967 }
17{ "location" : { "country" : "India", "continent" : "Asia" }, "name" : "Kolkata", "population" : 14.681 }
18{ "location" : { "country" : "United States", "continent" : "North America" }, "name" : "New York", "population" : 18.819 }
19{ "location" : { "country" : "Philippines", "continent" : "Asia" }, "name" : "Manila", "population" : 13.482 }
20{ "location" : { "country" : "China", "continent" : "Asia" }, "name" : "Chongqing", "population" : 14.838 }
21{ "location" : { "country" : "Turkey", "continent" : "Europe" }, "name" : "Istanbul", "population" : 14.751 }

每个文档现在都遵循通过投影阶段转换的新格式。

现在,您已经学会了如何使用$project阶段来构建一个新的文档结构,以便通过汇总管道的文档,您已经准备好将整个指南所涵盖的所有管道阶段合并到一个汇总管道中。

步骤6 - 将所有阶段结合起来

现在,您已经准备好将您在之前的步骤中练习过的所有步骤合并在一起,以形成一个功能齐全的聚合管道,该管道既过滤和转换文档。

假设任务是为亚洲和北美国家的每个国家找到人口最多的城市,并返回其名称和人口。结果应按人口最多排序,返回具有人口最多的城市的国家,并且您只对人口最多的城市超过2000万人口的国家感兴趣。

 1[label Example document]
 2{
 3    "location" : {
 4        "country" : "Japan",
 5        "continent" : "Asia"
 6    },
 7    "most_populated_city" : {
 8        "name" : "Tokyo",
 9        "population" : 37.4
10    }
11}

为了说明如何获取能够满足这些要求的数据集,此步骤概述了如何构建合并管道。

首先,请运行以下查询,该查询将过滤来自城市集中的初始文档,以便结果集仅包含亚洲和北美国家。尽管可以稍后缩小文档选择,但提前这样做会优化管道的效率。

1db.cities.aggregate([
2    {
3        $match: {
4            "continent": { $in: ["North America", "Asia"] }
5        }
6    }
7])

该管道的美元匹配阶段只会找到北美和亚洲的城市,代表这些城市的文件将以完整的原始结构和默认排序返回:

1[secondary_label Output]
2{ "_id" : ObjectId("612d1e835ebee16872a109a4"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }
3{ "_id" : ObjectId("612d1e835ebee16872a109a5"), "name" : "Mumbai", "country" : "India", "continent" : "Asia", "population" : 19.98 }
4{ "_id" : ObjectId("612d1e835ebee16872a109a7"), "name" : "Beijing", "country" : "China", "continent" : "Asia", "population" : 19.618 }
5{ "_id" : ObjectId("612d1e835ebee16872a109a8"), "name" : "Shanghai", "country" : "China", "continent" : "Asia", "population" : 25.582 }
6. . .

在步骤 4 中,您了解到将未分类的文档列表转移到组合阶段,如果您需要访问组中的第一个文档中的字段,可能会产生意想不到的结果。

 1db.cities.aggregate([
 2    {
 3        $match: {
 4            "continent": { $in: ["North America", "Asia"] }
 5        }
 6    },
 7    {
 8        $sort: { "population": -1 }
 9    }
10])

这个聚合()方法的第二个管道阶段告诉 MongoDB 按照{人口: -1 }排序文档所示,按人口排序排序文档。

再次,返回的文件有相同的结构,但这次东京首先是因为它有最高的人口:

1[secondary_label Output]
2{ "_id" : ObjectId("612d1e835ebee16872a109ab"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 }
3. . .

现在你有城市的列表,按预期大陆的人口排序,所以这个场景的下一个必要操作是按各自的国家组合城市,只从每个组中选择人口最多的城市。

 1db.cities.aggregate([
 2    {
 3        $match: {
 4            "continent": { $in: ["North America", "Asia"] }
 5        }
 6    },
 7    {
 8        $sort: { "population": -1 }
 9    },
10    {
11        $group: {
12            "_id": {
13                "continent": "$continent",
14                "country": "$country"
15            },
16            "first_city": { $first: "$name" },
17            "highest_population": { $max: "$population" }
18        }
19    }
20])

这个新阶段的组合表达式告诉 MongoDB 将城市按独特的大陆和国家对组合。对于每个组,两个计算值总结了组合。最高_人口值使用$max积累运算器来找到组合中最高的人口。第一_城市得到文件组中的第一个城市的名称。 借助先前应用的分类阶段,您可以确定这个第一个城市也将是组合中人口最多的城市,并且它将匹配人口数值。

添加这个$group阶段会改变该方法返回的文档数量以及它们的结构。这次,该方法只返回九个文档,因为在先前过滤的城市列表中只有九个国家和大陆对。

 1[secondary_label Output]
 2{ "_id" : { "continent" : "North America", "country" : "United States" }, "first_city" : "New York", "highest_population" : 18.819 }
 3{ "_id" : { "continent" : "Asia", "country" : "China" }, "first_city" : "Shanghai", "highest_population" : 25.582 }
 4{ "_id" : { "continent" : "Asia", "country" : "Japan" }, "first_city" : "Tokyo", "highest_population" : 37.4 }
 5{ "_id" : { "continent" : "Asia", "country" : "South Korea" }, "first_city" : "Seoul", "highest_population" : 25.674 }
 6{ "_id" : { "continent" : "Asia", "country" : "Bangladesh" }, "first_city" : "Dhaka", "highest_population" : 19.578 }
 7{ "_id" : { "continent" : "Asia", "country" : "Philippines" }, "first_city" : "Manila", "highest_population" : 13.482 }
 8{ "_id" : { "continent" : "Asia", "country" : "India" }, "first_city" : "Delhi", "highest_population" : 28.514 }
 9{ "_id" : { "continent" : "Asia", "country" : "Pakistan" }, "first_city" : "Karachi", "highest_population" : 15.4 }
10{ "_id" : { "continent" : "North America", "country" : "Mexico" }, "first_city" : "Mexico City", "highest_population" : 21.581 }

请注意,每个群体的结果文件不是按人口值排序的,纽约是第一个,但第二个城市 - 上海 - 拥有近700万人口的人口。

请记住,过滤和排序步骤可以在管道中多次出现,此外,对于每个聚合步骤,最后一个步骤的输出是下一个步骤的输入。

 1db.cities.aggregate([
 2    {
 3        $match: {
 4            "continent": { $in: ["North America", "Asia"] }
 5        }
 6    },
 7    {
 8        $sort: { "population": -1 }
 9    },
10    {
11        $group: {
12            "_id": {
13                "continent": "$continent",
14                "country": "$country"
15            },
16            "first_city": { $first: "$name" },
17            "highest_population": { $max: "$population" }
18        }
19    },
20    {
21        $match: {
22            "highest_population": { $gt: 20.0 }
23        }
24    }
25])

此过滤$match阶段指来自组合阶段的文档中可用的highest_population字段,即使此类字段不是原始文档结构的一部分。

这一次,五个国家出现在输出中:

1[secondary_label Output]
2{ "_id" : { "continent" : "Asia", "country" : "China" }, "first_city" : "Shanghai", "highest_population" : 25.582 }
3{ "_id" : { "continent" : "Asia", "country" : "Japan" }, "first_city" : "Tokyo", "highest_population" : 37.4 }
4{ "_id" : { "continent" : "Asia", "country" : "South Korea" }, "first_city" : "Seoul", "highest_population" : 25.674 }
5{ "_id" : { "continent" : "Asia", "country" : "India" }, "first_city" : "Delhi", "highest_population" : 28.514 }
6{ "_id" : { "continent" : "North America", "country" : "Mexico" }, "first_city" : "Mexico City", "highest_population" : 21.581 }

接下来,按其最高_人口值对结果进行排序. 要做到这一点,请添加另一个$sort阶段:

 1db.cities.aggregate([
 2    {
 3        $match: {
 4            "continent": { $in: ["North America", "Asia"] }
 5        }
 6    },
 7    {
 8        $sort: { "population": -1 }
 9    },
10    {
11        $group: {
12            "_id": {
13                "continent": "$continent",
14                "country": "$country"
15            },
16            "first_city": { $first: "$name" },
17            "highest_population": { $max: "$population" }
18        }
19    },
20    {
21        $match: {
22            "highest_population": { $gt: 20.0 }
23        }
24    },
25    {
26        $sort: { "highest_population": -1 }
27    }
28])

文档结构没有改变,MongoDB仍然返回五个国家组的文档,但这一次,日本出现在第一位,因为东京是数据集中人口最多的城市:

1[secondary_label Output]
2{ "_id" : { "continent" : "Asia", "country" : "Japan" }, "first_city" : "Tokyo", "highest_population" : 37.4 }
3{ "_id" : { "continent" : "Asia", "country" : "India" }, "first_city" : "Delhi", "highest_population" : 28.514 }
4{ "_id" : { "continent" : "Asia", "country" : "South Korea" }, "first_city" : "Seoul", "highest_population" : 25.674 }
5{ "_id" : { "continent" : "Asia", "country" : "China" }, "first_city" : "Shanghai", "highest_population" : 25.582 }
6{ "_id" : { "continent" : "North America", "country" : "Mexico" }, "first_city" : "Mexico City", "highest_population" : 21.581 }

最后的要求是转换文档结构以匹配之前显示的样本。

 1[label Example document]
 2{
 3    "location" : {
 4        "country" : "Japan",
 5        "continent" : "Asia"
 6    },
 7    "most_populated_city" : {
 8        "name" : "Tokyo",
 9        "population" : 37.4
10    }
11}

此样本的位置嵌入文档类似于_id组合表达式值,因为这两个字段包括国家大陆字段。

若要将结果转换为与此结构一致,请在管道中添加一个$project阶段:

 1db.cities.aggregate([
 2    {
 3        $match: {
 4            "continent": { $in: ["North America", "Asia"] }
 5        }
 6    },
 7    {
 8        $sort: { "population": -1 }
 9    },
10    {
11        $group: {
12            "_id": {
13                "continent": "$continent",
14                "country": "$country"
15            },
16            "first_city": { $first: "$name" },
17            "highest_population": { $max: "$population" }
18        }
19    },
20    {
21        $match: {
22            "highest_population": { $gt: 20.0 }
23        }
24    },
25    {
26        $sort: { "highest_population": -1 }
27    },
28    {
29        $project: {
30            "_id": 0,
31            "location": {
32                "country": "$_id.country",
33                "continent": "$_id.continent",
34            },
35            "most_populated_city": {
36                "name": "$first_city",
37                "population": "$highest_population"
38            }
39        }
40    }
41])

此「$project」階段首先會阻止「_id」字段在輸出中出現,然後創建一個「位置」字段作為包含兩個字段的嵌入文檔:「國家」和「大陸」。使用美元符號標記,每個字段都指向輸入文件的值。「$_id.country」從輸入的「_id」嵌入文檔內部抽取「國家」字段的值,並使用「$_id.continent」抽取「大陸」字段的值。

这个投影阶段有效地构建了一个全新的输出结构,如下:

1[secondary_label Output]
2{ "location" : { "country" : "Japan", "continent" : "Asia" }, "most_populated_city" : { "name" : "Tokyo", "population" : 37.4 } }
3{ "location" : { "country" : "India", "continent" : "Asia" }, "most_populated_city" : { "name" : "Delhi", "population" : 28.514 } }
4{ "location" : { "country" : "South Korea", "continent" : "Asia" }, "most_populated_city" : { "name" : "Seoul", "population" : 25.674 } }
5{ "location" : { "country" : "China", "continent" : "Asia" }, "most_populated_city" : { "name" : "Shanghai", "population" : 25.582 } }
6{ "location" : { "country" : "Mexico", "continent" : "North America" }, "most_populated_city" : { "name" : "Mexico City", "population" : 21.581 } }

此输出符合本步骤开始时定义的所有要求:

  • 它只包括来自亚洲和北美的城市在列表中.
  • 每个国家和大陆对,单个城市被选中,它是人口最多的城市
  • 选择的城市的名称和人口列出
  • 城市从人口最多到人口最少排序
  • 输出格式被更改以与示例文档一致

结论

在本文中,您熟悉了聚合管道,一个 MongoDB 功能用于多步文档处理,包括过滤,排序,总结和转换。您已经使用了$match,$sort,$group$project聚合步骤,并联合执行示例报告场景处理输入文档以呈现聚合数据。

本教程只描述了一小部分由MongoDB提供的聚合管道功能来处理和转换数据. 还有更多的处理步骤可用,在本文中描述的每一个步骤都可以以额外和不同的方式使用。

Published At
Categories with 技术
comments powered by Disqus