玩转MongoDB 计算

MongoDB属于 NoSql 中的基于分布式文件存储的文档型数据库,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 json 的 bson 格式,因此可以存储比较复杂的数据类型。Mongo 最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,但是写起来并不简单。若能集算器 SPL 语言结合,处理起来就相对容易多了。

        现在我们针对 MongoDB 在计算方面的问题进行讨论分析,通过集算器 SPL 语言加以改进,方便用户使用 MongoDB。现从如下情况加以说明:

1. 单表内嵌数组结构的统计............................................... 1

2. 单表内嵌文档求和......................................................... 3

3. 分段分组结构................................................................ 5

4. 同构表合并................................................................... 6

5. 关联嵌套结构情况 1...................................................... 8

6. 关联嵌套结构情况 2..................................................... 10

7. 关联嵌套结构情况 3..................................................... 11

8. 多字段分组统计........................................................... 14

9. 两表关联查询............................................................... 16

10. 多表关联查询............................................................. 17

11. 指定数组查找............................................................. 19

12. 关联表中的数组查找................................................... 20

1. 单表内嵌数组结构的统计

对嵌套数组结构中的数据统计处理。查询考试科目的平均分及每个学生的总成绩情况。

测试数据:

期待统计结果:

脚本:

db.student.aggregate( [

{\$unwind:"\$scroe"},

{\$group: {

"_id":   {"lesson":"\$scroe.lesson"} ,

"qty":{"\$avg":"\$scroe.mark"}

}

}

] )

db.student.aggregate( [

{\$unwind:"\$scroe"},

{\$group: {

"_id": {"name":"\$name"} ,

"qty":{"\$sum":"\$scroe.mark"}

}

}

] )

由于各科分数 scroe 是按课目、成绩记录的数组结构,统计前需要将它拆解,将每科成绩与学生对应,然后再实现分组计算。这需要熟悉 unwind 与 group 组合的应用。

SPL 脚本:

按课目统计的总分数

每个学生的总成绩

脚本说明:

A1:连接 mongo 数据库。

A2:获取 student 表中的数据。

A3:将 scroe 数据合并成序表,再按课程分组,计算平均分。

A4:统计每个学生的成绩后返回列名为 NAME、TOTAL 的序表。new 函数表示生成新序表。

A5:关闭数据库连接。

这个比较常用嵌套结构统计的例子许多人遭遇过、需要先拆解,主要是熟悉 mongodb 对嵌套数据结构的处理。

2. 单表内嵌文档求和

对内嵌文档中的数据求和处理, 下面要统计每条记录的 income,output 的数量和。

测试数据:

期待统计结果

Mongodb脚本:

varfields = ["income","output"];

db.computer.aggregate([

{

\$project:{

"values":{

\$filter:{

input:{

"\$objectToArray":"\$\$ROOT"

},

cond:{

\$in:[

"\$\$this.k",

fields

]

}

}

}

}

},

{

\$unwind:"\$values"

},

{

\$project:{

key:"\$values.k",

values:{

"\$sum":{

"\$let":{

"vars":{

"item":{

"\$objectToArray":"\$values.v"

}

},

"in":"\$\$item.v"

}

}

}

}

},

{\$sort: {"_id":-1}},

{"\$group": {

"_id":"\$_id",

'income':{"\$first":"\$values"},

"output":{"\$last":"\$values"}

}},

]);

filter将income,output 部分信息存放到数组中,用 unwind 拆解成记录,再累计各项值求和,按 _id 分组合并数据。

SPL 脚本:

统计结果

脚本说明:

A1:连接数据库

A2:获取 computer 表中的数据

A3:将 income、output 字段中的数据分别转换成序列求和,再与 ID 组合生成新序表

A4:关闭数据库连接。

获取子记录的字段值,然后求和,相对于 mongo 脚本简化了不少。这个内嵌文档与内嵌数组在组织结构上有点类似,不小心容易混淆,注意与上例中的 scroe 数组结构比较,写出的脚本有所不同。

3. 分段分组结构

统计各段内的记录数量。下面按销售量分段,统计各段内的数据量,数据如下:

分段方法:0-3000;3000-5000;5000-7500;7500-10000;10000 以上。

期望结果:

Mongo 脚本

vara_count=0;

varb_count=0;

varc_count=0;

vard_count=0;

vare_count=0;

db.sales.find({

}).forEach(

function(myDoc){

if(myDoc.SALES <3000)   {

a_count +=1;

}

elseif(myDoc.SALES <5000)   {

b_count +=1;

}

elseif(myDoc.SALES   <7500) {

c_count +=1;

}

elseif(myDoc.SALES   <10000) {

d_count +=1;

}

else{

e_count +=1;

}

}

);

print("a_count="+a_count)

print("b_count="+b_count)

print("c_count="+c_count)

print("d_count="+d_count)

print("e_count="+e_count)

这个需求按条件分段分组,mongodb 没有提供对应的 api,实现起来有点繁琐,上面的程序是其中实现的一个例子参考,当然也可以写成其它实现形式。下面看看集算器脚本的实现。

SPL 脚本:

脚本说明:

      A1:定义 SALES 分组区间。

      A2:连接 mongodb 数据库。

      A3:获取 sales 表中的数据。

      A4:根据 SALES 区间分组统计员工数。其中函数 pseg()表示返回成员在序列中的区段序号,int() 表示转换成整数。

      A5:关闭数据库连接。

pseg 的使用让 SPL 脚本精简了不少。

4. 同构表合并

具有相同结构的多表数据合并。下面将两个员工表数据合并。

Emp1:

Emp2:

合并数据结果:

Mongo 脚本:

db.emp1.aggregate([

{"\$limit":1},

{"\$facet": {

"collection1": [

{"\$limit":1},

{"\$lookup": {

"from":"emp1",

"pipeline": [{"\$match": {} }],

"as":"collection1"

}}

],

"collection2": [

{"\$limit":1},

{"\$lookup": {

"from":"emp2",

"pipeline": [{"\$match": {} }],

"as":"collection2"

}}

]

}},

{"\$project": {

"data": {

"\$concatArrays": [

{"\$arrayElemAt": ["\$collection1.collection1",0]   },

{"\$arrayElemAt": ["\$collection2.collection2",0]   },

]

}

}},

{"\$unwind":"\$data"},

{"\$replaceRoot": {"newRoot":"\$data"} }

])

通过 facet 将两表数据先存入各自的数组中,然后 concatArrays 将数组合并,unwind 拆解子记录后,并将它呈现在最外层。SPL 脚本实现则没有那么多“花样”。

SPL 脚本:

脚本说明:

A1:连接 mongodb 数据库。

A2:获取 emp1 表中的数据。

A3:获取 emp2 表中的数据。

A4:合并两表数据。

A5:关闭数据库连接。

熟悉 sql 语句的 mongo 初学者面对数据合并的 mongo 脚本,估计首次遇到时有点“懵”,SPL 脚本就显得自然易懂了。

5. 关联嵌套结构情况 1

两个关联表,表 A 与表 B 中的内嵌文档信息关联, 且返回的信息在内嵌文档中。表 childsgroup 字段 childs 是嵌套数组结构,需要合并的信息 name 在其下。

history:

childsgroup:

表History中的child_id与表childsgroup中的childs.id关联,希望得到下面结果:

{

“_id” : ObjectId(“5bab2ae8ab2f1bdb4f434bc3”),

“id” : “001”,

“history” : “today worked”,

“child_id” : “ch001”,

“childInfo” :

{

“name” : “a”

}

………………

}

Mongo 脚本

db.history.aggregate([

{\$lookup: {

from:"childsgroup",

let: {child_id:"\$child_id"},

pipeline: [

{\$match: {   \$expr: { \$in: ["\$\$child_id","\$childs.id"] } } },

{\$unwind:"\$childs"},

{\$match: {   \$expr: { \$eq: ["\$childs.id","\$\$child_id"] } } },

{\$replaceRoot: {   newRoot:"\$childs.info"} }

],

as:"childInfo"

}},

{"\$unwind":"\$childInfo"}

])

这个脚本用了几个函数lookup、pipeline、match、unwind、replaceRoot处理,一般 mongodb 用户不容易写出这样复杂脚本;那我们再看看 spl 脚本的实现:

SPL 脚本:

关联查询结果:

脚本说明:

      A1:连接 mongodb 数据库。

      A2:获取 history 表中的数据。

      A3:获取 childsgroup 表中的数据。

      A4:将 childsgroup 中的 childs 数据提取出来合并成序表。

      A5:表 history 中的 child_id 与表 childs 中的 id 关联查询,追加 name 字段, 返回序表。

      A6:关闭数据库连接。

相对 mongodb 脚本写法,SPL 脚本的难度降低了不少,省去了熟悉有关 mongo 函数的用法,如何去组合处理数据等,节约了不少时间。

6. 关联嵌套结构情况 2

两个关联表,表 A 与表 B 中的内嵌文档信息关联, 将信息合并到内嵌文档中。表 txtPost 字段 comment 是嵌套数组结构,需要把 comment_content 合并到其下。

txtComment:

txtPost

期望结果:

Mongo 脚本

db.getCollection("txtPost").aggregate([

{"\$unwind":"\$comment"},

{"\$lookup": {

"from":"txtComment",

"localField":"comment.comment_no",

"foreignField":"comment_no",

"as":"comment.comment_content"

}},

{"\$unwind":"\$comment.comment_content"},

{"\$addFields": {"comment.comment_content":

"\$comment.comment_content.comment_content"}},

{"\$group": {

"_id":"\$_id",

'post_no':{"\$first":"\$post_no"},

"comment": {"\$push":"\$comment"}

}},

]).pretty()

表txtPost 按 comment 拆解成记录,然后与表 txtComment 关联查询,将其结果放到数组中,再将数组拆解成记录,将comment_content 值移到 comment 下,最后分组合并。

SPL 脚本:

脚本说明:

      A1:连接 mongodb 数据库。

      A2:获取 txtPost 表中的数据。

      A3:获取 txtComment 表中的数据。

      A4:将序表 A2 下的 comment 与 post_no 组合成序表,其中 post_no 改名为 pno。

      A5:序表 A4 通过 comment_no 与序表 A3 关联,追加字段 comment_content,将其改名为 Content。

      A6:按 pno 分组返回序表,~ 表示当前记录。

      A7:关闭数据库连接。

7. 关联嵌套结构情况 3

两个关联表,表 A 与表 B 中的内嵌文档信息关联, 且返回的信息在记录上。表 collection2 字段 product 是嵌套数组结构,返回的信息是 isCompleted 等字段。

测试数据:

collection1:

{

_id: '5bc2e44a106342152cd83e97',

description:

{

status: 'Good',

machine: 'X'

},

order: 'A',

lot: '1'

};

collection2:

{

_id: '5bc2e44a106342152cd83e80',

isCompleted: false,

serialNo: '1',

batchNo: '2',

product: [ // note the subdocuments here

{order: 'A', lot: '1'},

{order: 'A', lot: '2'}

]

}

期待结果

{

_id: 5bc2e44a106342152cd83e97,

description:

{

status: 'Good',

machine: 'X',

},

order: 'A',

lot: '1' ,

isCompleted: false,

serialNo: '1',

batchNo: '2'

}

Mongo 脚本

db.collection1.aggregate([{

\$lookup:   {

from:"collection2",

let:   {order:"\$order", lot:"\$lot"},

pipeline:   [{

\$match:   {

\$expr:{  \$in: [ { order:"\$\$order", lot:"\$\$lot"},"\$product"] }

}

}],

as:"isCompleted"

}

},   {

\$addFields:   {

"isCompleted":   {\$arrayElemAt: ["\$isCompleted",0] }

}

},   {

\$addFields:   {// add the required fields to the top level structure 

"isCompleted":"\$isCompleted.isCompleted",

"serialNo":"\$isCompleted.serialNo",

"batchNo":"\$isCompleted.batchNo"

}

}])

lookup 两表关联查询,首个 addFields获取isCompleted数组的第一个记录,后一个addFields 转换成所需要的几个字段信息

SPL 脚本:

脚本说明:

      A1:连接 mongodb 数据库。

      A2:获取 collection1 表中的数据。

      A3:获取 collection2 表中的数据。

      A4:根据条件 order, lot 从序表 A2 中查询记录,然后追加序表 A3 中的字段serialNo, batchNo,返回合并后的序表。

      A5:关闭数据库连接。

实现从数据记录中的内嵌结构中筛选,将符合条件的数据合并成新序表。

8. 多字段分组统计

统计分类项下的总数及各子项数。下面统计按 addr 分类 book 数及其下不同的 book 数。

期望结果:

Mongo 脚本

db.books.aggregate([

{"\$group": {

"_id": {

"addr":"\$addr",

"book":"\$book"

},

"bookCount": {"\$sum":1}

}},

{"\$group": {

"_id":"\$_id.addr",

"books": {

"\$push": {

"book":"\$_id.book",

"count":"\$bookCount"

},

},

"count": {"\$sum":"\$bookCount"}

}},

{"\$sort": {"count": -1} },

{"\$project": {

"books": {"\$slice": ["\$books",2] },

"count":1

}}

]).pretty()

先按 addr,book 分组统计 book 数,再按 addr 分组统计 book 数,调整显示顺序

SPL脚本:

计算结果:

脚本说明:

A1:连接 mongodb 数据库。

A2:获取books表中的数据。

A3:按 addr,book 分组统计 book 数,

A4:再按 addr 分组统计 book 数。

A5:将 A4 中的 Total 按 addr 关联后合并到序表中。

A6:关闭数据库连接。

9. 两表关联查询

从关联表中选择所需要的字段组合成新表。

Collection1:

collection2:

期望结果:

Mongo 脚本

db.c1.aggregate([

{"\$lookup": {

"from":"c2",

"localField":"user1",

"foreignField":"user1",

"as":"collection2_doc"

}},

{"\$unwind":"\$collection2_doc"},

{"\$redact": {

"\$cond": [

{"\$eq": ["\$user2","\$collection2_doc.user2"] },

"\$\$KEEP",

"\$\$PRUNE"

]

}},

{"\$project": {

"user1":1,

"user2":1,

"income":"\$income",

"output":"\$collection2_doc. output"

}}

]).pretty()

lookup 两表进行关联查询,redact 对记录根据条件进行遍历处理,project 选择要显示的字段。

SPL脚本:

脚本说明:

A1:连接 mongodb 数据库。

A2:获取c1表中的数据。

A3:获取c2表中的数据。

A4:两表按字段 user1,user2 关联,追加序表 A3 中的 output 字段,返回序表。

A5:关闭数据库连接。

通过 join 把两个关联表不同的字段合并成新表。

10. 多表关联查询

多于两个表的关联查询,结合成一张大表。

Doc1:

Doc2:

Doc3:

合并后的结果:

{

"_id" : ObjectId("5901a4c63541b7d5d3293766"),

"firstName" : "shubham",

"lastName" : "verma",

"address" : {

"address" : "Gurgaon"

},

"social" : {

"fbURLs" : "http://www.facebook.com",

"twitterURLs" : "http://www.twitter.com"

}

}

Mongo 脚本

db.doc1.aggregate([

{\$match:   { _id: ObjectId("5901a4c63541b7d5d3293766") } },

{

\$lookup:

{

from:"doc2",

localField:"_id",

foreignField:"userId",

as:"address"

}

},

{

\$unwind:"\$address"

},

{

\$project: {

"address._id":0,

"address.userId":0,

"address.mob":0

}

},

{

\$lookup:

{

from:"doc3",

localField:"_id",

foreignField:"userId",

as:"social"

}

},

{

\$unwind:"\$social"

},

{

\$project:   {

"social._id":0,

"social.userId":0

}

}

]).pretty();

由于 Mongodb 数据结构原因,写法也多样化,展示也各不相同。

SPL 脚本:

此脚本与上面例子类似,只是多了一个关联表,每次 join 就新增加字段,最后叠加构成一张大表。.

SPL 脚本的简洁性、统一性就非常明显。

11. 指定数组查找

从指定的数组中查找符合条件的记录。所给的数组为:["Chemical", "Biology", "Math"]。

测试数据:

期望结果:

Mongodb 脚本

var field = ["Chemical","Biology","Math"]

db.student.aggregate([

{"\$project": {

"name":1,

"lessons": {

"\$filter": {

"input":"\$lesson",

"cond": {

"\$in": [

"\$\$this",

field

]

}

}

},

}},

{"\$project":   {"name":1,"lessons":1,"sizeOflesson":   {"\$size":"\$lessons"} }},

{  \$match: {"sizeOflesson":{ \$gt:0}}}

])

查询选修课包含["Chemical", "Biology", "Math"]的同学。

SPL 脚本:

脚本说明:

A1:定义查询条件科目数组。

A2:连接 mongodb 数据库。

A3:获取 student 表中的数据。

A4:查询存在数组中的科目记录。

A5:生成字段为 name, lesson 的新序表,其中符合条件的值存放在字段 lesson 中

A6:关闭数据库连接。

集算器对给定数组中查询记录的实现更简明易懂。

12. 关联表中的数组查找

从关联表记录数据组中查找符合条件的记录, 用给定的字段组合成新表。

测试数据:

users:

workouts:

期望结果:

Mongo 脚本

db.users.aggregate([

{"\$lookup": {

"from":"workouts",

"localField":"workouts",

"foreignField":"_id",

"as":"workoutDocumentsArray"

}},

{\$project: {   _id:0,workouts:0} } ,

{"\$unwind":"\$workoutDocumentsArray"},;

{"\$replaceRoot": {"newRoot":  { \$mergeObjects:   ["\$\$ROOT","\$workoutDocumentsArray"] } }

},

{$project: {   workoutDocumentsArray:0} }

]).pretty()

把关联表 users,workouts 查询结果放到数组中,再将数组拆解,提升子记录的位置,去掉不需要的字段。

SPL 脚本:

脚本说明:

      A1:连接 mongodb 数据库。

      A2:获取 users 表中的数据。

      A3:获取 workouts 表中的数据。

      A4:查询序表 A3 的 _id 值存在于序表 A2 中 workouts 数组的记录, 并追加 name 字段, 返回合并的序表。

      A5:关闭数据库连接。

由于需要获取序列的交集不为空为条件,故将 _id 转换成序列。

        Mongo 存储的数据结构相对关联数据库更复杂、更灵活,其提供的查询语言也非常强、能适应不同的情况,需要了解函数也不少,函数之间的结合更是变化无穷,因此要掌握并熟悉应用它并非易事。集算器的离散性、易用性恰好能弥补 Mongo 这方面的不足,它降低了 mongo 学习成本及使用 mongo 操作的复杂度、难度,让 mongo 的功能得到更充分的展现,同时也希望 mongo 越来越受到广大爱好者的青睐。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,790评论 2 89
  • 一、MongoDB简介 1.概述 ​ MongoDB是一个基于分布式文件存储的数据库,由C++语言编写。旨在为WE...
    郑元吉阅读 964评论 0 2
  • 知识点:美化输出:db.stu.find().pretty() $project:修改输入文档的结构。可以用来重命...
    胖虎很可爱阅读 675评论 0 1
  • 简介 MongoDB 是一个基于分布式文件存储的NoSQL数据库 由C++语言编写,运行稳定,性能高 旨在为 WE...
    大熊_7d48阅读 33,166评论 1 8
  • 早上7:42闹铃起床,周末过后有点懈怠,随笔又写的不积极,晚上很被动。把今天相关的几件事记录一下。 今天听了许岑如...
    Maker在杭州阅读 118评论 0 0