7. 聚合( 重点 )
MongoDB的产生背景是在大数据环境,所谓的大数据实际上也就是进行的信息收集汇总。那么就必须存在有信息的统计操作,而这样的统计操作就称为聚合(直白: 分组统计就是一种聚合操作 ).
7.1 取得集合的数据量
对于集合的数据量而言, 在MongoDB里面直接使用count()函数就可以完成了。
范例: 统计students表中的数据量
db.students.count()
范例: 模糊查询
db.students.count({"name": /张/i})
在进行信息查询的时候, 不设置条件永远要比设置条件的查询快很多, 也就是说在之前的代码编写里面不管是查询全部还是模糊查询, 实际上最终都是用的模糊查询一种(没有设置关键字)。
7.2 消除重复数据
在学习SQL的时候对于重复的数据可以使用"DISTINCT", 那么这一操作在MongoDB之中依然支持.
范例: 查询所有name的信息
本次的操作没有直接的函数支持,只能够利用runCommand()函数.
db.runCommand({"distinct": "students", "key": "name})
此时实现了对于name数据的重复值的筛选.
7.3 group操作
使用"group"操作可以实现数据的分组操作, 在MongoDB里面会将集合一句指定的key的不同进行分组操作,并且每一个组都会产生一个处理的文档信息.
查询所有年龄大于等于19岁的学生信息,并且按照年龄分组
db.runCommand({"group": {
//指定要进行分组的集合
"ns": "students",
//指定文档分组的依据,这里是username键的值相等的被划分到一组, true为返回键username的值
"key": {"age": true},
//每一组reduce函数调用的初始个数,每一组的所有成员都会使用这个累加器。
"initial": {"count": 0},
//这个age的值大于等于19
"condition": {"age": {"$gte": 19}},
//每个文档都对应的调用一次.系统会传递两个参数: 当前文档和累加器文档
"$reduce": function(doc, prev) {
prev.count++
}
}})
db.students.group({
key: {sex: 1},
cond: {age: {"$age": 19}},
reduce: function(cur, result) { result.count ++ },
initial: {count: 0}
})
以上的操作代码里面实现的就属于一种MapReducer,但是这样只要根据传统的数据库的设计思路,实现了一个所谓的分组操作,但是这个分组操作的最终结果是有限的
7.3 MapReduce
Mapreduce是整个大数据的精髓所在(实际中别用 ),所谓的MapReducer就是分为两部处理数据:
Map: 将数据分别取出.
Reducer: 负责数据的最后处理.
可以要想在MongoDB里面实现MapReducer处理,那么复杂度是相当高的。
范例: 建立一组雇员数据
db.emps.insert({name: '张三',age: 30, sex: '男', job: 'CLERK', salary: 1000})
db.emps.insert({name: '李四',age: 28, sex: '女', job: 'Manger', salary: 9000})
db.emps.insert({name: '王五',age: 26, sex: '男', job: 'CLERK', salary: 1000})
db.emps.insert({name: '赵六',age: 32, sex: '女', job: 'PRESIDENT', salary: 5000})
db.emps.insert({name: '孙七',age: 31, sex: '女', job: 'CLERK', salary: 1000})
db.emps.insert({name: '王八',age: 35, sex: '男', job: 'Manger', salary: 1000})
使用MapReducer操作最终会将处理结果保存在一个单独的集合里面, 而最终的处理效果如下.
范例: 按照职位分组, 取得每个职位的人名
编写分组的定义
var jobMapFun = function () {
emit(this.job, this.name) //按照job分组, 取出name
}
第一组: {key: "CLERK", values: [姓名,姓名,...]}
第二步: 编写reducer操作
var jobReducerFun = function() {
return {"job": key, "names": values}
}
第三步: 针对于MapReducer处理完成的数据实际上也可以执行一个最后处理.
var jobFinalizeFun = function(key, values) {
if(key == "PRESIDENT") {
return {obj: key, names: values, info: '公司的老大'}
}
return {obj: key, names: values}
}
进行操作的整合
db.runCommand({
"mapreduce": "emps",
"map": jobMapFun,
"reduce": jobReducerFun,
"out": "t_job_map",
"finalize": jobFinalizeFun
})
现在执行之后, 所有的处理结果都保存在了"t_job_emp"集合里面.
db.t_job_emp.find().pretty()
范例: 统计出各性别的人数、平均工资、最低工资、雇员工资
var sexMapFun = function() {
//定义好了分组的条件,以及每个集合要取出的内容
emit(this.sex, {"count": 1, "csal": this.salary, "cmax":this.salary, "cmin": this.salary, "cname': this.name}
}
var sexRedecerFun = function(key, values) {
var total = 0 //统计
var sum = 0 //计算总工资
var max = values[0].NaNax//假设第一个数据是最高工资
var min = values[0]NaNin//假设第一个数据是最低工资
var names = [] //定义数组内容
for(var x in values) {
total +=values[x].count//人数增加
sum += values[x].csal//累加工资
if(max < values[x]NaNax) { //不是最高工资
max = values[x]NaNax
}
if(min > values[x]NaNin) { //不是最低工资
min = values[x]NaNin
}
}
var avg = (sum / total ).toFixed(2)
}
db.runCommand({
"mapreduce": "emps",
"map": sexMapFun,
"reduce": sexReduceFun,
"out": "t_sex_emp"
})
虽然大数据的时代提供有强悍的MapReduce支持,但是从现实的开发来讲,真的不可能使用起来。
7.5 聚合框架(核心)
MapReduce功能强大, 但是它的复杂度和功能一样强大, 那么很多时候我们需要MapReduce的功能,可以又不想把代码写得太复杂, 所以从Mongo 2.X版本之后之后开始引入了聚合框架并且提供了聚合函数: aggregate()
7.5.1 $group
group主要进行分组的数据操作。
范例: 实现聚和查询的功能 - 求出每个职位的雇员人数
db.emps.aggregate([{"$group": {"_id": "$job",job_count: {"$sum": 1}}}])
这样的操作更加复合与传统的group by 子句的操作.
范例: 求出每个职位的总工资
db.emps.aggregate([{"$group": {"_id": "$sex", salary_count: {"$sum": "$salary"}}}])
在整个聚合框架里面如果要引用每行的数据使用: "$字段名称"
范例: 计算出每个职位的平均工资
db.emps.aggregate([{
"$group": {
"_id": "$job",
"job_sal": {"$sum": "$salary"},
"job_avg": {"$avg": "$salary"}
}
}])
范例: 求出最高与最低工资
db.emps.aggregate([{
"$group": {
"_id": "$job",
"max_sal": {"$max": "$salary"},
"min_sal": {"$min": "$salary"}
}
}])
以上的几个与SQL类似的操作计算就成功的实现了。
范例: 计算出每个职位的工资数据(数组显示)
db.emps.aggregate([{
"$group": {
"_id": "job",
"sal_data": {"$push": "$salary"}
}
}])
范例: 求出每个职位的人员
db.emps.aggregate([{
"$group": {
"_id": "job",
"sal_data": {"$push": "$name"}
}
}])
使用"$push"的确可以将数据变为数组进行保存,但是有一股问题出现了, 重复的内容也会进行保存,那么在MongoDB里面保存有取消重复的设置.
范例: 取消重复的数据
db.emps.aggregate([{
"$group": {
"_id": "job",
"sal_data": {"$addToSet": "$name"}
}
}])
默认情况行下是将所有数据的数据都保存进去了, 但是现在只希望可以保留第一个或者最后一个。
保留第一个内容
db.emps.aggregate([{
"$group": {
"_id": "job",
"sal_data": {"$first": "$name"}
}
}])
保留最后一个内容
db.emps.aggregate([{
"$group": {
"_id": "job",
"sal_data": {"$last": "$name"}
}
虽然可以方便的实现分组处理, 但是有一点需要注意,所有的分组数据是无序的, 并且都是在内存之中完成的, 所以不可能支持大数据量.
7.5.2 $project
可以利用"$project" 来控制数据列的显示规则,那么可以执行的规则如下:
|- 普通列({成员: 1 | true}}): 便是要显示的内容;
|- "_id"列({"_id": 0 | false}): 表示"_id"列是否显示;
|- 条件过滤器({成员: 表达式}):满足表达式之后的数据可以进行显示.
范例: 只显示name、job列, 不显示"_id"列
db.emps.aggregate([{"$project": {
"_id": 0,
"name": 1
}}])
此时只有设置进去的列才可以被显示出来, 而其他的列不能够被显示出来。实际上这就属于数据库的投影机制。
实际上在进行数据投影的过程里面也支持四则运算:加法("$add" )、减法("$subtract")、乘法("$multipty")、除法("$divide" )、求模($mod).
范例: 观察四则运算
db.emps.aggregate([{
"$group": {
"_id": "$job",
"max_sal": {"$max": "$salary"},
"min_sal": {"$min": "$salary"}
}
}])
db.emps.aggregate([{
"$group": {
"_id": "job",
"sal_data": {"$push": "$name"}
}
}])
db.emps.aggregate([{
"$group": {
"_id": "job",
"sal_data": {"$addToSet": "$name"}
}
}])
db.emps.aggregate([{
"$group": {
"_id": "job",
"sal_data": {"$last": "$name"}
}
}])
db.emps.aggregate([{
"$project": {
"_id": 0,
"name": 1,
"job": 1,
"salary": 1
}
}])
除了四则运算之外也支持如下的各种运算符:
关系运算:大小比较(“$cmp”)、等于(“$eq”)、大于(“$gt”)、大于等于(“$gte”)、小于(“$lt”)、小于等于(“$lte”)、不等于(“$ne”)、判断NULL(“$ifNull”),这些返回的结果都是布尔型数据;
·逻辑运算:与(“$and”)、或(“$or”)、非(“$not”);
·字符串操作:连接(“$concat”)、截取(“$substr”)、转小写(“$toLower”)、转大写(“toUpper”)、不区分大小写比较(“$strcasecmp”)。
范例: 找出所有工资大于等于2000的雇员姓名、年龄、工资
db.emps.aggregate([{
"$project": {
"_id":0,
"name": 1,
"salary": 1,
"age": 1,
"salary": {"$gte": ["$salary", 2000]}
}
}])
范例:查询职位是manger的信息
MongoDB中的数据是区分大小写的;
db.emps.aggregate([{
"$project": {
"_id": 0,
"name": 1,
"age": 1,
"job": {
"$eq": ["$job", {"$toUpper": "Manger"}]
}
}
}])
db.emps.aggregate([{
"$project": {
"_id": 0,
"name": 1,
"job": {"$strcasecmp": ["$job", "Manger"]}
}
}])
范例:使用字符串截取
db.emps.aggregate([{
"$project": {
"_id": 0,
"name": 1,
"job": {"$substr": ["$job", 0, 3]}
}
}])
利用"$project" 实现的投影操作功能相当强大,所有可以出现的操作几乎都能够使用。
7.5.3 $match
"$match" 是一个滤波操作,就是进行WHERE的过滤.
范例:查询工资在2000 ~5000的雇员
db.emps.aggregate([
{
"$match": {
"salary": {"$gte": 2000, "$lte": 5000}
}
}
])
这个时候实现的代码严格来讲只是相当于“SELECT * FROM 表 WHERE 条件”,属于所有的内容都进行了查询。
范例:控制投影操作
db.emps.aggregate([
{
"$match": {
"salary": {"$gte": 2000, "$lte": 5000}
}
},
{
"$project": {
"_id": 0,
name: 1,
age: 1
}
}
])
此时相当于实现了“SELECT字段 FROM ... WHERE”语句结构。
范例: 继续分组
db.emps.aggregate([
{
"$match": {
"salary": {"$gte": 2000, "$lte": 5000}
}
},
{
"$project": {
"_id": 0,
name: 1,
age: 1
}
},
{
"$group": {
"_id": "$job",
"count": {"$sum": 1},
"avg": {
"$avg": "$salary"
}
}
}
])
通过一些列的演示可以总结一点
"$project":相当于SELECT子句;
"$match":相当于WHERE子句;
"$group": 相当于GROUP BY子句。
7.5.4 $sort
使用"$sort" 可以实现排序,设置为1表示升序,设置为-1表示降序.
范例: 实现排序
db.emps.aggregate([{ "$sort": {"age": -1, "salary": 1}}])
范例: 将所有操作一起使用
db.emps.aggregate([
{
"$match": {
"salary": {"$gte": 2000, "$lte": 5000}
}
},
{
"$project": {
"_id": 0,
name: 1,
age: 1
}
},
{
"$group": {
"_id": "$job",
"count": {"$sum": 1},
"avg": {
"$avg": "$salary"
}
}
},
{
"$sort": {"count": -1}
}
])
此时实现了降序排序,使用的是生成定义的别名。
7.5.5 分页处理: $limit、$skip
“$limit”:负责数据的取出个数;
“$skip”:数据的跨过个数。
范例: 使用"$limit"设置取出的个数
db.emps.aggregate([
{
"$project": {"_id": 0, "name": 1, "salary": 1, "job": 1},
},
{
"$limit": 2
}
])
跨过3行数据
db.emps.aggregate([
{
"$project": {"_id": 0, "name": 1, "salary": 1, "job": 1},
},
{
"$skip": 3
}
{
"$limit": 2
}
])
综合运用
db.emps.aggregate([
{
"$match": {
"salary": {"$gte": 2000, "$lte": 5000}
}
},
{
"$project": {
"_id": 0,
name: 1,
age: 1
}
},
{
"$group": {
"_id": "$job",
"count": {"$sum": 1},
"avg": {
"$avg": "$salary"
}
}
},
{
"$sort": {"count": -1}
},
{
"$skip": 3
}
{
"$limit": 2
}
])
范例: 7.5.6 $unwind
在查询数据的时候经常会返回数组信息,但是数组并不方便信息的浏览,所以提供有"$unwind"可以将数组变为独立的字符串内容
添加一些信息
db.nw.insert({"title": "技术部", "bus": ["研发", "生产", "培训"]})
db.nw.insert({"title": "财务部", "bus": ["工资", "税收"]})
范例: 将信息进行转化
db.nw.aggregate([
{
"$project": {"_id": 0, "title": true, "bus": true},
},
{
"$unwind": "$bus"
}
])
此时相当于将数组中的数据变为了单行数据.
5.5.7 $geoNear
使用"$geoNear"可以得到附近的坐标点
范例:准备测试数据
db.shop.drop()
db.shop.insert({loc: [10, 10]})
db.shop.insert({loc: [11, 10]})
db.shop.insert({loc: [10, 11]})
db.shop.insert({loc: [12, 15]})
db.shop.insert({loc: [16, 17]})
db.shop.insert({loc: [90, 90]})
db.shop.insert({loc: [120, 130]})
db.shop.ensureIndex({"loc": "2d"})
范例: 设置查询
db.shop.aggregate([
{
$geoNear: {
near: { type: "Point", coordinates: [ 11 , 11 ] },
distanceField: "dist.calculated",
maxDistance: 2,
query: { type: "public" },
includeLocs: "dist.location",
num: 5,
spherical: true
}
}
])
地理信息的检索必须存在有索引的支持。
7.5.8 $out
“$out”:利用此操作可以将查询结果输出到指定的集合里面。
范例: 将投影的结果输出到集合里
db.emps.aggregate([
{"$project": {"_id": 0, "name": 1, "salary": 1, "job": 1}},
{"$out": "emp_infos"}
])
这类的操作就相当于实现了最早的数据表的复制操作.