MongoDB屬于 NoSql 中的基于分布式文件存儲的文檔型數(shù)據(jù)庫,是非關(guān)系數(shù)據(jù)庫當(dāng)中功能最豐富,最像關(guān)系數(shù)據(jù)庫的。它支持的數(shù)據(jù)結(jié)構(gòu)非常松散,是類似 json 的 bson 格式,因此可以存儲比較復(fù)雜的數(shù)據(jù)類型。Mongo 最大的特點(diǎn)是它支持的查詢語言非常強(qiáng)大,其語法有點(diǎn)類似于面向?qū)ο蟮牟樵冋Z言,幾乎可以實(shí)現(xiàn)類似關(guān)系數(shù)據(jù)庫單表查詢的絕大部分功能,但是寫起來并不簡單。若能集算器 SPL 語言結(jié)合,處理起來就相對容易多了。
? ? ? ? 現(xiàn)在我們針對 MongoDB 在計(jì)算方面的問題進(jìn)行討論分析,通過集算器 SPL 語言加以改進(jìn),方便用戶使用 MongoDB?,F(xiàn)從如下情況加以說明:
1. 單表內(nèi)嵌數(shù)組結(jié)構(gòu)的統(tǒng)計(jì)............................................... 1
2. 單表內(nèi)嵌文檔求和......................................................... 3
3. 分段分組結(jié)構(gòu)................................................................ 5
4. 同構(gòu)表合并................................................................... 6
5. 關(guān)聯(lián)嵌套結(jié)構(gòu)情況 1...................................................... 8
6. 關(guān)聯(lián)嵌套結(jié)構(gòu)情況 2..................................................... 10
7. 關(guān)聯(lián)嵌套結(jié)構(gòu)情況 3..................................................... 11
8. 多字段分組統(tǒng)計(jì)........................................................... 14
9. 兩表關(guān)聯(lián)查詢............................................................... 16
10. 多表關(guān)聯(lián)查詢............................................................. 17
11. 指定數(shù)組查找............................................................. 19
12. 關(guān)聯(lián)表中的數(shù)組查找................................................... 20
1. 單表內(nèi)嵌數(shù)組結(jié)構(gòu)的統(tǒng)計(jì)
對嵌套數(shù)組結(jié)構(gòu)中的數(shù)據(jù)統(tǒng)計(jì)處理。查詢考試科目的平均分及每個(gè)學(xué)生的總成績情況。
測試數(shù)據(jù):

期待統(tǒng)計(jì)結(jié)果:

腳本:
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"}
}
}
] )
由于各科分?jǐn)?shù) scroe 是按課目、成績記錄的數(shù)組結(jié)構(gòu),統(tǒng)計(jì)前需要將它拆解,將每科成績與學(xué)生對應(yīng),然后再實(shí)現(xiàn)分組計(jì)算。這需要熟悉 unwind 與 group 組合的應(yīng)用。
SPL 腳本:

按課目統(tǒng)計(jì)的總分?jǐn)?shù)

每個(gè)學(xué)生的總成績

腳本說明:
A1:連接 mongo 數(shù)據(jù)庫。
A2:獲取 student 表中的數(shù)據(jù)。
A3:將 scroe 數(shù)據(jù)合并成序表,再按課程分組,計(jì)算平均分。
A4:統(tǒng)計(jì)每個(gè)學(xué)生的成績后返回列名為 NAME、TOTAL 的序表。new 函數(shù)表示生成新序表。
A5:關(guān)閉數(shù)據(jù)庫連接。
這個(gè)比較常用嵌套結(jié)構(gòu)統(tǒng)計(jì)的例子許多人遭遇過、需要先拆解,主要是熟悉 mongodb 對嵌套數(shù)據(jù)結(jié)構(gòu)的處理。
2. 單表內(nèi)嵌文檔求和
對內(nèi)嵌文檔中的數(shù)據(jù)求和處理, 下面要統(tǒng)計(jì)每條記錄的 income,output 的數(shù)量和。
測試數(shù)據(jù):

期待統(tǒng)計(jì)結(jié)果

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 部分信息存放到數(shù)組中,用 unwind 拆解成記錄,再累計(jì)各項(xiàng)值求和,按 _id 分組合并數(shù)據(jù)。
SPL 腳本:

統(tǒng)計(jì)結(jié)果

腳本說明:
A1:連接數(shù)據(jù)庫
A2:獲取 computer 表中的數(shù)據(jù)
A3:將 income、output 字段中的數(shù)據(jù)分別轉(zhuǎn)換成序列求和,再與 ID 組合生成新序表
A4:關(guān)閉數(shù)據(jù)庫連接。
獲取子記錄的字段值,然后求和,相對于 mongo 腳本簡化了不少。這個(gè)內(nèi)嵌文檔與內(nèi)嵌數(shù)組在組織結(jié)構(gòu)上有點(diǎn)類似,不小心容易混淆,注意與上例中的 scroe 數(shù)組結(jié)構(gòu)比較,寫出的腳本有所不同。
3. 分段分組結(jié)構(gòu)
統(tǒng)計(jì)各段內(nèi)的記錄數(shù)量。下面按銷售量分段,統(tǒng)計(jì)各段內(nèi)的數(shù)據(jù)量,數(shù)據(jù)如下:

分段方法:0-3000;3000-5000;5000-7500;7500-10000;10000 以上。
期望結(jié)果:

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)
這個(gè)需求按條件分段分組,mongodb 沒有提供對應(yīng)的 api,實(shí)現(xiàn)起來有點(diǎn)繁瑣,上面的程序是其中實(shí)現(xiàn)的一個(gè)例子參考,當(dāng)然也可以寫成其它實(shí)現(xiàn)形式。下面看看集算器腳本的實(shí)現(xiàn)。
SPL 腳本:

腳本說明:
??????A1:定義 SALES 分組區(qū)間。
??????A2:連接 mongodb 數(shù)據(jù)庫。
??????A3:獲取 sales 表中的數(shù)據(jù)。
??????A4:根據(jù) SALES 區(qū)間分組統(tǒng)計(jì)員工數(shù)。其中函數(shù) pseg()表示返回成員在序列中的區(qū)段序號,int() 表示轉(zhuǎn)換成整數(shù)。
??????A5:關(guān)閉數(shù)據(jù)庫連接。
pseg 的使用讓 SPL 腳本精簡了不少。
4. 同構(gòu)表合并
具有相同結(jié)構(gòu)的多表數(shù)據(jù)合并。下面將兩個(gè)員工表數(shù)據(jù)合并。
Emp1:

Emp2:

合并數(shù)據(jù)結(jié)果:

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 將兩表數(shù)據(jù)先存入各自的數(shù)組中,然后 concatArrays 將數(shù)組合并,unwind 拆解子記錄后,并將它呈現(xiàn)在最外層。SPL 腳本實(shí)現(xiàn)則沒有那么多“花樣”。
SPL 腳本:

腳本說明:
A1:連接 mongodb 數(shù)據(jù)庫。
A2:獲取 emp1 表中的數(shù)據(jù)。
A3:獲取 emp2 表中的數(shù)據(jù)。
A4:合并兩表數(shù)據(jù)。
A5:關(guān)閉數(shù)據(jù)庫連接。
熟悉 sql 語句的 mongo 初學(xué)者面對數(shù)據(jù)合并的 mongo 腳本,估計(jì)首次遇到時(shí)有點(diǎn)“懵”,SPL 腳本就顯得自然易懂了。
5. 關(guān)聯(lián)嵌套結(jié)構(gòu)情況 1
兩個(gè)關(guān)聯(lián)表,表 A 與表 B 中的內(nèi)嵌文檔信息關(guān)聯(lián), 且返回的信息在內(nèi)嵌文檔中。表 childsgroup 字段 childs 是嵌套數(shù)組結(jié)構(gòu),需要合并的信息 name 在其下。
history:

childsgroup:

表History中的child_id與表childsgroup中的childs.id關(guān)聯(lián),希望得到下面結(jié)果:
{
“_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"}
])
這個(gè)腳本用了幾個(gè)函數(shù)lookup、pipeline、match、unwind、replaceRoot處理,一般 mongodb 用戶不容易寫出這樣復(fù)雜腳本;那我們再看看 spl 腳本的實(shí)現(xiàn):
SPL 腳本:

關(guān)聯(lián)查詢結(jié)果:

腳本說明:
??????A1:連接 mongodb 數(shù)據(jù)庫。
??????A2:獲取 history 表中的數(shù)據(jù)。
??????A3:獲取 childsgroup 表中的數(shù)據(jù)。
??????A4:將 childsgroup 中的 childs 數(shù)據(jù)提取出來合并成序表。
??????A5:表 history 中的 child_id 與表 childs 中的 id 關(guān)聯(lián)查詢,追加 name 字段, 返回序表。
??????A6:關(guān)閉數(shù)據(jù)庫連接。
相對 mongodb 腳本寫法,SPL 腳本的難度降低了不少,省去了熟悉有關(guān) mongo 函數(shù)的用法,如何去組合處理數(shù)據(jù)等,節(jié)約了不少時(shí)間。
6. 關(guān)聯(lián)嵌套結(jié)構(gòu)情況 2
兩個(gè)關(guān)聯(lián)表,表 A 與表 B 中的內(nèi)嵌文檔信息關(guān)聯(lián), 將信息合并到內(nèi)嵌文檔中。表 txtPost 字段 comment 是嵌套數(shù)組結(jié)構(gòu),需要把 comment_content 合并到其下。
txtComment:

txtPost

期望結(jié)果:

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 關(guān)聯(lián)查詢,將其結(jié)果放到數(shù)組中,再將數(shù)組拆解成記錄,將comment_content 值移到 comment 下,最后分組合并。
SPL 腳本:

腳本說明:
??????A1:連接 mongodb 數(shù)據(jù)庫。
??????A2:獲取 txtPost 表中的數(shù)據(jù)。
??????A3:獲取 txtComment 表中的數(shù)據(jù)。
??????A4:將序表 A2 下的 comment 與 post_no 組合成序表,其中 post_no 改名為 pno。
??????A5:序表 A4 通過 comment_no 與序表 A3 關(guān)聯(lián),追加字段 comment_content,將其改名為 Content。
??????A6:按 pno 分組返回序表,~ 表示當(dāng)前記錄。
??????A7:關(guān)閉數(shù)據(jù)庫連接。
7. 關(guān)聯(lián)嵌套結(jié)構(gòu)情況 3
兩個(gè)關(guān)聯(lián)表,表 A 與表 B 中的內(nèi)嵌文檔信息關(guān)聯(lián), 且返回的信息在記錄上。表 collection2 字段 product 是嵌套數(shù)組結(jié)構(gòu),返回的信息是 isCompleted 等字段。
測試數(shù)據(jù):
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'}
]
}
期待結(jié)果
{
_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 兩表關(guān)聯(lián)查詢,首個(gè) addFields獲取isCompleted數(shù)組的第一個(gè)記錄,后一個(gè)addFields 轉(zhuǎn)換成所需要的幾個(gè)字段信息
SPL 腳本:

腳本說明:
??????A1:連接 mongodb 數(shù)據(jù)庫。
??????A2:獲取 collection1 表中的數(shù)據(jù)。
??????A3:獲取 collection2 表中的數(shù)據(jù)。
??????A4:根據(jù)條件 order, lot 從序表 A2 中查詢記錄,然后追加序表 A3 中的字段serialNo, batchNo,返回合并后的序表。
??????A5:關(guān)閉數(shù)據(jù)庫連接。
實(shí)現(xiàn)從數(shù)據(jù)記錄中的內(nèi)嵌結(jié)構(gòu)中篩選,將符合條件的數(shù)據(jù)合并成新序表。
8. 多字段分組統(tǒng)計(jì)
統(tǒng)計(jì)分類項(xiàng)下的總數(shù)及各子項(xiàng)數(shù)。下面統(tǒng)計(jì)按 addr 分類 book 數(shù)及其下不同的 book 數(shù)。

期望結(jié)果:

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 分組統(tǒng)計(jì) book 數(shù),再按 addr 分組統(tǒng)計(jì) book 數(shù),調(diào)整顯示順序
SPL腳本:

計(jì)算結(jié)果:

腳本說明:
A1:連接 mongodb 數(shù)據(jù)庫。
A2:獲取books表中的數(shù)據(jù)。
A3:按 addr,book 分組統(tǒng)計(jì) book 數(shù),
A4:再按 addr 分組統(tǒng)計(jì) book 數(shù)。
A5:將 A4 中的 Total 按 addr 關(guān)聯(lián)后合并到序表中。
A6:關(guān)閉數(shù)據(jù)庫連接。
9. 兩表關(guān)聯(lián)查詢
從關(guān)聯(lián)表中選擇所需要的字段組合成新表。
Collection1:
collection2:

期望結(jié)果:

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 兩表進(jìn)行關(guān)聯(lián)查詢,redact 對記錄根據(jù)條件進(jìn)行遍歷處理,project 選擇要顯示的字段。
SPL腳本:

腳本說明:
A1:連接 mongodb 數(shù)據(jù)庫。
A2:獲取c1表中的數(shù)據(jù)。
A3:獲取c2表中的數(shù)據(jù)。
A4:兩表按字段 user1,user2 關(guān)聯(lián),追加序表 A3 中的 output 字段,返回序表。
A5:關(guān)閉數(shù)據(jù)庫連接。
通過 join 把兩個(gè)關(guān)聯(lián)表不同的字段合并成新表。
10. 多表關(guān)聯(lián)查詢
多于兩個(gè)表的關(guān)聯(lián)查詢,結(jié)合成一張大表。
Doc1:

Doc2:

Doc3:

合并后的結(jié)果:
{
"_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 數(shù)據(jù)結(jié)構(gòu)原因,寫法也多樣化,展示也各不相同。
SPL 腳本:

此腳本與上面例子類似,只是多了一個(gè)關(guān)聯(lián)表,每次 join 就新增加字段,最后疊加構(gòu)成一張大表。.
SPL 腳本的簡潔性、統(tǒng)一性就非常明顯。
11. 指定數(shù)組查找
從指定的數(shù)組中查找符合條件的記錄。所給的數(shù)組為:["Chemical", "Biology", "Math"]。
測試數(shù)據(jù):

期望結(jié)果:

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"]的同學(xué)。
SPL 腳本:

腳本說明:
A1:定義查詢條件科目數(shù)組。
A2:連接 mongodb 數(shù)據(jù)庫。
A3:獲取 student 表中的數(shù)據(jù)。
A4:查詢存在數(shù)組中的科目記錄。
A5:生成字段為 name, lesson 的新序表,其中符合條件的值存放在字段 lesson 中
A6:關(guān)閉數(shù)據(jù)庫連接。
集算器對給定數(shù)組中查詢記錄的實(shí)現(xiàn)更簡明易懂。
12. 關(guān)聯(lián)表中的數(shù)組查找
從關(guān)聯(lián)表記錄數(shù)據(jù)組中查找符合條件的記錄, 用給定的字段組合成新表。
測試數(shù)據(jù):
users:

workouts:

期望結(jié)果:

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()
把關(guān)聯(lián)表 users,workouts 查詢結(jié)果放到數(shù)組中,再將數(shù)組拆解,提升子記錄的位置,去掉不需要的字段。
SPL 腳本:

腳本說明:
??????A1:連接 mongodb 數(shù)據(jù)庫。
??????A2:獲取 users 表中的數(shù)據(jù)。
??????A3:獲取 workouts 表中的數(shù)據(jù)。
??????A4:查詢序表 A3 的 _id 值存在于序表 A2 中 workouts 數(shù)組的記錄, 并追加 name 字段, 返回合并的序表。
??????A5:關(guān)閉數(shù)據(jù)庫連接。
由于需要獲取序列的交集不為空為條件,故將 _id 轉(zhuǎn)換成序列。
? ? ? ? Mongo 存儲的數(shù)據(jù)結(jié)構(gòu)相對關(guān)聯(lián)數(shù)據(jù)庫更復(fù)雜、更靈活,其提供的查詢語言也非常強(qiáng)、能適應(yīng)不同的情況,需要了解函數(shù)也不少,函數(shù)之間的結(jié)合更是變化無窮,因此要掌握并熟悉應(yīng)用它并非易事。集算器的離散性、易用性恰好能彌補(bǔ) Mongo 這方面的不足,它降低了 mongo 學(xué)習(xí)成本及使用 mongo 操作的復(fù)雜度、難度,讓 mongo 的功能得到更充分的展現(xiàn),同時(shí)也希望 mongo 越來越受到廣大愛好者的青睞。