7. 聚合( 重点 )

2017-09-26  本文已影响0人  简人CC

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"}
])

这类的操作就相当于实现了最早的数据表的复制操作.

上一篇下一篇

猜你喜欢

热点阅读