ps:以前都在iteye写博文,现在工作换了,转战前端,基本告别了java和python,看这边氛围还不错,就把那里的博客迁移过来了~~~
出于对性能的要求,公司希望把Mysql的数据迁移到MongoDB上,于是我开始学习Mongo的一些CRUD操作,由于第一次接触NoSQL,还是有点不习惯。
先吐个槽,公司的Mongo版本是2.6.4,而用的java驱动包版本是超级老物2.4版。当时一个“如何对分组后的文档进行筛选”这个需求头痛了很久,虽然shell命令下可以使用Aggregation很方便地解决,但是java驱动包从2.9.0版本才开始支持该特性,我至今没有找到不用Aggregation解决上述需求的办法。只能推荐公司升级驱动包版本,希望没有后续的兼容问题。
Mongo2.2版本后开始支持Aggregation Pipeline,而java驱动包从2.9.0版本才开始支持2.2的特性,2.9版本是12年发布的,mongodb在09年就出现了,似乎Mongo对java的开发者不怎么友好←_←
MongoDB目前提供了三个可以执行聚合操作的命令:aggregate、mapReduce、group。三者在性能和操作的优劣比较见官网提供的表格 Aggregation Commands Comparison,这里不再赘述细节。
aggregate、mapReduce、group原型及内部实现
我从官网总结出来了这三个函数的原型及底层封装的命令
函数名:db.collection.group()
函数原型:
1db.collection.group( 2 { 3 key, 4 reduce, 5 initial 6 [, keyf] 7 [, cond] 8 [, finalize] 9 } 10)
封装的命令:
1db.runCommand( 2 { 3 group: 4 { 5 ns: <namespace>, 6 key: <key>, 7 $reduce: <reduce function>, 8 $keyf: <key function>, 9 cond: <query>, 10 finalize: <finalize function> 11 } 12 } 13)
函数名:db.collection.mapReduce()
函数原型:
1db.collection.mapReduce( 2 <map>, 3 <reduce>, 4 { 5 out: <collection>, 6 query: <document>, 7 sort: <document>, 8 limit: <number>, 9 finalize: <function>, 10 scope: <document>, 11 jsMode: <boolean>, 12 verbose: <boolean> 13 } 14)
封装的命令:
1db.runCommand( 2 { 3 mapReduce: <collection>, 4 map: <function>, 5 reduce: <function>, 6 finalize: <function>, 7 out: <output>, 8 query: <document>, 9 sort: <document>, 10 limit: <number>, 11 scope: <document>, 12 jsMode: <boolean>, 13 verbose: <boolean> 14 } 15)
函数名:db.collection.aggregate()
函数原型:
1db.collection.aggregate( 2 pipeline, 3 options 4)
封装的命令:
1db.runCommand( 2 { 3 aggregate: "<collection>", 4 pipeline: [ <stage>, <...> ], 5 explain: <boolean>, 6 allowDiskUse: <boolean>, 7 cursor: <document> 8 } 9)
Mysql与MongoDB对聚合处理的对比
好记性不如烂笔头,下面通过操作来了解这几个函数和命令
1、准备测试数据
先准备SQL的测试数据(用来验证结果、比较SQL语句和NoSQL的异同):
先创建数据库表:
1create table dogroup ( 2 _id int, 3 name varchar(45), 4 course varchar(45), 5 score int, 6 gender int, 7 primary key(_id) 8);
插入数据:
1insert into dogroup (_id, name, course, score, gender) values (1, "N", "C", 5, 0); 2insert into dogroup (_id, name, course, score, gender) values (2, "N", "O", 4, 0); 3insert into dogroup (_id, name, course, score, gender) values (3, "A", "C", 5, 1); 4insert into dogroup (_id, name, course, score, gender) values (4, "A", "O", 6, 1); 5insert into dogroup (_id, name, course, score, gender) values (5, "A", "U", 8, 1); 6insert into dogroup (_id, name, course, score, gender) values (6, "A", "R", 8, 1); 7insert into dogroup (_id, name, course, score, gender) values (7, "A", "S", 7, 1); 8insert into dogroup (_id, name, course, score, gender) values (8, "M", "C", 4, 0); 9insert into dogroup (_id, name, course, score, gender) values (9, "M", "U", 7, 0); 10insert into dogroup (_id, name, course, score, gender) values (10, "E", "C", 7, 1);
接着准备MongoDB测试数据:
创建Collection(等同于SQL中的表,该行可以不写,Mongo会在插入数据时自动创建Collection)
db.createCollection("dogroup")
插入数据:
1db.dogroup.insert({"_id": 1,"name": "N",course: "C","score": 5,gender: 0}) 2db.dogroup.insert({"_id": 2,"name": "N",course: "O","score": 4,gender: 0}) 3db.dogroup.insert({"_id": 3,"name": "A",course: "C","score": 5,gender: 1}) 4db.dogroup.insert({"_id": 4,"name": "A",course: "O","score": 6,gender: 1}) 5db.dogroup.insert({"_id": 5,"name": "A",course: "U","score": 8,gender: 1}) 6db.dogroup.insert({"_id": 6,"name": "A",course: "R","score": 8,gender: 1}) 7db.dogroup.insert({"_id": 7,"name": "A",course: "S","score": 7,gender: 1}) 8db.dogroup.insert({"_id": 8,"name": "M",course: "C","score": 4,gender: 0}) 9db.dogroup.insert({"_id": 9,"name": "M",course: "U","score": 7,gender: 0}) 10db.dogroup.insert({"_id": 10,"name": "E",course: "C","score": 7,gender: 1})
以下操作可能逻辑上没有实际意义,主要是帮助熟悉指令
2、查询每门课程参与考试的人数
SQL写法:
select course as '课程名', count(*) as '数量' from dogroup group by course;
MongoDB写法:
① group方式
1db.dogroup.group({ 2key : { course: 1 }, 3initial : { count: 0 }, 4reduce : function Reduce(curr, result) { 5 result.count += 1; 6}, 7finalize : function Finalize(out) { 8 return {"课程名": out.course, "数量": out.count}; 9} 10});
返回的格式如下:
1{ 2 "课程名" : "C", 3 "数量" : 4 4}, 5{ 6 "课程名" : "O", 7 "数量" : 2 8}, 9{ 10 "课程名" : "U", 11 "数量" : 2 12}, 13{ 14 "课程名" : "R", 15 "数量" : 1 16}, 17{ 18 "课程名" : "S", 19 "数量" : 1 20}
② mapReduce方式
1db.dogroup.mapReduce( 2 function () { 3 emit( 4 this.course, 5 {course: this.course, count: 1} 6 ); 7 }, 8 function (key, values) { 9 var count = 0; 10 values.forEach(function(val) { 11 count += val.count; 12 }); 13 return {course: key, count: count}; 14 }, 15 { 16 out: { inline : 1 }, 17 finalize: function (key, reduced) { 18 return {"课程名": reduced.course, "数量": reduced.count}; 19 } 20 } 21)
这里把count初始化为1的原因是,MongoDB执行完map函数(第一个函数)后,如果key所对应的values数组的元素个数只有一个,reduce函数(第二个函数)将不会被调用。
返回的格式如下:
1{ 2 "_id" : "C", 3 "value" : { 4 "课程名" : "C", 5 "数量" : 4 6 } 7}, 8{ 9 "_id" : "O", 10 "value" : { 11 "课程名" : "O", 12 "数量" : 2 13 } 14}, 15{ 16 "_id" : "R", 17 "value" : { 18 "课程名" : "R", 19 "数量" : 1 20 } 21}, 22{ 23 "_id" : "S", 24 "value" : { 25 "课程名" : "S", 26 "数量" : 1 27 } 28}, 29{ 30 "_id" : "U", 31 "value" : { 32 "课程名" : "U", 33 "数量" : 2 34 } 35}
③ aggregate方式
1db.dogroup.aggregate( 2 { 3 $group: 4 { 5 _id: "$course", 6 "数量": { $sum: 1 } 7 } 8 } 9)
返回格式如下:
1{ "_id" : "S", "数量" : 1 } 2{ "_id" : "R", "数量" : 1 } 3{ "_id" : "U", "数量" : 2 } 4{ "_id" : "O", "数量" : 2 } 5{ "_id" : "C", "数量" : 4 }
以上三种方式中,group得到了我们想要的结果,mapReduce返回的结果只能嵌套在values里面,aggregate必须返回_id,无法为分组的字段指定别名,但是无疑第三种是最简单的。
虽然上面的问题不影响程序在前台展现数据,但是对于一个略微有强迫症的开发者确实难以忍受的。本人才疏学浅,刚接触Mongo,不知道后两者有没有可行的方法来获取想要的结果,希望网友指教。
3、查询Docouments(等同于SQL中记录)数大于2的课程
SQL写法:
select course, count(*) as count from dogroup group by course having count > 2;
MongoDB写法:
① aggregate方式(注意$group和$match的先后顺序)
1db.dogroup.aggregate({ 2 $group: { 3 _id: "$course", 4 count: { $sum: 1 } 5 } 6 },{ 7 $match: { 8 count:{ 9 $gt: 2 10 } 11 } 12});
目前尚未找到group和mapReduce对分组结果进行筛选的方法,欢迎网友补充
4、找出所有分数高于5分的考生数量及分数,返回的格式为“分数、数量”
SQL写法:
select score as '分数', count(distinct(name)) as '数量' from dogroup where score > 5 group by score;
MongoDB写法:
① group方式
1db.dogroup.group({ 2 key : { score: 1 }, 3 cond : { score: {$gt: 5} }, 4 initial : { name:[] }, 5 reduce : function Reduce(curr, result) { 6 var flag = true; 7 for(i=0;i<result.name.length&&flag;i++){ 8 if(curr.name==result.name[i]){ 9 flag = false; 10 } 11 } 12 // 如果result.name数组里面没有curr.name则添加curr.name 13 if(flag){ 14 result.name.push(curr.name); 15 } 16 }, 17 finalize : function Finalize(out) { 18 return {"分数": out.score, "数量": out.name.length}; 19 } 20});
② mapReduce方式
1db.dogroup.mapReduce( 2 function () { 3 if(this.score > 5){ 4 emit( 5 this.score, 6 {score: this.score, name: this.name} 7 ); 8 } 9 }, 10 function (key, values) { 11 var reduced = {score: key, names: []}; 12 var json = {};//利用json对象的key去重 13 for(i = 0; i < values.length; i++){ 14 if(!json[values[i].name]){ 15 reduced.names.push(values[i].name); 16 json[values[i].name] = 1; 17 } 18 } 19 return reduced; 20 }, 21 { 22 out: { inline : 1 }, 23 finalize: function (key, reduced) { 24 return {"分数": reduced.score, "数量": reduced.names?reduced.names.length:1}; 25 } 26 } 27)
③ aggregate方式
1db.dogroup.aggregate({ 2 $match: { 3 score: { 4 $gt: 5 5 } 6 } 7 },{ 8 $group: { 9 _id: { 10 score: "$score", 11 name: "$name" 12 } 13 } 14 },{ 15 $group: { 16 _id: { 17 "分数": "$_id.score" 18 }, 19 "数量": { $sum: 1 } 20 } 21});
弄熟上面这几个方法,大部分的分组应用场景应该没大问题了。
这张图示可以更直观地理解(点击看大图):
