前一部分的文章:
MongoDB 快速入门实战教程基础篇 一 :文档的 CR操作
MongoDB 快速入门实战教程基础篇 一 :文档的 UD操作
基础篇 二 流式聚合操作
信息科学中的聚合是指对相关数据进行内容筛选、处理和归类并输出结果的过程。MongoDB 中的聚合是指同时对多个文档中的数据进行处理、筛选和归类并输出结果的过程。数据在聚合操作的过程中,就像是水流过一节一节的管道一样,所以 MongoDB 中的聚合又被人称为流式聚合。MongoDB 提供了几种聚合方式:
- Aggregation Pipeline
- Map-Reduce
- 简单聚合
接下来,我们将全方位地了解 MongoDB 中的聚合。
Aggregation Pipeline
Aggregation Pipeline 又称聚合管道。开发者可以将多个文档传入一个由多个 Stage 组成的 Pipeline,每一个 Stage 处理的结果将会传入下一个 Stage 中,最后一个 Stage 的处理结果就是整个 Pipeline 的输出。
创建聚合管道的语法如下:
db.collection.aggregate( [ { <stage> }, ... ] )
MongoDB 提供了 23 种 Stage,它们是:
| Stage | 描述 |
|---|---|
$addFields |
向文档添加新字段。 |
$bucket |
根据指定的表达式和存储区边界将传入的文档分组。 |
$bucketAuto |
根据指定的表达式将传入的文档分类为特定数量的组,自动确定存储区边界。 |
$collStats |
返回有关集合或视图的统计信息。 |
$count |
返回聚合管道此阶段的文档数量计数。 |
$facet |
在同一组输入文档的单个阶段内处理多个聚合操作。 |
$geoNear |
基于与地理空间点的接近度返回有序的文档流。 |
$graphLookup |
对集合执行递归搜索。 |
$group |
按指定的标识符表达式对文档进行分组。 |
$indexStats |
返回集合的索引信息。 |
$limit |
将未修改的前 n 个文档传递给管道。 |
$listSessions |
列出system.sessions集合的所有会话。 |
$lookup |
对同一数据库中的另一个集合执行左外连接。 |
$match |
过滤文档,仅允许匹配的文档地传递到下一个管道阶段。 |
$out |
将聚合管道的结果文档写入指定集合,它必须是管道中的最后一个阶段。 |
$project |
为文档添加新字段或删除现有字段。 |
$redact |
可用于实现字段级别的编辑。 |
$replaceRoot |
用指定的嵌入文档替换文档。该操作将替换输入文档中的所有现有字段,包括_id字段。指定嵌入在输入文档中的文档以将嵌入文档提升到顶层。 |
$sample |
从输入中随机选择指定数量的文档。 |
$skip |
跳过前 n 个文档,并将未修改的其余文档传递到下一个阶段。 |
$sort |
按指定的排序键重新排序文档流。只有订单改变; 文件保持不变。对于每个输入文档,输出一个文档。 |
$sortByCount |
对传入文档进行分组,然后计算每个不同组中的文档计数。 |
$unwind |
解构文档中的数组字段。 |
文档、Stage 和 Pipeline 的关系如下图所示:
上图描述了文档经过 $match、$sample 和 $project 等三个 Stage 并输出的过程。SQL 中常见的聚合术语有 WHERE、SUM 和 COUNT 等。下表描述了常见的 SQL 聚合术语、函数和概念以及对应的 MongoDB 操作符或 Stage。
| SQL | MongoDB |
|---|---|
| WHERE | $match |
| GROUP BY | $group |
| HAVING | $match |
| SELECT | $project |
| ORDER BY | $sort |
| LIMIT | $limit |
| SUM() | $sum |
| COUNT() |
$sum$sortByCount
|
| join | $lookup |
下面,我们将通过示例了解 Aggregate、 Stage 和 Pipeline 之间的关系。
概念浅出
$match 的描述为“过滤文档,仅允许匹配的文档地传递到下一个管道阶段”。其语法格式如下:
{ $match: { <query> } }
在开始学习之前,我们需要准备以下数据:
> db.artic.insertMany([
... { "_id" : 1, "author" : "dave", "score" : 80, "views" : 100 },
... { "_id" : 2, "author" : "dave", "score" : 85, "views" : 521 },
... { "_id" : 3, "author" : "anna", "score" : 60, "views" : 706 },
... { "_id" : 4, "author" : "line", "score" : 55, "views" : 300 }
... ])
然后我们建立只有一个 Stage 的 Pipeline,以实现过滤出 author 为 dave 的文档。对应示例如下:
> db.artic.aggregate([
... {$match: {author: "dave"}}
... ])
{ "_id" : 1, "author" : "dave", "score" : 80, "views" : 100 }
{ "_id" : 2, "author" : "dave", "score" : 85, "views" : 521 }
如果要建立有两个 Stage 的 Pipeline,那么就在 aggregate 中添加一个 Stage 即可。现在有这样一个需求:统计集合 artic 中 score 大于 70 且小于 90 的文档数量。这个需求分为两步进行:
- 过滤出符合要求的文档
- 统计文档数量
Aggregation 非常适合这种多步骤的操作。在这个场景中,我们需要用到 $match、$group 这两个 Stage ,然后再与聚合表达式 $sum 相结合,对应示例如下:
> db.artic.aggregate([
... {$match: {score: {$gt: 70, $lt: 90}}},
... {$group: {_id: null, number: {$sum: 1}}}
... ])
{ "_id" : null, "number" : 2 }
这个示例的完整过程可以用下图表示:
通过上面的描述和举例,我相信你对 Aggregate、 Stage 和 Pipeline 有了一定的了解。接下来,我们将学习常见的 Stage 的语法和用途。
常见的 Stage
sample
$sample 的作用是从输入中随机选择指定数量的文档,其语法格式如下:
{ $sample: { size: <positive integer> } }
假设要从集合 artic 中随机选择两个文档,对应示例如下:
> db.artic.aggregate([
... {$sample: {size: 2}}
... ])
{ "_id" : 1, "author" : "dave", "score" : 80, "views" : 100 }
{ "_id" : 3, "author" : "anna", "score" : 60, "views" : 706 }
size 对应的值必须是正整数,如果输入负数会得到错误提示:size argument to $sample must not be negative。要注意的是,当值超过集合中的文档数量时,返回结果是集合中的所有文档,但文档顺序是随机的。
project
$project 的作用是过滤文档中的字段,这与投影操作相似,但处理结果将会传入到下一个阶段 。其语法格式如下:
{ $project: { <specification(s)> } }
准备以下数据:
> db.projects.save(
{_id: 1, title: "篮球训练营青春校园活动开始啦", numb: "A829Sck23", author: {last: "quinn", first: "James"}, hot: 35}
)
假设 Pipeline 中的下一个 Stage 只需要文档中的 title 和 author 字段,对应示例如下:
> db.projects.aggregate([{$project: {title: 1, author: 1}}])
{ "_id" : 1, "title" : "篮球训练营青春校园活动开始啦", "author" : { "last" : "quinn", "first" : "James" } }
0 和 1 可以同时存在。对应示例如下:
> db.projects.aggregate([{$project: {title: 1, author: 1, _id: 0}}])
{ "title" : "篮球训练营青春校园活动开始啦", "author" : { "last" : "quinn", "first" : "James" } }
true 等效于 1,false 等效于 0,也可以混用布尔值和数字,对应示例如下:
> db.projects.aggregate([{$project: {title: 1, author: true, _id: false}}])
{ "title" : "篮球训练营青春校园活动开始啦", "author" : { "last" : "quinn", "first" : "James" } }
如果想要排除指定字段,那么在 $project 中将其设置为 0 或 false 即可,对应示例如下:
> db.projects.aggregate([{$project: {author: false, _id: false}}])
{ "title" : "篮球训练营青春校园活动开始啦", "numb" : "A829Sck23", "hot" : 35 }
$project 也可以作用于嵌入式文档。对于 author 字段,有时候我们只需要 FirstName 或者 Lastname ,对应示例如下:
> db.projects.aggregate([{$project: {author: {"last": false}, _id: false, numb: 0}}])
{ "title" : "篮球训练营青春校园活动开始啦", "author" : { "first" : "James" }, "hot" : 35 }
这里使用 {author: {"last": false}} 过滤掉 LastName,但保留 first。
以上就是 $project 的基本用法和作用介绍,更多与 $project 相关的知识可查阅官方文档 $project。
lookup
$lookup 的作用是对同一数据库中的集合执行左外连接,其语法格式如下:
{
$lookup:
{
from: <collection to join>,
localField: <field from the input documents>,
foreignField: <field from the documents of the "from" collection>,
as: <output array field>
}
}
左外连接类似与下面的伪 SQL 语句:
SELECT *, <output array field>
FROM collection WHERE <output array field> IN (
SELECT * FROM <collection to join> WHERE
<foreignField>= <collection.localField>);
lookup 支持的指令及对应描述如下:
| 领域 | 描述 |
|---|---|
from |
指定集合名称。 |
localField |
指定输入 $lookup 中的字段。 |
foreignField |
指定from 给定的集合中的文档字段。 |
as |
指定要添加到输入文档的新数组字段的名称。 |
新数组字段包含from集合中的匹配文档。
如果输入文档中已存在指定的名称,则会覆盖现有字段 。 |
准备以下数据:
> db.sav.insert([
{ "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 },
{ "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 },
{ "_id" : 3 }
])
> db.avi.insert([
{ "_id" : 1, "sku" : "almonds", description: "product 1", "instock" : 120 },
{ "_id" : 2, "sku" : "bread", description: "product 2", "instock" : 80 },
{ "_id" : 3, "sku" : "cashews", description: "product 3", "instock" : 60 },
{ "_id" : 4, "sku" : "pecans", description: "product 4", "instock" : 70 },
{ "_id" : 5, "sku": null, description: "Incomplete" },
{ "_id" : 6 }
])
假设要连接集合 sav 中的 item 和集合 avi 中的 sku,并将连接结果命名为 savi。对应示例如下:
> db.sav.aggregate([
{
$lookup:
{
from: "avi",
localField: "item",
foreignField: "sku",
as: "savi"
}
}
])
命令执行后,输出如下内容:
{
"_id" : 1,
"item" : "almonds",
"price" : 12,
"quantity" : 2,
"savi" : [
{ "_id" : 1, "sku" : "almonds", "description" : "product 1", "instock" : 120 }
]
}
{
"_id" : 2,
"item" : "pecans",
"price" : 20,
"quantity" : 1,
"savi" : [
{ "_id" : 4, "sku" : "pecans", "description" : "product 4", "instock" : 70 }
]
}
{
"_id" : 3,
"savi" : [
{ "_id" : 5, "sku" : null, "description" : "Incomplete" },
{ "_id" : 6 }
]
}
上面的连接操作等效于下面这样的伪 SQL:
SELECT *, savi
FROM sav
WHERE savi IN (SELECT *
FROM avi
WHERE sku= sav.item);
以上就是 lookup 的基本用法和作用介绍,更多与 lookup 相关的知识可查阅官方文档 lookup。
unwind
unwind 能将包含数组的文档拆分称多个文档,其语法格式如下:
{
$unwind:
{
path: <field path>,
includeArrayIndex: <string>,
preserveNullAndEmptyArrays: <boolean>
}
}
unwind 支持的指令及对应描述如下:
| 指令 | 类型 | 描述 |
|---|---|---|
path |
string | 指定数组字段的字段路径, 必填。 |
includeArrayIndex |
string | 用于保存元素的数组索引的新字段的名称。 |
preserveNullAndEmptyArrays |
boolean | 默认情况下,如果path为 null、缺少该字段或空数组, 则不输出文档。反之,将其设为 true 则会输出文档。 |
在开始学习之前,我们需要准备以下数据:
> db.shoes.save({_id: 1, brand: "Nick", sizes: [37, 38, 39]})
集合 shoes 中的 sizes 是一个数组,里面有多个尺码数据。假设要将这个文档拆分成 3 个 size 为单个值的文档,对应示例如下:
> db.shoes.aggregate([{$unwind : "$sizes"}])
{ "_id" : 1, "brand" : "Nick", "sizes" : 37 }
{ "_id" : 1, "brand" : "Nick", "sizes" : 38 }
{ "_id" : 1, "brand" : "Nick", "sizes" : 39 }
显然,这样的文档更方便我们做数据处理。preserveNullAndEmptyArrays 指令默认为 false,也就是说文档中指定的 path 为空、null 或缺少该 path 的时候,会忽略掉该文档。假设数据如下:
> db.shoes2.insertMany([
{"_id": 1, "item": "ABC", "sizes": ["S", "M", "L"]},
{"_id": 2, "item": "EFG", "sizes": [ ]},
{"_id": 3, "item": "IJK", "sizes": "M"},
{"_id": 4, "item": "LMN" },
{"_id": 5, "item": "XYZ", "sizes": null}
])
我们执行以下命令:
> db.shoes2.aggregate([{$unwind: "$sizes"}])
就会得到如下输出:
{ "_id" : 1, "item" : "ABC", "sizes" : "S" }
{ "_id" : 1, "item" : "ABC", "sizes" : "M" }
{ "_id" : 1, "item" : "ABC", "sizes" : "L" }
{ "_id" : 3, "item" : "IJK", "sizes" : "M" }
_id 为 2、4 和 5 的文档由于满足 preserveNullAndEmptyArrays 的条件,所以不会被拆分。
以上就是 unwind 的基本用法和作用介绍,更多与 unwind 相关的知识可查阅官方文档 unwind。
out







网友评论