2021 iThome 鐵人賽 - DAY10 MongoDB 聚合(Aggregate)種類介紹

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 分為三種,後兩種都是比較有彈性、客製化,但是方法不同。

  1. Single purpose
  2. Aggregation pipeline
  3. 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 的位置。

  • 作者: MingYi Chou
  • 版權聲明: 轉載不用問,但請註明出處!本網誌均採用 BY-NC-SA 許可協議。