# MongoDB 聚合操作
## 一、聚合简述
在日常开发中,我们通常需要对存储数据进行聚合分析后,再返回给客户端。MongoDB提供了三种聚合的方式,分别是聚合管道,map-reduce 函数和单用途聚合方法。
## 二、聚合管道
MongoDB 的聚合操作类似于流水线处理,文档会依次进入多个管道阶段并执行相应的操作。这里先插入部分演示数据:
```shell
db.employees.insertMany([
{
emp_no: 10001,
name: {firstName:"Georgi",lastName:"Facello"},
age: 26,
gender: "F",
hobby: ["basketball", "football"]
},
{
emp_no: 10002,
name: {firstName:"Bezalel",lastName:"Simmel"},
age: 32,
gender: "M",
hobby: ["basketball", "tennis"]
},
{
emp_no: 10003,
name: {firstName:"Parto",lastName:"Bamford"},
age: 46,
gender: "M",
hobby: []
},
{
emp_no: 10004,
name: {firstName:"Chirstian",lastName:"Koblick"},
age: 40,
gender: "F",
hobby: ["football", "tennis"]
}
])
```
一个简单的聚合操作如下,这个聚合操作会经过两个阶段的数据处理:
+ 第一个管道阶段为 $match:会筛选出所有性别值为 F 的雇员的文档,然后输出到下一个管道操作中;
+ 第二个管道阶段为 $project:用于定义返回的字段内容,这里返回 fullname 字段,它由 `firstName + lastName` 组成。
```shell
db.employees.aggregate([
{ $match: { gender: "F" } },
{ $project:
{ fullName:
{ $concat: ["$name.firstName", "$name.lastName"]}
}
}
])
```
所以最后的输出结果如下:
```shell
{
"_id" : ObjectId("5d3fe6488ba16934ccce999d"),
"fullName" : "GeorgiFacello"
},
{
"_id" : ObjectId("5d3fe6488ba16934ccce99a0"),
"fullName" : "ChirstianKoblick"
}
```
在当前最新的 MongoDB 4.x 中,MongoDB 提供了将近 30 个管道阶段,用于满足不同数据处理的需求。以下主要介绍常用几个管道阶段,如果想要了解全部的管道阶段,可以参见官方文档:[Aggregation Pipeline Stages](https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/) 。
### 1.1 $match
$match 主要用于筛选符合条件的数据,通常应该把 $match 放在尽量靠前的位置,这时候它会利用索引来优化查询,同时还可以降低后续阶段所需要处理的数据量。示例如下:
```shell
db.employees.aggregate([
{ $match: { gender: "F" } }
])
```
### 1.2 $project
$project 主要用于定义需要返回的字段,1 代表包含该字段,0 代表不包含,除了可以作用于顶层字段外,还可以作用于内嵌字段。同时 $project 还支持使用表达式将多个字段或变量进行组合,并作为新的字段返回。示例如下:
```shell
db.employees.aggregate([
{
$project: {
_id: 0,
"name.firstName": 1,
gender: 1,
fullName: { $concat: ["$name.firstName", "$name.lastName"] }
}
}
])
```
从 MongoDB 3.6 开始,还可以在聚合表达式中使用 `$project + REMOVE` 来按照条件过滤返回字段,设置为 REMOVE 的字段将会从 $projection 的输出中排除。示例如下:
```shell
db.employees.aggregate([
{
$project: {
hobby: {
$cond: {
if: { $eq: [ [], "$hobby" ] },
then: "$$REMOVE",
else: "$hobby"
}
}
}
}
])
```
这里判断当文档的 hobby 属性为空数组时,其 hobby 属性不会被输出到下一个管道阶段。
### 1.3 $group
$group 管道阶段和大多数关系型数据库中的 group by 字句功能类似,都是用于分组计算。示例如下:
```shell
db.employees.aggregate(
[
{ $group : {
_id : "$gender",
totalAge: { $sum: "$age"},
avgAge: { $avg: "$age" },
count: { $sum: 1 }
}
}
]
)
```
上面的语句会按照性别进行分组,并计算分组后两组人的总年龄、平均年龄和总人数,输出如下:
```json
{
"_id" : "M",
"totalAge" : 78,
"avgAge" : 39,
"count" : 2
},
{
"_id" : "F",
"totalAge" : 66,
"avgAge" : 33,
"count" : 2
}
```
如果你想计算所有员工的年龄总和、平均年龄、以及员工总数,则可以将 $group 管道阶段的 _id 字段设置为 null ,如下:
```shell
db.employees.aggregate(
[
{ $group : {
_id : null,
totalAge: { $sum: "$age"},
avgAge: { $avg: "$age" },
count: { $sum: 1 }
}
}
]
)
# 输出如下
{
"_id" : null,
"totalAge" : 144,
"avgAge" : 36,
"count" : 4
}
```
### 1.4 $unwind
$unwind 将文档按照数组中的每一个元素进行拆分,类似于大多数流式计算中的 flatMap 算子。其语法格式如下:
```shell
{
$unwind:
{
path: ,
includeArrayIndex: ,
preserveNullAndEmptyArrays:
}
}
```
+ **path**:用于展开的数组字段;
+ **includeArrayIndex**:用于显示对应元素在原数组的位置信息;
+ **preserveNullAndEmptyArrays**:如果用于展开的字段值为 null 或空数组时,则对应的文档不会被输出到下一阶段。如果想要输出到下一阶段则需要将该属性设置为 true。示例语句如下:
```shell
db.employees.aggregate( [
{$project: {_id: 0, emp_no: 1, hobby:1}},
{ $unwind:
{ path: "$hobby",
includeArrayIndex: "arrayIndex",
preserveNullAndEmptyArrays: true
}
}
] )
```
此时输出内容如下。如果 preserveNullAndEmptyArrays 的值为 false 或者没有设置,则 10003 这条数据不会被输出:
```json
{"emp_no":10001,"hobby":"basketball","arrayIndex":0},
{"emp_no":10001,"hobby":"football","arrayIndex":1},
{"emp_no":10002,"hobby":"basketball","arrayIndex":0},
{"emp_no":10002,"hobby":"tennis","arrayIndex":1},
{"emp_no":10003,"arrayIndex":null},
{"emp_no":10004,"hobby":"football","arrayIndex":0},
{"emp_no":10004,"hobby":"tennis","arrayIndex":1}
```
### 1.5 $sort
$sort 主要用于排序操作,需要注意的是如果可以,应当尽量将该操作放置在管道的第一阶段,从而可以利用索引进行排序,否则就需要使用内存进行排序,这时排序操作就会变得相当昂贵,需要额外消耗内存和计算资源。示例如下:
```shell
db.employees.aggregate([
{$skip: 2} ,
{$sort: {age: 1}},
{$limit: 10}
])
```
### 1.6 $limit
限制返回文档的数量。
### 1.7 $skip
跳过一定数量的文档。
### 1.8 $lookup
#### 1. 关联查询
$lookup 类似与大多数关系型数据库中的 left outer join ,用于实现不同集合之间的连接。其基本的语法如下:
```shell
{
$lookup:
{
from: ,
localField: ,
foreignField: ,
as: