聚合是指同时处理多条数据,并对这些数据进行统计计算,最终返回一个统计结果。也就是说,聚合操作是将多个documents进行相关的各种类型的操作,并返回一个计算结果,这个过程就是聚合。 在MongoDB中,支持三种类型的聚合方式:聚合管道、Map-Reduce、简单聚合
一、聚合管道
聚合管道是指将Documents传入一个多阶段任务的管道中,经过管道中每个阶段的处理最终返回一个针对多个Document计算的结果。一个最简单的聚合管道包含两个部分:
①利用一个类似一个查询条件的query进行文档过滤;
②通过一个文档格式转换的任务,将文档以一种期望的形式输出。
此外,聚合管道还支持一些其他的功能,例如根据某些字段进行排序和分组等以及一些算数操作符进行数学计算。 聚合管道的工作流程如下:
该聚合管道包含两个部分:
match部分用于数据过滤(找出其中所有status为A的记录)
group部分用于分组(针对cust_id进行分组,并将同一个组的amount字段求和赋予sum字段返回)
二、Map-Reduce
除了标准的聚合管道外,MongoDB来支持了目前流行的Map-Reduce方式进行数据聚合。通常,map-reduce聚合包含两个阶段:
①map处理每个文档并为每个输入文档进行将其中一个或多个字段映射到数组中。
②reduce将map中映射到数据中的数据进行统计计算。
此外,map-reduce还可以有一个finalize阶段来对结果进行最终格式修改;map-reduce还可以指定查询条件来选择输入文档以及对结果进行排序和限制。
在上图所示的map-reduce中,第一步是根据搜索条件进行过滤,第二步是Map将字段映射至数组,第三步则是reduce对数组中的数据进行统计计算。
三、简单聚合
除了聚合管道与Map-Reduce外,针对单个Collection的一些简单汇聚功能,MongoDB本身也直接提供了一些汇聚函数。包括:db.collection.count() and db.collection.distinct()。
上图则表示找出orders中cust_id的存在的值的列表。
四、Mongo聚合与SQL对比
下面,我们将Mongo聚合与SQL进行对比,并用一些示例来加深对Mongo聚合功能的理解。
Mongo聚合 | SQL | 说明 |
WHERE | $match | 查询条件 |
GROUP BY | $group | 分组 |
HAVING | $match | 分组结果过滤条件 |
SELECT | $project | 返回结果字段过滤 |
ORDER BY | $sort | 排序 |
LIMIT | $limit | 记录限制 |
SUM() | $sum | 求和 |
COUNT() | $sum | 计数 |
join | $lookup | 连表查询 |
假设我们查询的Document的格式如下类似:
{
cust_id: "abc123",
ord_date: ISODate("2012-11-02T17:04:11.102Z"),
status: 'A',
price: 50,
items: [ { sku: "xxx", qty: 25, price: 1 },
{ sku: "yyy", qty: 25, price: 1 } ]
}
下面,我们来用一些实例来进行比较:
Demo1:
SQL: SELECT COUNT(*) AS count FROM orders
Mongo聚合:
db.orders.aggregate( [
{
$group: {
_id: null,
count: { $sum: 1 }
}
}
] )
功能描述:
查询orders表中记录的数目,count的计数方式为1*num,_id: null则表示无Group条件。
Demo2:
SQL: SELECT SUM(price) AS total FROM orders
Mongo聚合:
db.orders.aggregate( [
{
$group: {
_id: null,
total: { $sum: "$price" }
}
}
] )
功能描述:
查询orders表中所有记录price字段的和。total: { $sum: "$price" }
则表示将price字段之和作为total字段。
Demo3:
SQL: SELECT cust_id, SUM(price) AS total FROM orders GROUP BY cust_id
Mongo聚合:
db.orders.aggregate( [
{
$group: {
_id: "$cust_id",
total: { $sum: "$price" }
}
}
] )
功能描述:
根据cust_id字段进行分组,并将每个组的price字段进行求和作为total字段。_id: "$cust_id"
表示根据cust_id作为分组条件。
Demo4:
SQL: SELECT cust_id, SUM(price) AS total FROM orders GROUP BY cust_id ORDER BY total
Mongo聚合:
db.orders.aggregate( [
{
$group: {
_id: "$cust_id",
total: { $sum: "$price" }
}
},
{ $sort: { total: 1 } }
] )
功能描述:
根据cust_id字段进行分组,并将每个组的price字段进行求和作为total字段,并将最终结果按照total排序。
Demo5:
SQL:
SELECT cust_id,
ord_date,
SUM(price) AS total
FROM orders
GROUP BY cust_id,
ord_date
Mongo聚合:
db.orders.aggregate( [
{
$group: {
_id: {
cust_id: "$cust_id",
ord_date: {
month: { $month: "$ord_date" },
day: { $dayOfMonth: "$ord_date" },
year: { $year: "$ord_date"}
}
},
total: { $sum: "$price" }
}
}
] )
功能描述:
根据cust_id和ord_date两个字段进行组合条件分组,并按照分组对price字段进行求和。
Demo6:
SQL:
SELECT cust_id,
count(*)
FROM orders
GROUP BY cust_id
HAVING count(*) > 1
Mongo聚合:
db.orders.aggregate( [
{
$group: {
_id: "$cust_id",
count: { $sum: 1 }
}
},
{ $match: { count: { $gt: 1 } } }
] )
功能描述:
根据cust_id进行分组,并最终显示记录超过一条的结果。
Demo7:
SQL:
SELECT cust_id,
ord_date,
SUM(price) AS total
FROM orders
GROUP BY cust_id,
ord_date
HAVING total > 250
Mongo聚合:
db.orders.aggregate( [
{
$group: {
_id: {
cust_id: "$cust_id",
ord_date: {
month: { $month: "$ord_date" },
day: { $dayOfMonth: "$ord_date" },
year: { $year: "$ord_date"}
}
},
total: { $sum: "$price" }
}
},
{ $match: { total: { $gt: 250 } } }
] )
功能描述:
根据cust_id和ord_date两个字段进行联合分组,对每个组的结果中price字段求和作为total字段,并最终显示total结果大于250的结果。
Demo8:
SQL:
SELECT cust_id,
SUM(price) as total
FROM orders
WHERE status = 'A'
GROUP BY cust_id
Mongo聚合:
db.orders.aggregate( [
{ $match: { status: 'A' } },
{
$group: {
_id: "$cust_id",
total: { $sum: "$price" }
}
}
] )
功能描述:
首先找出所有status为A的字段,并将符合结果的记录按照cust_id进行分组,并将每个组的记录中price字段进行求和作为total字段。
Demo9:
SQL:
SELECT cust_id,
SUM(price) as total
FROM orders
WHERE status = 'A'
GROUP BY cust_id
HAVING total > 250
Mongo聚合:
db.orders.aggregate( [
{ $match: { status: 'A' } },
{
$group: {
_id: "$cust_id",
total: { $sum: "$price" }
}
},
{ $match: { total: { $gt: 250 } } }
] )
功能描述:
首先找出所有status为A的字段,并将符合结果的记录按照cust_id进行分组,并将每个组的记录中price字段进行求和作为total字段并仅显示total字段大约250的结果。
Demo10:
SQL:
SELECT cust_id,
SUM(li.qty) as qty
FROM orders o,
order_lineitem li
WHERE li.order_id = o.id
GROUP BY cust_id
Mongo聚合:
db.orders.aggregate( [
{ $unwind: "$items" },
{
$group: {
_id: "$cust_id",
qty: { $sum: "$items.qty" }
}
}
] )
功能描述:
根据cust_id字段进行分组,并且对每个分组下所有记录的items中每条记录的qty字段进行求和。
Ps:unwind的功能用于打散数组,即将数据中的每个元素作为一条记录看待进行后续处理。
Demo11:
SQL:
SELECT COUNT(*)
FROM (SELECT cust_id,
ord_date
FROM orders
GROUP BY cust_id,
ord_date)
as DerivedTable
Mongo聚合:
db.orders.aggregate( [
{
$group: {
_id: {
cust_id: "$cust_id",
ord_date: {
month: { $month: "$ord_date" },
day: { $dayOfMonth: "$ord_date" },
year: { $year: "$ord_date"}
}
}
}
},
{
$group: {
_id: null,
count: { $sum: 1 }
}
}
] )
功能描述:
多个group组合处理。(相当于Select的嵌套使用且多次汇聚)
五、操作符
##基础数据
>db.article.find().pretty()
{
"_id" : ObjectId("5c088fec651e67152257d453"),
"title" : "MongoDB Aggregate",
"author" : "simon",
"tags" : [
"Mongodb",
"Database",
"Query"
],
"pages" : 5.0,
"time" : ISODate("2017-06-11T16:00:00.000Z")
},
{
"_id" : ObjectId("5c088fec651e67152257d454"),
"title" : "MongoDB Index",
"author" : "simon",
"tags" : [
"Mongodb",
"Index",
"Query"
],
"pages" : 3.0,
"time" : ISODate("2018-11-11T16:00:00.000Z")
},
{
"_id" : ObjectId("5c088fec651e67152257d455"),
"title" : "MongoDB Query",
"author" : "Aaron",
"tags" : [
"Mongodb",
"Query"
],
"pages" : 8.0,
"time" : ISODate("2019-06-11T16:00:00.000Z")
}
1、$project 用于修改输入文档的结构。可以用来重命名、增加或删除字段(域),也可以用于创建计算结果以及嵌套文档。
返回的文档中只包含_id和tages
:db.article.aggregate([{$project:{_id:1,tags:1}}])
新增字段:db.article.aggregate([{$project:{_id:1,tags:1,editAuthor:'$author'}}])
editAuthor为新增加字段
2、$match用于过滤数据,只输出符合条件的文档。
查询出文档中 author 为 simon的数据:db.article.aggregate([{$match:{author:'simon'}}])
3、$group用于将集合中的文档分组,可用于统计结果
统计每个作者写的文章篇数:db.article.aggregate([{$group:{_id:'$author',total:{$sum:1}}}])
4、$sort 对集合中的文档进行排序
让集合按照页数进行升序排序:db.article.aggregate([{$sort:{pages:1}}])
降序 {pages:-1}
5、$unwind 将文档中数组类型的字段拆分成多条,每条文档包含数组中的一个值
将集合中 tags字段进行拆分:db.article.aggregate([{$match:{author:'Aaron'}},{$unwind:'$tags'}])
备注:
$unwind参数不是一个数组类型时,将会抛出异常
$unwind所作的修改,只用于输出,不能改变原文档
6、$limit限制返回文档的数量
返回集合的前一条文档:db.article.aggregate([{$limit: 1}])
7、$skip 跳过指定数量的文档,并返回余下的文档
跳过集合的前两个文档:db.article.aggregate([{$skip: 2}])
8、count 返回集合中的文档数量
统计product表中创建时间大于06/11/2019的数量值(下面三个都可以):db.getCollection('product').find({'createTime': {$gt: new Date('06/11/2019')}}).count()
db.getCollection('product').count({'createTime': {$gt: new Date('06/11/2019')}})
db.product.find({'createTime':{$gt:new Date('06/11/2019')}}).count()
9、distinct去重
对 name 值进行去重操作:db.getCollection('product').distinct('name')
①直接使用distinct 语句查询, 这种查询会将所有查询出来的数据返回给用户, 然后对查询出来的结果集求总数(耗内存,耗时一些)
格式:db.customer.distinct("去重字段",{条件}).length
示例:db.customer.distinct("phone",{"phone":{"$exists":1}}).length
备注:使用这种方法查询时,查询的结果集大于16M 时会查询失败,失败信息如下:
{"message" : "distinct failed: MongoError: distinct too big, 16mb cap","stack" : "script:1:20"}
②使用聚合函数,多次分组统计结果,最终将聚合的结果数返回给用户
示例:
db.customer.aggregate([
{$match:{"phone":{"$exists":1}}},
{$project:{"phone":true}},
{$group:{_id:"$phone"}},
{$group:{_id:null,count:{$sum:1}}}
])
备注:这种查询数据量大时就不会出现如上查询失败的情况,而且这种查询不管是内存消耗还是时间消耗都优于上面一种查询
更多请参考官网:https://docs.mongodb.com/manual/
或者:http://www.mongoing.com/docs/aggregation.html
或者:https://www.jianshu.com/p/f1d4300c0067