2021 iThome 鐵人賽 MongoDB披荊斬棘之路
DAY10 MongoDB 聚合(Aggregate)種類介紹
終於來到第十天,進入比較有趣的聚合了,這個算是 MongoDB 裡面比較有趣(X)、痛苦(O)的開始。
大家很常使用到 RDBMS 的 group
語法,MongoDB 也是有這樣的語法,叫做 Aggregation
,只要是將資料筆數縮減、或是文件變成某一種形式呈現,就是屬於 Aggregation 範圍。
MongoDB 的 Aggregation 分為兩種階段,第一種階段為 資料篩選
,第二種階段為資料聚合與統計
,白話來說就是一個是選擇要處理的資料,另一個是把資料整理成你想看得樣子。但要記得,這兩者間沒有順序關係,也沒有次數限制,你可以[篩選 -> 統計]
或者 [統計 -> 篩選 -> 統計]
都是可以任意安排的。
基本範例長這樣
1 2 3 4
| db.employee.aggregate([ { $match: { status: "A" } }, { $group: { _id: "$employee_id", total: { $sum: "$field" } } } ])
|
MongoDB Aggregation 種類
MongoDB aggregation 分為三種,後兩種都是比較有彈性、客製化,但是方法不同。
- Single purpose
- Aggregation pipeline
- Map-reduce function
Single purpose
單一功能,最基本的聚合,例如你要查詢不重覆欄位,通常就是 distinct
語法,而在 MongoDB 內也是一樣,就是
db.employee.distinct("field_name")
這個稱為 single purpose,馬上學完 1/3 了,是不是很輕鬆。
Map-reduce function
顧名思義就是 function 做法。
Map:
1 2 3
| var mapFunc = function(){ emit(this.employee_id, this.employee_field); };
|
Reduce:
1 2 3
| var reduceFunc = function(empId, fields){ return Array.sum(fields); };
|
最後組合在一起
1 2 3 4 5
| db.employee.mapReduce( mapFunc, reduceFunc, { out: "example_map_reduce"} );
|
db.example_map_reduce.find()
1 2 3 4 5
| { {"_id": 10001, "value": 33}, {"_id": 10002, "value": 144}, {"_id": 10003, "value": 15} }
|
Aggregation pipeline
1 2 3 4
| db.employee.aggregate([ { $group: { _id: "$employee_id", value: { $sum: "$employee_field" }}}, { $out: "example_map_reduce_2"} ])
|
db.example_map_reduce_2.find()
Aggregation pipeline,顧名思義所有動作就是在一個個管道中進行,每個水管大小以及形狀都是依照需求而定,比較常見的就是(但沒有一定的順序喔)
$match
找到目標資料 => ($unwind
拆解數據) => $group
資料聚合&統計 => $project
重新呈現資料樣貌 => $out
輸出
這其中還有像是 $sort
、$limit
沒完全列出來。
整理以一下後兩種特性:
map-reduce
aggregation pipeline
- 程式碼難重複使用
- 程式碼很多,難以閱讀
- 效能就是比較快
查詢過去網路上的使用者經驗,map-reduce
速度就是會比較慢,雖然時至今日也演進了好幾個版本,但當時已經選擇 aggregate 做法,就沒有再深入探討 map-reduce 了,如果有人在近期版本有在使用,可以分享一下使用心得。
Aggregation Operators
MongoDB aggregate 的 operator 不算太多,幾乎都很常使用到,再來會一一介紹功能以及如何使用,但在之前我們先準備好測試資料。
1 2 3 4 5 6 7 8
| db.getCollection('movie').insertMany([ {"name": "movieA", "language": "en-gb", "rating": 8, "totalCost": 30000000, "producer": "companyA"}, {"name": "movieB", "language": "en-gb", "rating": 5, "totalCost": 10000000, "producer": "companyA"}, {"name": "movieC", "language": "zh-tw", "rating": 6, "totalCost": 25000000, "producer": "companyA"}, {"name": "movieD", "language": "zh-tw", "rating": 8, "totalCost": 10000000, "producer": "companyB"}, {"name": "movieE", "language": "zh-tw", "rating": 9, "totalCost": 6000000, "producer": "companyC"}, ])
|
$sort
對特定欄位進行排序,例如我們針對rating
進行倒序排序
1
| db.movie.aggregate({"$sort" : { "rating" : -1 }})
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| { "_id" : ObjectId("6120c79d2976f517181ffefa"), "name" : "movieE", "language" : "zh-tw", "rating" : 9.0, "totalCost" : 6000000.0, "producer" : "companyC" }
{ "_id" : ObjectId("6120c79d2976f517181ffef6"), "name" : "movieA", "language" : "en-gb", "rating" : 8.0, "totalCost" : 30000000.0, "producer" : "companyA" }
{ "_id" : ObjectId("6120c79d2976f517181ffef9"), "name" : "movieD", "language" : "zh-tw", "rating" : 8.0, "totalCost" : 10000000.0, "producer" : "companyB" }
{ "_id" : ObjectId("6120c79d2976f517181ffef8"), "name" : "movieC", "language" : "zh-tw", "rating" : 6.0, "totalCost" : 25000000.0, "producer" : "companyA" }
{ "_id" : ObjectId("6120c79d2976f517181ffef7"), "name" : "movieB", "language" : "en-gb", "rating" : 5.0, "totalCost" : 10000000.0, "producer" : "companyA" }
|
$limit
設定希望取得資料的筆數,例如我們只希望取得評價前二高的電影
1 2 3
| db.movie.aggregate( {"$sort" : { "rating" : -1 }}, {"$limit" : 2})
|
結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| { "_id" : ObjectId("6120c79d2976f517181ffefa"), "name" : "movieE", "language" : "zh-tw", "rating" : 9.0, "totalCost" : 6000000.0, "producer" : "companyC" }
{ "_id" : ObjectId("6120c79d2976f517181ffef9"), "name" : "movieD", "language" : "zh-tw", "rating" : 8.0, "totalCost" : 10000000.0, "producer" : "companyB" }
|
pipeline 是依照語法的順序執行,如果今天反過來,那結果會變怎樣呢?
1 2 3
| db.movie.aggregate( {"$limit" : 2}, {"$sort" : { "rating" : -1 }})
|
結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| { "_id" : ObjectId("6120c79d2976f517181ffef6"), "name" : "movieA", "language" : "en-gb", "rating" : 8.0, "totalCost" : 30000000.0, "producer" : "companyA" }
{ "_id" : ObjectId("6120c79d2976f517181ffef7"), "name" : "movieB", "language" : "en-gb", "rating" : 5.0, "totalCost" : 10000000.0, "producer" : "companyA" }
|
可以看到取出來的評價前二高電影,跟我們想像的不同,原因就出在這次 pipeline 是先取出兩名,再進行排序的。因此各位在使用上要特別注意每個 pipeline 的位置。