MongoDB 學習筆記
===
## 安裝方式
可使用官網的[安裝步驟](https://docs.mongodb.com/manual/installation/),如果是安裝**ubuntu 16.04**的版本當你啟動mongo的服務會出現`Failed to start mongod.service: Unit mongod.service not found.`的問題,以下為解決方法:
- 建立 mongodb.service 檔案
```=
sudo nano /etc/systemd/system/mongodb.service
```
- 貼上啟動 mongodb 需要的內容
```=
[Unit]
Description=High-performance, schema-free document-oriented database
After=network.target
[Service]
User=mongodb
ExecStart=/usr/bin/mongod --quiet --config /etc/mongod.conf
[Install]
WantedBy=multi-user.target
```
- 啟動相關指令
```=
sudo systemctl start mongodb (啟動)
sudo systemctl status mongodb (狀態)
sudo systemctl stop mongodb (停止)
```
## 使用Docker建構MongoDB
- [Ubuntu 16.04 安裝 docker](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-16-04)
- 安裝docker-compose
```=
sudo apt install docker-compose
```
- 建立一個資料夾及docker-compose.yml,並貼上以下的內容,`docker-compose up` 啟動容器,記得port不要與本機相衝。
```=
version: '2'
services:
mongo:
image: mongo
ports:
- "40000:27017"
volumes_from:
- mongodata
mongodata:
image: tianon/true
volumes:
- /data/db
```
- 進入指令
```=
docker ps => 查看容器有沒有執行,容器可以當成VM
docker exec -it <container id> bash => 進入容器中,之後就可以用mongo來進入資料庫
```
## 資料庫基本用法
使用 mongo 進入資料庫的指令
|指令|說明|
|---|---|
| `show dbs` | 顯示所有資料庫|
| `show collections` | 顯示該資料庫的所有資料表 |
| `use testdb`、`use TableA` ` db.TableA.insert({name:'test'})` | ㄧ定要新增資料才可以建立資料庫及資料表 |
| `db.TableA.drop()` | 刪除資料表 |
| `use testdb` `db.dropDatabase()` | 刪除資料庫 |
## [資料表設計](https://blog.toright.com/posts/4483/mongodb-schema-%E8%A8%AD%E8%A8%88%E6%8C%87%E5%8D%97.html)
1. 如果你拆成很多資料表會有的 foreign key,但 mongodb 沒有幫你建立關聯關係也沒有 join 的方法,所以你只能靠程式建立資料ㄧ致性。
2. mongodb 鼓勵建立 embed data 不要用 join 建立太多資料表,浪費太多查詢成本,也可以透過 mongodb 保持資料ㄧ致性。
3. 如果 document 的 embeding data 有重複資料另建立 collection,如果沒有就用 embeding。
4. 鼓勵使用 MongoDB 的 embeded model 提昇查詢效能。
:::info
mongodb會進行所謂的預分配,將空間換取穩定,每當你第一次建立document,他就會切分固定大小給你,然後你就算刪除document時空間還是會存在,所以如果你先執行一次10萬筆測試,在執行第二次十萬筆測試時,你會發現執行速度變快了,因為它不用在預分配了。
:::
## CRUD 基本操作
### 新增
- 新增單一資料(insert),如果需要回傳objectID則使用(insertOne)
```=
user = {
name: 'jeffery',
age: 23
}
db.employee.insert(user)
```
- 新增多筆資料文檔(insert),如果需要回傳objectID則使用(insertMany)
```=
for(i=1;i<10;i++) {
users.push(user)
}
db.employee.insert(users)
```
- Bulk Insert
Ordered operation
```=
var bulk = db.collection.initializeOrderedBulkOp();
bulk.insert({ name: 'test' })
bulk.execute()
```
UnOrdered operation
```=
var bulk = db.collection.initializeUnorderedBulkOp();
bulk.insert({ name: 'test' })
bulk.execute()
```
- 測試效率
|測試案例(筆數大小)| Insert|InsertMany|Ordered Bulk|Unordered Bulk
|---|---|---|---|---|---|
|10 (380bytes)|150ms|146ms|145ms|146ms|
|10 (380bytes)|16ms|13ms|11ms|13ms|
- 結論
1. 在數據量較大情況下使用Bulk操作都名顯優於 insert、insertMany
2. 預分配確實會增加執行速度,但數據量越大越不明顯
:::info
- ordered 預設是 true 當新增到有錯誤就會停止,false 則會繼續新增,範例:db.employee.insert(user,{ordered: false})。
- 只要有相關聯的資料就選擇 ordered,反之像是log就選擇 unordered
:::
### 修改
- 基本語句結構
```=
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>, // 預設是false,當true時更改值外如果沒有該欄位會新增
multi: <boolean>, // 預設是false,當true時可以多筆更新
writeConcern: <document> // 拋出異常級別
}
)
```
- 基本更新
沒使用 $set 會把原本的資料替換成更新的資料
```=
db.users.update({name: 'carl'},{age:25})
原本:{"name" : "carl", "age" : 24 }
結果:{"age" : 25 }
```
使用 $set 只更新指定欄位的值
```=
db.users.update({name: 'carl'},{$set: {age:25}})
原本:{"name" : "carl", "age" : 24 }
結果:{"name" : "carl", "age" : 25 }
```
使用 $inc 遞增該欄位數值
```=
db.users.update({name: 'carl'},{$inc: {age:1}})
```
使用 $unset 移除特定欄位
```=
db.users.update({name: "tony"},{$unset: {age:''}})
```
- 更新陣列欄位修改器
$push 、 $each 搭配使用新增多筆資料到陣列
```=
db.users.update({name: 'robin'},{
$push: { friend: {$each: ["jack","jess"]}}
})
```
$slice限制陣列大小,超過會從第一筆開始刪除
```=
db.users.update({name: 'robin'},{
$push: { friend: {$each: ["jack","jess"], $slice: -5 }}
//只保留最後五位
})
//原本:{"name" : "robin", "age" : 23, "friend" : [ "jeffery", "carl", "jack", "jess" ] }
//結果:{"name": "robin", "age" : 23, "friend" : [ "carl", "jack", "jess", "tony", "witchery" ] }
```
$addToSet 更新一個元素到陣列,並且保證不會重複
```=
db.users.update({name:"mora"},{
$addToSet: { friend: {$each: ["charlie","jeffery"]}}
})
//原本:{"age" : 26, "name" : "mora", "family" : [ "charlie" ]
//結果:{"age" : 26, "name" : "mora", "family" : [ "charlie", "jeffery" ]}
```
$pop 可以從頭或尾刪除,而 $pull 則是基於特定條件來刪除。
```=
// 1從陣列尾巴刪除,-1從陣列開頭刪除
db.users.update({name: "robin"}, {$pop: {friend: 1}})
// 限制條件刪除
db.users.update({name: "robin"}, {$pull: {friend: "jess"}})
```
- 當某個欄位為空複製其他欄位的值,並多筆更新
```=
db.bike.find({bike_no: {$ne:""},s_no: {$eq:""}})
.forEach(function(document){
db.bike.update({_id:document._id},{$set: {s_no:document.bike_no}})
})
```
### 刪除
用 remove 刪除資料,並不會刪除 index 與 預分配空間
- justOne預設false,代表query到幾個就會刪除幾個,true則只會刪第一個。
```=
db.collection.remove(
<query>,
{
justOne: <boolean>,
}
)
db.user.remove({name: "max"})
```
刪除所有資料
```=
db.user.remove({}) # 刪除所有資料,但保留 index
db.user.drop() # 刪除所有資料及 index
```
deleteMany與deleteOne
```=
db.user.deleteMany({name: "steven"}) # 刪除多筆
db.user.deleteOne({name: "jj"}) # 刪除一筆
```
> 在 nodejs drivers 裡面的 remove 已經被 Deprecated,建議改用 deleteMany、deleteOne
### 搜尋
可以參考[官方文件](https://docs.mongodb.com/v3.2/reference/operator/query/)的搜尋方法。
第一個參數決定要那些資料,而第二個參數則決定要返回那些key。
```=
db.collection.find(
{條件式:條件操作符物件},
{鍵指定:想要顯示的欄位}
)
//範例
db.user.find({name:'jeffery'},{_id:0,name:1,age:1})
```
|條件|操作符號|
|---|---|
|邏輯符號 | $and、 $or、 $nor、 $not |
|比較符號 | $gt、 $gte、 $lt、 $lte、 $in、 $nin |
$not 通常搭配正則表達式
```=
db.zips.find({
city: {$not: /NEW/i }
})
```
小測驗
```
{"id":"1","name":"mark","age":25,"fans":100,"likes" : 1000}
{"id":"2","name":"steven","age":35,"fans":220,"likes" : 50}
{"id":"3","name":"stanly","age":30,"fans":120,"likes" : 33}
{"id":"4","name":"max","age":60,"fans":500,"likes" : 1000}
{"id":"5","name":"jack","age":30,"fans":130,"likes" : 1300}
{"id":"6","name":"crisis","age":30,"fans":130,"likes" : 100}
{"id":"7","name":"landry","age":25,"fans":130,"likes" : 100}
```
1. 年紀30歲以上(包含30),但不滿60歲(不包含60),fans又有200人以上(包含200)的人。
2. fans小於等於100,或是likes小於100的人(只要其中一個條件達成)。
3. age為25、60的人。
4. age不為25、60的人,並且只給我它的id就好。
5. likes小於等於100的人(使用$not)。
6. 同時不滿足fans小於100人且likes小於500(使用$nor)。
=>也就是要同時滿足fans大於100且likes大於500
#### 搜尋陣列內容
|操作符號|符號解釋|
|---|---|
|$all | 尋找全部都符合的陣列 |
|$size | 尋找特定長度的陣列 |
|$slice | 回傳指定的陣列元素,ex. 10就為前十條,-10就為後十條 |
|$elemMatch | 只針對陣列進行多組 query |
小測驗
```
{"id":"1","name":"mark",
"fans":["steven","stanly","max"],
"x":[10,20,30]};
{"id":"2","name":"steven",
"fans":["max","stanly"],
"x":[5,6,30]};
{"id":"3","name":"stanly",
"fans":["steven","max"],
"x":[15,6,30,40]};
{"id":"4","name":"max",
"fans":["steven","stanly"],
"x":[15,26,330,41,1]};
```
1. 尋找 fans 中同時有 steven、max 的網紅
2. 尋找 fans 總共有三位的網紅
3. 尋找 mark 的第一個 fans
4. 尋找 x 中至少有一個值為大於 30 小於 100 的網紅
5. 尋找 name 為 s 開頭的網紅
6. 尋找 fans 中有包含 m 開頭的網紅
#### Cursor 運用與搜尋原理
Cursor 是 find 時回傳的結果,它可以讓使用者對最終結果進行有效的控制,它事實上也就是Iterator 模式的實作。
**常用的cursor方法**
- 限制(limit)
```=
db.user.find().limit(10) # 限制最多只回傳10個使用者
```
- 忽略(skip)
**注意略過的筆數越多速度越慢**
```=
db.user.find().skip(2) # 略過前面兩筆資料,其他回傳
```
- 排序(sort)
**1 表示由小到大,-1表示由大到小**
```=
db.user.find().sort({age:1}) # 根據搜尋後的結果,按照age由小到大排序
```
## 索引
最常見的說法是,一本字典中,你要找單字,會先去前面的索引找他在第幾頁,是的這就是索引,可以幫助我們更快速的尋找到 document。

### 優點
- 搜尋速度更(飛)快 ~
- 在使用分組或排度時更快 ~
### 缺點
- 每次進行操作(新增、更新、刪除)時,都會更費時,因為也要修改索引。
- 索引需要佔據空間。
### 使用時機
所以根據以上的優缺點可知,不是什麼都要建立索引的,通常只有下列時機才會使用。
- 搜尋結果佔原collection越小,才越適合(下面會說明更清楚)。
- 常用的搜尋。
- 該搜尋造成性能瓶頸。
- 在經常需要排序的搜尋。
- 當索引性能大於操作性能時。
```=
db.member.ensureIndex({x:1}) # 建立該欄位索引
db.member.getIndexes() # 查看目前建立索引的欄位,預設有_id
```
### 特別注意
在mongodb中排序是非常的耗費內存資源,如果排序時內存耗費到32mb,mongodb就會報錯,如果超出值,那麼必須使用索引來獲取經過排序的結果。
### 不要使用索引的時機
從結果可看出有用索引的比較慢,主要原因為他要先去掃索引然後,再去找全文,正常情況下索引會比較快,但是如果結果佔原collection比過多時就會發生索引反而比較慢。
所以記好當你要找的結果可能會佔你原資料太多部份的,請不要用索引。
### 複合索引
針對多個欄位建立索引
```=
db.user.ensureIndex({"name" : 1 , "age" : 1})
```
DB結果顯示,先按照name排序,再按照age排序
```
索引目錄 存放位置
["mark",20] -> xxxxxxxx
["mark",25] -> xxxxxxxx
["max",15] -> xxxxxxxx
["steven",30] -> xxxxxxxx
```
索引建立的順序會造成查詢速度影響很大
- 情境一
有利於此情境的搜尋,索引要改成`{ "age": 1 , "name" : 1 }`先以age排序才會讓搜尋時間大大減少
```
db.user.find({}).sort({"age" : 1})
```
- 情境二
此情境對於一開始先以name排序比先以age排序的索引,搜尋時間比較少
```
db.user.find({"name" : "mark00"}).sort({"age" : 1})
```
注意事項
- 實際應用時{ "sortKey" : 1 , "queryKey" : 1 }是很有用的,也就是說如果某欄位很常被排序或是排序很耗時,在建立索引時請放至到前面也就是sortKey那位置。
- 當你建立{"name" : 1, "age" : 1}時,等同於建立了
{"name" : 1}和{"age" : 1},他們都可以用索引來搜尋name、age,但注意,這種建法排序索引只能用在上name。
## Aggregation
可以參考[官方文件](https://docs.mongodb.com/v3.2/reference/operator/aggregation)
聚合就是能幫助我們**分析**的工具,它能處理數據記錄並回傳結果。
mongodb 的 aggregate framework 主要是建立在聚合管道(pipeline)基礎下,而這管道就是可以一連串的處理事件,如下:
```
db.collection.aggregate(
[將每篇文章作者與like數抓取出來],
[依作者進行分類],
[將like數進行加總]
[返like數前五多的結果]
)
```
### 管道 pipeline 操作符號
- $project:用來選取 document 欄位,或是新增欄位、對欄位進行操作
```=
db.user.aggregate({$project: {id:1,name:1}})
```
- $match:對 document 進行篩選(建議先使用把資料量縮小)
```=
db.user.aggregate({$match: {age: {$gt:10,$lte:30}}}) # age為10至30歲的人
```
- $group:依照條件進行分組,並進行其他操作的運算
```=
db.user.aggregate({
$group: {
_id: {status: '$status'},
total: {$sum: '$count'}
}
}) # 用 status 來分成兩組,並計算總數
// 顯示結果
{ "_id" : { "status" : "x" }, "total" : 20 }
{ "_id" : { "status" : "o" }, "total" : 15 }
```
```=
db.bike.aggregate({
$group: {
_id: "$status", total: {$sum: "$count"}
}
})
// 顯示結果
{ "_id" : "x", "total" : 20 }
{ "_id" : "o", "total" : 15 }
```
- $unwind:將陣列欄位**拆分**成每個 document
```
{
"name" : "mark",
"fans" : [
{"name" : "steven","age":20},
{"name" : "max","age":60},
{"name" : "stanly","age":30}
]
}
```
```=
db.bike.aggregate({$unwind: "$fans"}) # fans內的資料拆分成三個document
```

- $sort:根據任何欄位進行排序,跟搜尋方法一樣
***如果大量的資料要進行排序,建議在管道的第一節進行排序,因為可以用索引***
```=
db.bike.aggregate({$sort: {age:1}})
```
- $limit:限制回傳 document 數量
```=
db.bike.aggregate({$limit: 5})
```
- $skip:捨棄前n筆然後再開始回傳結果
***就如同在find時一樣,大量數據下他的效能會非常的差。***
```=
db.bike.aggregate({$skip: 5})
```
小測驗
按照下面的步驟建立管道,來找出第二年輕的男性。
1. 先篩選出sex為M的user。
2. 將每個user的name與age投射出來。
3. 根據age進行排序。
4. 跳過1名user。
5. 限制輸出結果為1。
```=
db.friend.aggregate(
{$match: {sex: "M"}},
{$project: {name:1,age:1}},
{$sort: {age:1}},
{$skip: 1},
{$limit: 1}
)
```
### Pipeline函數庫
- 數學運算式
|符號|描述|
|---|---|
|$add | 多個表達式相加|
|$subtract| 兩個表達式相減 |
|$multiply| 多個表達式相乘 |
|$divide| 兩個表達式相除 |
|$mod | 兩個表達式相除取餘數 |
小測驗
計算一筆訂單的總收入是多少?
公式=> price*count-discount
```
{ "id" : 1 , "price" : 100 , "count" : 20, "discount" : 0 },
{ "id" : 2 , "price" : 200 , "count" : 20, "discount" : 100 },
{ "id" : 3 , "price" : 50 , "count" : 20, "discount" : 100 },
{ "id" : 4 , "price" : 10 , "count" : 210, "discount" : 200 },
{ "id" : 5 , "price" : 100 , "count" : 30, "discount" : 20 }
```
pipeline步驟為先算出每筆訂單收入,再加總起來
- 日期運算式
|符號|描述|
|---|---|
|$year | 轉換成年份 |
|$month | 轉換成月份|
|$dayOfMonth| 轉換成一個月的日期(1~31) |
|$dayOfYear| 轉換成一年中的天(1~365) |
|$dayOfWeek| 轉換成一週的哪一天(1日~7六) |
|$dateToString| 轉換成指定的日期格式|
```
{ "_id" : 1, "date" : ISODate("2016-01-02T08:10:20.651Z") }
```
如果 DB 的日期儲存 timestamp 須先轉換成毫秒(乘1000),在轉換成 ISODATE 格式
```
$add: [new Date('1970-01-01T08:00:00Z'),
{$multiply: ["$unixtime",1000]}
]
```
範例:
```
back_date: {
$dateToString: {
format: "%Y-%m-%d %H:%M:%S",
date: {$add: [new Date('1970-01-01T08:00:00Z'), {$multiply:["$unixtime",1000]}]},
}
}
```
:::info
在MongoDB是使用Date()只能傳入日期,在PHP是使用MongoDate()只能傳入時戳
:::
- 字串表達式
|符號|描述|
|---|---|
|$substr | 只取字串某一個範圍 |
|$contact| 將指定的字串連再一起|
|$toLower| 變小寫|
|$toUpper| 變大寫|
|$strcasecmp| 比較兩個字串是否相等,如果相等為0,如果字串ASCII碼大於另一字串則為1,否則為-1|
小測驗
取得 item 開頭為 B 的 document ,並且輸出的 describe 要全轉換為小寫
```
{ "item" : "ABC", "describe":"AAbbcc"},
{ "item" : "BCE" , "describe":"hello WorD"},
{ "item" : "CAA" , "describe":"BBCCaa"}
```
拆分以下步驟
1. 取得每個item的第一個值,並存放在temp欄位中。
2. 並且每個temp與B進行比較,比較結果放在result欄位中。
3. 篩選出result為0的document。
4. 將該document的describe欄位轉換成小寫。
- 常用邏輯表達式
|符號|描述|使用|
|---|---|---|
|$cmp| 比較expr1與2,相同為0,1>2為1,相反則為-1|"$cmp":[expr1,expr2]|
|$eq|一樣比較expr1與2,但相同則返回true否則為false|"$eq":[expr1,expr2] |
| $lt $lte |小於和小於等於|"$lt" : value|
| $gt $gte |大於和大於等於|"$gt" : value|
|$and |所有表達式都為true,則回傳true|"$and":[expr1,expr2..]|
|$or|其中一個表達式為true,則回傳true|"$or" : [expr1,expr2..]|
|$not|針對表達示取反值|"$not" : expr|
|$cond|就是一般程式裡的ifelse|"$cond":[boolExpr,trueExpr,falseExpr]|
小測驗
計算出每筆訂單的實際收入,其中當數量大於200時打八折,最後在依 class 進行分組,算出各組的總收入
```
{ "id":1,"class" : "1" ,"count" : 10,"price" : 180},
{ "id":1,"class" : "1" ,"count" : 10,"price" : 350},
{ "id":1,"class" : "2" ,"count" : 10,"price" : 90},
{ "id":1,"class" : "2" ,"count" : 10,"price" : 320},
{ "id":1,"class" : "2" ,"count" : 10,"price" : 150}
```
拆分以下步驟
1. 全部的訂單先判斷折扣率,並存放在discount裡。
2. 計算每分訂單的收入,並存放在total裡。
3. 根據class進行分組,並計算各組的總收入,存放在result裡。
## 正規化與反正規化
正規化:主要目的為解決資料的『重複性』與『相依性』
- 第一正規化(降低重複性)
將重複的10提出來
|TradeId| Name | Date | Volume|
|---|---|---|---|
|1 |Mark| 20160101| 10
|2 |Mark| 20160101| -20
|3 |Jiro| 20160102| -20
|4 |Jiro| 20160102| 30
|5 |Ian |20160103 |34
|6 |Ian |20160103 |-10
- 第二正規化(去除相依性)
Age相依於Name,應將提出來
交易訂單
|TradeId(主鍵) |UserId| Date| Volume|
|---|---|---|---|
|1 |001 |20160101| 10|
|2 |001 |20160101| -20|
|3 |002 |20160102| -20|
|4 |002 |20160102| 30|
|5 |003 |20160103| 34|
|6 |003 |20160103| -10|
交易者資料
|UserId| Name| Age|
|---|---|---|
|001| Mark| 18|
|002| Jiro| 35|
|003| Ian| 25|
### 何時使用正規化 ? 何時使用反正規化
|反正規化| 正規化|
|---|---|
|子文件較小| 子文件較大|
|資料不太常改變| 資料很常改變|
|最終資料一致即可| 中間階段的資料必須一致|
|document資料小幅增加| document資料大幅增加|
|資料通常需要執行二次搜尋才能獲得| 資料通常不包含在結果中|
|要求快速搜尋| 要求快速寫入|
## 實作範例
### PO 文模擬情境
**題目**
1. 使用者可以簡單的新增發文,並且會存放Text、Date、Author、likes、Message。
2. 使用者可以刪除發文。
3. 使用者可以對自已的po文進行更新。
4. 使用者可以進行留言或刪除留言。
5. 使用者可以like發文。
6. 使用者可以根據Text、Author、likes、Date進行搜尋。
7. 管理者可以速行分析個案(那些個案之後再想)
**答案**
1.
```
{
"id" : 1,
"text" : "XXXXXX",
"date" : "20160101",
"author" : "mark" , # 不常修改,注重搜尋效率(反正規化)
"likes" : 1,
"messages" : [
{"msgId" : 1,"author" : "steven" , "msg" : "what fuc." , "date" :20160101},
{"msgId" : 2,"author" : "ian" , "msg" : "hello world java","date":20160101}
]
}
```
2. `db.posts.remove({_id: ObjectId(xxx)})`
3. `db.posts.update({_id: ObjectId(xxx)}, {$set: {text: 'happy nice day'}})`
4. ```
db.posts.update({_id: ObjectId(xxx)}, {
$push: { messages: { "author" : "mark" ,
"msg": "hello word" ,
"date" : 20160101 }
}})
db.posts.update({_id: ObjectId(xxx)}, {
$pull: {messages: {msgId: 2}}
})
```
5. `db.posts.update({_id: ObjectId(xxx)}, {$inc: {likes : 1}})`
6. 首先我們要先想想索引要著麼建,咱們可以確定text要用全文索引來建立,因為我們要根據單詞來尋找它,再來是author與date這兩個可以一起建立,並且考慮常排序的欄位我們應該要將建立date先行的索引,因為我們常用來尋找最新或最舊的資料,而likes這獨立建立個索引,因為它排序與搜尋都很常會用到。
建立索引
```
db.posts.ensureIndex({"text": "text"})
db.posts.ensureIndex({"author": 1,"date": 1})
db.posts.ensureIndex({"likes": 1})
```
根據 text 進行搜尋
`db.posts.find({"text": {$search: "dog"}}).sort({"date":1}).limit(10)`
根據 author 進行搜尋
`db.posts.find({"author": "mark"}).count()`
根據 likes 找出最受歡迎的貼文
`db.posts.find().sort({"likes": -1}).limit(1)`
根據 date 找出最新的貼文
`db.posts.find().sort({"date": -1}).limit(1)`
8. Boss希望可以知道最多人留言的貼文,並且知道該貼文中,前三位留言最熱絡的使用者,並計算留言次數
- 先計算每篇貼文的留言數,將結果放入 messageCount
- 將 messageCount 排序後取出第一個
- 取出該篇貼文的留言,再將 messages 中的 author 進行分組統計,將結果放入 count
- 將 count 排序後將前三名印出
```
db.posts.aggregate([
{$project: {messages:1,messageCount: {$size: "$messages"}}},
{$sort: {messageCount: -1}},
{$limit: 1},
{$unwind: "$messages"},
{$group: {_id: "$messages.author", count: {"$sum":1}}},
{$sort: {"count": -1}}
],
{allowDiskUse: true}
)
```

> allowDiskUse參數,mongodb有個限制在Pipeline的階段中,規定記憶體只能用100mb,不然就會跳出上圖的錯誤,但如果將allowDiskUse設定為true,則它多出來的資料暫存寫入到臨時的collection,只是會不會有什麼問題或壞處,官網上都沒特別提到……
## MongoDB的副本集(replica set)
這個系統它有三個mongodb,其中primary節點接受所有client端的讀或寫,整個副本集只有一個primary,並且每當有資料新增時,primary會同步到其它兩個secondary。

各節點都是通過一個叫心跳請求(heartbeat request)的機制來通信,如果當primary節點如果在10秒內無法和其它節點進行通信,這系統會自動從secondary節點中選取一個當主節點。

### 指令說明
|指令|描述|
|---|---|
|mongod --replSet <名稱>|設定副本集名稱|
|rs.initiate( {..} )| 先進入mongo shell,然後設定節點|
|rs.conf()|查看副本設定|
|rs.status()| 查看各節點的狀態 |
|rs.add('host')|新增節點|
|rs.remove('host')|移除節點|
|(new Mongo('localhost:27017')).getDB('test')| 取得 host的連線|
## MongoDB的分片(Sharding)
主要的概念就是將collection拆分,將其分散到不同的機器,來分擔單一server的壓力。
如下圖三個mongod都會統一通信到mongos,在和client進行通訊,mongos不存儲任何資料,它就是個路由server,你要什麼資料就發給它,它在去決定去那個mongod裡尋找資料。

### 片鍵 Shard Keys
假設咱們拆分為三片,然後我們指定片鍵為age欄位,它就大致上可能會分成這樣,會根據片鍵建立chunk,然後再將這堆chunk分散到這幾個分片中,{min~10}就是一個chunk,就是一組document。

### chunk 的分配與拆分
每個分片中都包含了多個chunk,而每chunk中,又包含了某個範圍的document組,我們先簡單來畫個圖複習一下

## 參考連結
[SQL 到彙總(Aggregation)對應表](http://calvert.logdown.com/posts/159915-sql-to-aggregation-mapping-chart)
[MongoDB的30教程](http://marklin-blog.logdown.com/posts/1392582)