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。 ![](https://i.imgur.com/AekpgV8.png) ### 優點 - 搜尋速度更(飛)快 ~ - 在使用分組或排度時更快 ~ ### 缺點 - 每次進行操作(新增、更新、刪除)時,都會更費時,因為也要修改索引。 - 索引需要佔據空間。 ### 使用時機 所以根據以上的優缺點可知,不是什麼都要建立索引的,通常只有下列時機才會使用。 - 搜尋結果佔原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 ``` ![](https://i.imgur.com/LYMxFXP.png) - $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} ) ``` ![](https://i.imgur.com/4SdCeIv.png) > allowDiskUse參數,mongodb有個限制在Pipeline的階段中,規定記憶體只能用100mb,不然就會跳出上圖的錯誤,但如果將allowDiskUse設定為true,則它多出來的資料暫存寫入到臨時的collection,只是會不會有什麼問題或壞處,官網上都沒特別提到…… ## MongoDB的副本集(replica set) 這個系統它有三個mongodb,其中primary節點接受所有client端的讀或寫,整個副本集只有一個primary,並且每當有資料新增時,primary會同步到其它兩個secondary。 ![](https://i.imgur.com/c8lbdKR.png) 各節點都是通過一個叫心跳請求(heartbeat request)的機制來通信,如果當primary節點如果在10秒內無法和其它節點進行通信,這系統會自動從secondary節點中選取一個當主節點。 ![](https://i.imgur.com/6eAqNxP.png) ### 指令說明 |指令|描述| |---|---| |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裡尋找資料。 ![](https://i.imgur.com/jjdquS2.png) ### 片鍵 Shard Keys 假設咱們拆分為三片,然後我們指定片鍵為age欄位,它就大致上可能會分成這樣,會根據片鍵建立chunk,然後再將這堆chunk分散到這幾個分片中,{min~10}就是一個chunk,就是一組document。 ![](https://i.imgur.com/O1xJrS9.png) ### chunk 的分配與拆分 每個分片中都包含了多個chunk,而每chunk中,又包含了某個範圍的document組,我們先簡單來畫個圖複習一下 ![](https://i.imgur.com/DHEQEi1.png) ## 參考連結 [SQL 到彙總(Aggregation)對應表](http://calvert.logdown.com/posts/159915-sql-to-aggregation-mapping-chart) [MongoDB的30教程](http://marklin-blog.logdown.com/posts/1392582)