03. 聚合
MongoDB 聚合操作
一、聚合简述
在日常开发中,我们通常需要对存储数据进行聚合分析后,再返回给客户端。
二、聚合管道
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
组成。
db.employees.aggregate([
{ $match: { gender: "F" } },
{ $project:
{ fullName:
{ $concat: ["$name.firstName", "$name.lastName"]}
}
}
])
所以最后的输出结果如下:
{
"_id" : ObjectId("5d3fe6488ba16934ccce999d"),
"fullName" : "GeorgiFacello"
},
{
"_id" : ObjectId("5d3fe6488ba16934ccce99a0"),
"fullName" : "ChirstianKoblick"
}
在当前最新的
1.1 $match
db.employees.aggregate([
{ $match: { gender: "F" } }
])
1.2 $project
db.employees.aggregate([
{
$project: {
_id: 0,
"name.firstName": 1,
gender: 1,
fullName: { $concat: ["$name.firstName", "$name.lastName"] }
}
}
])
从$project + REMOVE
来按照条件过滤返回字段,设置为
db.employees.aggregate([
{
$project: {
hobby: {
$cond: {
if: { $eq: [ [], "$hobby" ] },
then: "$$REMOVE",
else: "$hobby"
}
}
}
}
])
这里判断当文档的
1.3 $group
db.employees.aggregate(
[
{ $group : {
_id : "$gender",
totalAge: { $sum: "$age"},
avgAge: { $avg: "$age" },
count: { $sum: 1 }
}
}
]
)
上面的语句会按照性别进行分组,并计算分组后两组人的总年龄、平均年龄和总人数,输出如下:
{
"_id" : "M",
"totalAge" : 78,
"avgAge" : 39,
"count" : 2
},
{
"_id" : "F",
"totalAge" : 66,
"avgAge" : 33,
"count" : 2
}
如果你想计算所有员工的年龄总和、平均年龄、以及员工总数,则可以将
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:
{
path: <field path>,
includeArrayIndex: <string>,
preserveNullAndEmptyArrays: <boolean>
}
}
- path:用于展开的数组字段;
- includeArrayIndex:用于显示对应元素在原数组的位置信息;
- preserveNullAndEmptyArrays:如果用于展开的字段值为
null 或空数组时,则对应的文档不会被输出到下一阶段。如果想要输出到下一阶段则需要将该属性设置为true 。示例语句如下:
db.employees.aggregate( [
{$project: {_id: 0, emp_no: 1, hobby:1}},
{ $unwind:
{ path: "$hobby",
includeArrayIndex: "arrayIndex",
preserveNullAndEmptyArrays: true
}
}
] )
此时输出内容如下。如果
{"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
db.employees.aggregate([
{$skip: 2} ,
{$sort: {age: 1}},
{$limit: 10}
])
1.6 $limit
限制返回文档的数量。
1.7 $skip
跳过一定数量的文档。
1.8 $lookup
1. 关联查询
{
$lookup:
{
from: <collection to join>,
localField: <field from the input documents>,
foreignField: <field from the documents of the "from" collection>,
as: <output array field>
}
}
- from:指定同一数据库中的集合以进行连接操作;
- localField:连接集合中用于进行连接的字段;
- foreignField:待连接集合中用于进行连接的字段;
- as:指定用于存放匹配文档的新数组字段的名称。如果指定的字段已存在,则进行覆盖。
为了
db.titles.insertMany([
{
emp_no: 10001,
title: "Senior Engineer"
},
{
emp_no: 10002,
title: "Staff"
},
{
emp_no: 10003,
title: "Senior Engineer"
},
{
emp_no: 10004,
title: "Engineer"
},
{
emp_no: 10004,
title: "Senior Engineer"
}
])
执行连接查询,连接条件为员工工号:
db.employees.aggregate([
{
$lookup:
{
from: "titles",
localField: "emp_no",
foreignField: "emp_no",
as: "emp_title"
}
}
])
输出结果如下,为节省篇幅和突出重点,这里只显示部分内容,下文亦同。从输出中可以看到员工的职位信息都作为内嵌文档插入到指定的
{
"_id" : ObjectId("5d3ffaaa8ba16934ccce99a1"),
"emp_no" : 10001,
........
"emp_title" : [
{
"_id" : ObjectId("5d4011728ba16934ccce99a5"),
"emp_no" : 10001,
"title" : "Senior Engineer"
}
]
},
.........
{
"_id" : ObjectId("5d3ffaaa8ba16934ccce99a4"),
"emp_no" : 10004,
........
"emp_title" : [
{
"_id" : ObjectId("5d4011728ba16934ccce99a8"),
"emp_no" : 10004,
"title" : "Engineer"
},
{
"_id" : ObjectId("5d4011728ba16934ccce99a9"),
"emp_no" : 10004,
"title" : "Senior Engineer"
}
]
}
2. 非相关查询
除了关联查询外,
{
$lookup:
{
from: <collection to join>,
let: { <var_1>: <expression>, …, <var_n>: <expression> },
pipeline: [ <pipeline to execute on the collection to join> ],
as: <output array field>
}
}
- from:指定同一数据库中的集合以进行连接操作。
- let:可选操作。用于指定在管道阶段中使用的变量,需要注意的是访问
let 变量必须使用$expr 运算符。 - pipeline:必选值,用于指定要在已连接集合上运行的管道。需要注意的是该管道无法直接访问输入文档中的字段,需要先在
let 中进行定义,然后再引用。 - as:指定用于存放匹配文档的新数组字段的名称。如果指定的字段已存在,则进行覆盖。
这里我们分别基于三种场景来讲解不相关查询:
场景一:假设每个员工都需要了解其他员工的职位信息,以便进行沟通,则对应的查询语法如下:
db.employees.aggregate([
{
$lookup:
{
from: "titles",
pipeline: [],
as: "emps_title"
}
}
])
此时输出如下,可以看到每个员工的
{
"_id" : ObjectId("5d3ffaaa8ba16934ccce99a1"),
"emp_no" : 10001,
.......
"emps_title" : [
{
"_id" : ObjectId("5d4011728ba16934ccce99a5"),
"emp_no" : 10001,
"title" : "Senior Engineer"
},
{
"_id" : ObjectId("5d4011728ba16934ccce99a6"),
"emp_no" : 10002,
"title" : "Staff"
},
{
"_id" : ObjectId("5d4011728ba16934ccce99a7"),
"emp_no" : 10003,
"title" : "Senior Engineer"
},
{
"_id" : ObjectId("5d4011728ba16934ccce99a8"),
"emp_no" : 10004,
"title" : "Engineer"
},
{
"_id" : ObjectId("5d4011728ba16934ccce99a9"),
"emp_no" : 10004,
"title" : "Senior Engineer"
}
]
},
{
"_id" : ObjectId("5d3ffaaa8ba16934ccce99a2"),
"emp_no" : 10002,
.......
"emps_title" : [
{
"_id" : ObjectId("5d4011728ba16934ccce99a5"),
"emp_no" : 10001,
"title" : "Senior Engineer"
},
{
"_id" : ObjectId("5d4011728ba16934ccce99a6"),
"emp_no" : 10002,
"title" : "Staff"
},
{
"_id" : ObjectId("5d4011728ba16934ccce99a7"),
"emp_no" : 10003,
"title" : "Senior Engineer"
},
{
"_id" : ObjectId("5d4011728ba16934ccce99a8"),
"emp_no" : 10004,
"title" : "Engineer"
},
{
"_id" : ObjectId("5d4011728ba16934ccce99a9"),
"emp_no" : 10004,
"title" : "Senior Engineer"
}
]
},
..........
场景二:假设每个员工都只需要知道办公室职员
db.employees.aggregate([
{
$lookup:
{
from: "titles",
pipeline: [
{ $match:
{ $expr: { $eq: [ "$title","Staff"]}}
}
],
as: "emps_title"
}
}
])
场景三:假设只有男员工需要知道其他职员的信息,此时就需要利用
db.employees.aggregate([
{
$lookup:
{
from: "titles",
let: { gender: "$gender"},
pipeline: [
{ $match:
{ $expr: { $eq: [ "$$gender","M"]}}
}
],
as: "emps_title"
}
}
])
这里需要使用两个 $
符号对
1.9 $out
- 创建临时集合;
- 将索引从现有集合复制到临时集合;
- 将文档插入临时集合中;
- 调用
db.collection.renameCollection(target, true)
方法将临时集合重命名为目标集合。
db.employees.aggregate([
{ $out: "emps"}
])
1.10 自动优化
在大多数情况下
$project or $addFields + $match
当投影操作后面有匹配操作时,
$sort + $match
当排序操作后面有匹配操作时,会将匹配操作提前,以减少需要排序的数据量。
$project + $skip
当投影操作后面有跳过操作时,会先执行跳过操作,从而减少需要进行投影操作的数据量。
$sort + $limit
当排序操作在限制操作之前时,如果没有中间阶段会修改文档数量
了解这些优化策略可以有助于我们在开发中合理设置管道的顺序。想要了解全部的优化策略,可以参阅
三、MapReduce
这里我们以按照性别分组计算员工的平均年龄为例,演示
db.employees.mapReduce(
function(){
emit(this.gender,this.age)
},
function(key,values){
return Array.avg(values)
},
{ out:"age_avg"}
)
对应的计算结果会被输出到
{
"_id" : "F",
"value" : 33
},
{
"_id" : "M",
"value" : 39
}
当前
四、单用途聚合方法
出于易用性考虑,
4.1 count
用于计算符合条件的文档的数量,如果想要计算集合中所有文档的数量,则传入空对象即可:
db.employees.count({gender:"M"})
4.2 estimatedDocumentCount
官方更加推荐使用
db.employees.estimatedDocumentCount({})
4.3 distinct
和大多数数据库中的
db.titles.distinct("title")
参考资料
- 官方文档:Aggregation、Map-Reduce
- 所有聚合管道总览:https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/
- 聚合管道中所有可选操作符:https://docs.mongodb.com/manual/reference/operator/aggregation/