上一节简单介绍了一下mongoDB的增删改查操作,这一节将介绍其聚合操作。我们在使用mysql、sqlserver时经常会用到一些聚合函数,如sum/avg/max/min/count等,mongoDB也提供了丰富的聚合功能,让我们可以方便地进行数据的分析和计算。这里主要介绍两种聚合方式:聚合管道和MapReduce.

1 聚合管道

  官网文档:https://docs.mongodb.com/manual/core/aggregation-pipeline/

  聚合管道(aggregation pipeline),顾名思义是基于管道模式的聚合框架,简单的说就是在聚合管道中前一个阶段处理的结果会作为后一阶段处理的输入,documents通过管道中的各个阶段处理后得到最终的聚合结果。聚合管道的语法: db.aggregate( [ { stage1 },{ stage2} ... ] ) 。

  我们看一个官网提供的栗子就理解了:

//准备测试数据
db.orders.insertMany([
  {cust_id:"A123",amount:,status:"A"},
  {cust_id:"A123",amount:,status:"A"},
  {cust_id:"B212",amount:,status:"A"},
  {cust_id:"A123",amount:,status:"D"}
])
//聚合操作
db.orders.aggregate([
{$match:{status:"A"}},
{$group:{_id:"$cust_id",total:{$sum:"$amount"}}}
])

  执行上边的命令,结果如下:

  上边的聚合过程一共有两个阶段,如下图所示:

  第一阶段:$match会筛选出status="A"的documents,并把筛选出的document传给下一个阶段;

  第二阶段:$group将上一阶段传过来的documents通过cust_id进行分组,并合计各组的amount。

  通过官网的栗子我们大概知道了集合管道的基本执行流程,下边我们通过几个栗子来理解几个常用的聚合运算符。

栗子1:$lookup,$match,$project,$group,$sort,$skip,$limit,$out

  首先看一个订单库存的栗子,我们将分步查看各个阶段的聚合结果:

////准备测试数据
//订单
db.orders.insertMany([
{ "_id" : , "item" : "almonds", "price" : , "quantity" : },
{ "_id" : , "item" : "bread", "price" : , "quantity" : },
{ "_id" : , "item" : "pecans", "price" : , "quantity" : },
{ "_id" : , "item" : "pecans", "price" : , "quantity" : },
{ "_id" : , "item" : "cashews", "price" : , "quantity" :},
{ "_id" : }
]) //库存
db.inventory.insertMany([
{ "_id" : , "sku" : "almonds", description: "product 1", "instock" : },
{ "_id" : , "sku" : "bread", description: "product 2", "instock" : },
{ "_id" : , "sku" : "cashews", description: "product 3", "instock" : },
{ "_id" : , "sku" : "pecans", description: "product 4", "instock" : },
{ "_id" : , "sku": null, description: "Incomplete" },
{ "_id" : }
]) db.orders.aggregate([
//$lookup实现collection连接,作用类似于sql中的join
{
$lookup:
{
from: "inventory",//要join的集合
localField: "item",//连接时的本地字段
foreignField: "sku",//连接时的外部字段
as: "inventory_docs"//连接后添加数组字段的名字
}
},
//$match过滤document,结果集合只包含符合条件的集合
{$match:
{price:{$gt:}}
},
//$project用于获取结果字段,可以新增字段;也可以对已存在的字段重新赋值
{
$project:{
_id:,
item:,
price:,
quantity:,
totalprice:{$multiply:["$price","$quantity"]}
}
},
//$group实现分组,_id是必须的,用于指定分组的字段;这里查询各个分组的totalprice的最大值
{
$group:{_id:"$item",maxtotalprice:{$max:"$totalprice"}}
},
//$sort用于排序,1表示正序,-1表示倒序
{$sort:{maxtotalprice:-}},
//$skip用于跳过指定条数的document,和linq中的skip作用一样
{$skip:},
//limit用于指定传递给下一阶段的document条数,和mysql的limit作用一样
{$limit:},
//$out用于将结果存在指定的collection中,如果collection不存在则新建一个,如果存在则用结果集合覆盖以前的值
{$out:"resultCollection"}
])

 $ lookup阶段 用于collection连接,作用类似于sql中的join,经过该阶段结果如下:

[
{
"_id" : ,
"item" : "almonds",
"price" : ,
"quantity" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : "almonds",
"description" : "product 1",
"instock" :
}
]
},
{
"_id" : ,
"item" : "bread",
"price" : ,
"quantity" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : "bread",
"description" : "product 2",
"instock" :
}
]
},
{
"_id" : ,
"item" : "pecans",
"price" : ,
"quantity" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : "pecans",
"description" : "product 4",
"instock" :
}
]
},
{
"_id" : ,
"item" : "pecans",
"price" : ,
"quantity" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : "pecans",
"description" : "product 4",
"instock" :
}
]
},
{
"_id" : ,
"item" : "cashews",
"price" : ,
"quantity" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : "cashews",
"description" : "product 3",
"instock" :
}
]
},
{
"_id" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : null,
"description" : "Incomplete"
},
{
"_id" :
}
]
}
]

$match阶段 用于筛选出符合过滤条件的documents,相当于sql中的where

[
{
"_id" : ,
"item" : "almonds",
"price" : ,
"quantity" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : "almonds",
"description" : "product 1",
"instock" :
}
]
},
{
"_id" : ,
"item" : "bread",
"price" : ,
"quantity" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : "bread",
"description" : "product 2",
"instock" :
}
]
},
{
"_id" : ,
"item" : "pecans",
"price" : ,
"quantity" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : "pecans",
"description" : "product 4",
"instock" :
}
]
},
{
"_id" : ,
"item" : "pecans",
"price" : ,
"quantity" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : "pecans",
"description" : "product 4",
"instock" :
}
]
},
{
"_id" : ,
"item" : "cashews",
"price" : ,
"quantity" : ,
"inventory_docs" : [
{
"_id" : ,
"sku" : "cashews",
"description" : "product 3",
"instock" :
}
]
}
]

$project阶段 用于设置查询的字段,想到于sql中的select field1,field2...

[
{
"item" : "almonds",
"price" : ,
"quantity" : ,
"totalprice" :
},
{
"item" : "bread",
"price" : ,
"quantity" : ,
"totalprice" :
},
{
"item" : "pecans",
"price" : ,
"quantity" : ,
"totalprice" :
},
{
"item" : "pecans",
"price" : ,
"quantity" : ,
"totalprice" :
},
{
"item" : "cashews",
"price" : ,
"quantity" : ,
"totalprice" :
}
]

$group 用于分组,相当于sql中的group by,经过该阶段结果如下:

[
{ "_id" : "cashews","maxtotalprice" : },
{ "_id" : "pecans","maxtotalprice" : },
{ "_id" : "bread", "maxtotalprice" : },
{ "_id" : "almonds","maxtotalprice" : }
]

$sort阶段 用于排序,相当于sql中的 sort by,其中值为1表示正序,-1表示反序,经过该阶段结果如下:

[
{ "_id" : "pecans","maxtotalprice" : },
{ "_id" : "cashews","maxtotalprice" : },
{ "_id" : "bread", "maxtotalprice" : },
{ "_id" : "almonds","maxtotalprice" : }
]

$skip阶段 用于跳过指定条数的document,和linq中的skip作用一样,经过该阶段结果如下:

[
{ "_id" : "cashews", "maxtotalprice" : },
{ "_id" : "bread","maxtotalprice" : },
{ "_id" : "almonds", "maxtotalprice" : }
]

$limit阶段 用于指定传递给下一阶段的document条数,相当于mysql中的limit,和linq中的take作用一样,经过该阶段结果如下:

[
{"_id" : "cashews","maxtotalprice" : },
{"_id" : "bread","maxtotalprice" : }
]

$out阶段,用于将结果存在指定的collection中,如果collection不存在则新建一个,如果存在则用结果集合覆盖以前的值,这里我们将 db.resultCollection.find() 查看resultCollection,结果如下:

[
{ "_id" : "cashews", "maxtotalprice" : },
{ "_id" : "bread", "maxtotalprice" : }
]

栗子2:$addFields,$unwind,$count

  看一个用户分析的栗子,添加测试数据:

//测试数据
db.userinfos.insertMany([
{ _id:, name: "王五", age: , roles:["vip","gen" ]},
{ _id:, name: "赵六", age: , roles:["vip"]},
{ _id:, name: "田七", age: }
])

$addFields  用于给所有的document添加新字段,如果字段名已经存在的话,用新值替代旧值

db.userinfos.aggregate([
  {$addFields:{address:'上海',age:}}
])

  执行上边的命令后,结果如下:

[{"_id":,"name":"王五","age":,"roles":["vip","gen"],"address":"上海"},
{"_id":,"name":"赵六","age":,"roles":["vip"],"address":"上海"},
{"_id":,"name":"田七","age":,"address":"上海"}]

$unwind  $unwind用于对数组字段解构,数组中的每个元素都分解成一个单独的document

 db.userinfos.aggregate([
{$unwind:"$roles"}
])

  执行命令后,结果如下:

[{"_id" : ,"name" : "王五","age" : ,"roles" : "vip"},
{"_id" : ,"name" : "王五","age" : ,"roles" : "gen"},
{"_id" : ,"name" : "赵六","age" : ,"roles" : "vip"}]

$count 用于获取document的条数,输入的值为字段名字,用法十分简单

db.userinfos.aggregate([
{$count:"mycount"}
])

  执行命令后,结果如下:

[ { "mycount" :  } ]

栗子3 $bucket,$bucketAuto,$sample

  看一个网上书店的栗子,首先添加测试数据:

db.books.insertMany([
   { "_id" : , "title" : "《白鲸》", "artist" : "赫尔曼・梅尔维尔(美)", "year" : ,"price" : NumberDecimal("199.99") },
{ "_id" : , "title" : "《忏悔录》", "artist" : "让・雅克・卢梭(法)", "year" : ,"price" : NumberDecimal("280.00") },
{ "_id" : , "title" : "《罪与罚》", "artist" : "陀斯妥耶夫斯基(俄)", "year" : ,"price" : NumberDecimal("76.04") },
{ "_id" : , "title" : "《复活》", "artist" : "列夫・托尔斯泰(俄)","year" : ,"price" : NumberDecimal("167.30") },
{ "_id" : , "title" : "《九三年》", "artist" : "维克多・雨果(法)", "year" : ,"price" : NumberDecimal("483.00") },
{ "_id" : , "title" : "《尤利西斯》", "artist" : "詹姆斯・乔伊斯(爱尔兰)", "year" : ,"price" : NumberDecimal("385.00") },
{ "_id" : , "title" : "《魔山》", "artist" : "托马斯・曼(德)", "year" : /* No price*/ },
{ "_id" : , "title" : "《永别了,武器》", "artist" : "欧内斯特・海明威(美)", "year" : ,"price" : NumberDecimal("118.42") }
])

$bucket  按范围分组,手动指定各个分组的边界,用法如下:

db.books.aggregate( [
{
$bucket: {
groupBy: "$price", //用于分组的表达式,这里使用价格进行分组
boundaries: [ , , ], //分组边界,这里分组边界为[0,200),[200,400)和其他
default: "Other", //不在[0,200)和[200,400)范围内的数据放在_id为Other的bucket中
output: {
"count": { $sum: }, //bucket中document的个数
"titles" : { $push: {title:"$title",price:"$price"} } //bucket中的titles
}
}
}
] )

  执行上边的聚合操作,结果如下:

[
{
"_id" : ,
"count" : ,
"titles" : [
{"title" : "《白鲸》","price" : NumberDecimal("199.99")},
{"title" : "《罪与罚》","price" : NumberDecimal("76.04")},
{"title" : "《复活》","price" : NumberDecimal("167.30")},
{"title" : "《永别了,武器》","price" : NumberDecimal("118.42")}
]
},
{
"_id" : ,
"count" : ,
"titles" : [
{"title" : "《忏悔录》","price" : NumberDecimal("280.00")},
{"title" : "《尤利西斯》","price" : NumberDecimal("385.00")}
]
},
{
"_id" : "Other",
"count" : ,
"titles" : [
{"title" : "《九三年》","price" : NumberDecimal("483.00")},
{"title" : "《魔山》"}
]
}
]

$bucketAuto 和$bucket作用类似,区别在于$bucketAuto不指定分组的边界,而是指定分组的个数,分组边界是自动生成的

 db.books.aggregate([
{
$bucketAuto: {
groupBy: "$year",
buckets: ,
output:{
count:{$sum:},
title:{$push:{title:"$title",year:"$year"}}
}
}
}
])

  执行上边的聚合操作,结果如下:

[
{
"_id" : {
"min" : ,
"max" :
},
"count" : ,
"title" : [
{"title" : "《忏悔录》","year" : },
{"title" : "《白鲸》","year" : },
{"title" : "《罪与罚》","year" : }
]
},
{
"_id" : {
"min" : ,
"max" :
},
"count" : ,
"title" : [
{"title" : "《九三年》","year" : },
{"title" : "《复活》","year" : },
{"title" : "《尤利西斯》","year" : }
]
},
{
"_id" : {
"min" : ,
"max" :
},
"count" : ,
"title" : [
{"title" : "《魔山》","year" : },
{"title" : "《永别了,武器》","year" : }
]
}
]

$sample 用于随机抽取指定数量的document

db.books.aggregate([
{ $sample:{size:2} }
])

  执行后随即抽取了2条document,结果如下,注意因为是随即抽取的,所以每次执行的结果不同。

[{"_id":,"title":"《尤利西斯》","artist":"詹姆斯・乔伊斯(爱尔兰)","year":,"price":NumberDecimal("385.00")},
{"_id":,"title":"《永别了,武器》","artist":"欧内斯特・海明威(美)","year":,"price":NumberDecimal("118.42")}]

  通过上边三个栗子,我们应该已经对聚合管道有了一定的理解,其实各种聚合运算符的用法都比较简单,怎么灵活的组合各种聚合操作以达到聚合的目的才是我们考虑的重点。

2 mapReduce

  MapReduce是一种编程模型,用于大规模数据集的并行运算,MapReduce采用"分而治之"的思想,简单的说就是:将待处理的大数据集分为若干个小数据集,对各个小数据集进行计算获取中间结果,最后整合中间结果获取最终的结果。mongoDB也实现了MapReduce,用法还是比较简单的,语法如下:

db.collection.mapReduce(
function() {emit(key,value);}, //map 函数
function(key,values) {return reduceFunction}, //reduce 函数
{
out: collection, //输出
query: document, //查询条件,在Map函数执行前过滤出符合条件的documents
sort: document, //再Map前对documents进行排序
limit: number //发送给map函数的document条数
}
)

其中:map: 映射函数,遍历符合query查询条件的所有document,获取一系列的键值对key-values,如果一个key对应多个value,多个value以数组形式保存。

     reduce: 统计函数,reduce函数的参数是map函数发送的key/value集合

      out:  指定将统计结果存放在哪个collection中 (不指定则使用临时集合,在客户端断开后自动删除)。

     query:筛选条件,只有满足条件的文档才会发送给map函数;

     sort:和limit结合使用,在将documents发往map函数前先排序;

     limit:和sort结合使用,设置发往map函数的document条数。

  我们通过官网的栗子来理解mapReduce的用法,命令如下:

//添加测试数据
db.orders.insertMany([
  {cust_id:"A123",amount:,status:"A"},
  {cust_id:"A123",amount:,status:"A"},
  {cust_id:"B212",amount:,status:"A"},
  {cust_id:"A123",amount:,status:"D"}
])
//mapReduce
db.orders.mapReduce(
function() {emit(this.cust_id,this.amount)}, //map函数,遍历documents key为cust_id值,values为amount值,或者数组[amount1,amount2...]
function(key,values){return Array.sum(values)}, //reduce函数,返回合计结果(只会统计values是数组)
{
query:{status:"A"}, //过滤条件,只向map函数发送status="A"的documents
out:"myresultColl" //结果存放在myresultColl集合中,如果没有名字为myresultCOll的集合则新建一个,如果集合存在的话覆盖旧值
}
)
printjson(db.myresultColl.find() .toArray())

mongoDB的MapReduce可以简单分为两个阶段:

Map阶段:

  栗子中的map函数为 function() {emit(this.cust_id,this.amount)} ,执行map函数前先进行query过滤,找到 status=A 的documents,然后将过滤后的documents发送给map函数,map函数遍历documents,将cust_id作为key,amount作为value,如果一个cust_id有多个amount值时,value为数组[amount1,amount2..],栗子的map函数获取的key/value集合中有两个key/value对: {“A123”:[,]}和{“B212”:}

Reduce阶段:

  reduce函数封装了我们的聚合逻辑,reduce函数会逐个计算map函数传过去的key/value对,在上边栗子中的reduce函数的目的是计算amount的总和。

  上边栗子最终结果存放在集合myresultColl中(out的作用),通过命令 db.myresultColl.find() 查看结果如下:

  {"_id" : "A123","value" : },
  {"_id" : "B212","value" : }
]

  MapReduce属于轻量级的集合框架,没有聚合管道那么多运算符,聚合的思路也比较简单:先写一个map函数用于确定需要整合的key/value对(就是我们感兴趣的字段),然后写一个reduce函数,其内部封装着我们的聚合逻辑,reduce函数会逐一处理map函数获取的key/value对,以此获取聚合结果。

小结

  本节通过几个栗子简单介绍了mongoDB的聚合框架:集合管道和MapReduce,聚合管道提供了十分丰富的运算符,让我们可以方便地进行各种聚合操作,因为聚合管道使用的是mongoDB内置函数所以计算效率一般不会太差。需要注意:①管道处理结果会放在一个document中,所以处理结果不能超过16M(Bson格式的最大尺寸),②聚合过程中内存不能超过100M(可以通过设置{“allowDiskUse”: True}来解决);MapReduce的map函数和reduce函数都是我们自定义的js函数,这种聚合方式比聚合管道更加灵活,我们可以通过编写js代码来处理复杂的聚合任务,MapReduce的缺点是聚合的逻辑需要我们自己编码实现。综上,对于一些简单的固定的聚集操作推荐使用聚合管道,对于一些复杂的、大量数据集的聚合任务推荐使用MapReduce。

快速掌握mongoDB(二)——聚合管道和MapReduce的更多相关文章

  1. 【翻译】MongoDB指南/聚合——聚合管道

    [原文地址]https://docs.mongodb.com/manual/ 聚合 聚合操作处理数据记录并返回计算后的结果.聚合操作将多个文档分组,并能对已分组的数据执行一系列操作而返回单一结果.Mo ...

  2. MongoDB聚合管道(Aggregation Pipeline)

    参考聚合管道简介 聚合管道 聚合管道是基于数据处理管道模型的数据聚合框架.文档进入一个拥有多阶段(multi-stage)的管道,并被管道转换成一个聚合结果.最基本的管道阶段提供了跟查询操作类似的过滤 ...

  3. MongoDB入门---聚合操作&管道操作符&索引的使用

    经过前段时间的学习呢,我们对MongoDB有了一个大概的了解,接下来就要开始使用稍稍深入一点的东西了,首先呢,就是MongoDB中的聚合函数,跟mysql中的count等函数差不多.话不多说哈,我们先 ...

  4. MongoDB 聚合(管道与表达式)

    MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果.有点类似sql语句中的 count(*). aggregate() 方法 MongoDB中 ...

  5. Mongodb的聚合和管道

    MongoDB 聚合 MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果. aggregate() 方法 MongoDB中聚合的方法使用agg ...

  6. MongoDb进阶实践之八 MongoDB的聚合初探

    一.引言 好久没有写东西了,MongoDB系列的文章也丢下好长时间了.今天终于有时间了,就写了一篇有关聚合的文章.一说到“聚合”,用过关系型数据库的人都应该知道它是一个什么东西.关系型数据库有“聚合” ...

  7. MongoDB,分组,聚合

    使用聚合,db.集合名.aggregate- 而不是find 管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数.MongoDB的聚合管道将MongoDB文档在一个管道处理完 ...

  8. MongoDB的聚合操作以及与Python的交互

    上一篇主要介绍了MongoDB的基本操作,包括创建.插入.保存.更新和查询等,链接为MongoDB基本操作. 在本文中主要介绍MongoDB的聚合以及与Python的交互. MongoDB聚合 什么是 ...

  9. 【Mongodb】聚合查询 && 固定集合

    概述 数据存储是为了可查询,统计.若数据只需存储,不需要查询,这种数据也没有多大价值 本篇介绍Mongodb 聚合查询(Aggregation) 固定集合(Capped Collections) 准备 ...

随机推荐

  1. C#通过HttpListener实现HTTP监听

    代码: using NLog; using System; using System.Diagnostics; using System.IO; using System.Net; using Sys ...

  2. 通过NLayer和NAudio转换MP3成WAV

    NuGet安装: Install-Package NLayer.NAudioSupport 示例代码: using Microsoft.Win32; using NAudio.Wave; using ...

  3. Inno Setup制作最简单的安装程序

    目标就是[把exe程序放到制定目录,然后自动把工程需要的dll放到system32目录下,自动注册注册表.] 实现上述需求,用Inno Setup可以非常方便快捷实现. 安装Inno Setup. 点 ...

  4. RxJava入门优秀博客推荐

    RxJava用了快半年了,现在越来越离不开这个库,从使用到逐渐接触它的背后实现,突然想写点什么关于RxJava的内容.在酝酿如何组织内容的时候,就去看看自己关于RxJava的收藏,发现满满的干货! 1 ...

  5. Qt 5 最小构建笔记(只编译QtBase)

    只想用Qt5最基本的功能,因此只编译QtBase.也不想为了编译一个Qt装很多东西(比如非常肥的DirectX SDK) 软件清单: Visual Studio 2010 Professional w ...

  6. 跨进程访问VCL的一个用例(Delphi6、TurboDelphi测试通过)

    Controls.pas单元中有一个FindControl函数,通过句柄获得对应的TWinControl对象. function FindControl(Handle: HWnd): TWinCont ...

  7. qt开发的小软件,可以递归转换文件编码(qt为了防止内存泄露所做的保护机制)

    应用场景 当你下载别人的源码的时候,而别人的源码跟你自己电脑里面的编码不一致的情况下将会出现乱码,但是如果要一个个转换编码的话那么那样所需要花的时间太多,所以就有必要写一个软件递归遍历项目下面所有的文 ...

  8. UILabel实现自适应宽高需要注意的地方(三)

        一.需求图如下所示    UILabel 的高度自适应 UILabel中的段落间距可设置   图片效果如下:   调整段落适应长宽高方式:         需求:   保证"游戏玩法 ...

  9. 给你的 GitHub Repository 加上 sponsor 按钮

    「本文微信公众号 AndroidTraveler 首发」 背景 其实之前 GitHub 就已经说过要给开源的开发者提供赞助支持. 当你进入 GitHub 主页时,你会在右边发现一个 Tips. 点击进 ...

  10. Redis 学习笔记(篇二):字典

    字典 字典又称为符号表.关联数组或映射(map),是一种用于保存键值对(key-value)的数据结构. 那么 C 语言中有没有这样 key-value 型的内置数据结构呢? 答案:没有. 说起键值对 ...