--- title: MongoDB tags: 網頁後端技術文章 --- # MongoDB [TOC] --- ## 基本觀念 在mongoDB中Table視為Collection,Data為Document,通常以Json格式,當Json資料裡面又有Json資料則稱為嵌套。 :::warning 單個BSON最大16M, ::: schema == struct ### MongoDB資料類別 * String * Boolean * Number(32bits) * ObjectID * Date * Array ### 關係(Relationship) ![](https://i.imgur.com/WtKzExd.jpg) 一對一關係不利於分析 多對多關係 ![](https://i.imgur.com/YObRlTq.png) ## 環境創建 1. 到[mongo官方網站](https://www.mongodb.com/3)安裝Server(Community)。 2. 下載完成後將目錄複製到根目錄底下 3. 在跟目錄下創建一個data的資料夾 4. 複製data的絕對路徑,並移動至bin目錄。 ![](https://i.imgur.com/MNcGJU5.png) 透過--dbpath設定路徑後就會自動執行。 ```bash= ./mongod --dbpath "/Users/chenshiyu/mongodb/data" ``` 開啟一個新的命令列,並移動到bin底下輸入`./mongo`即可開啟mongo的shell。 ![](https://i.imgur.com/U69WhR3.png) :::warning 一定要在Server運行時才可以啟用 ::: ### 設定後台執行 首先在mongo的資料夾中建立一個log的資料夾,透過命令行輸入以下指令設定log的輸出。 ```bash= mongod --dbpath "/Users/chenshiyu/mongodb/data" --logpath "//Users/chenshiyu/mongodb/log/mongo.log" --fork ``` 當設定好log的輸出後,我們會發現其實mongo在後台執行。 ## 基本指令 我們可以透過以下指令來查詢當前運行的mongo ```bash= ps -ef | grep mongod ``` 也可以透過kill指令來刪除程式 ```bash= kill 運行編號 ``` 一些關於mongoDB的基礎指令 ```bash= # 清理 cls # 離開 exit # 查看當前資料庫 show dbs # 使用資料庫,若未創建則會自動建立,但是必須建立Collection才會顯示 use 資料庫名稱 # 查看資料庫所有命令 db.help() # 創建Collection db.createCollection("名稱") # 返回當前使用的資料庫名稱 db.getName() # 返回所有Collection db.getCollectionNames() # 刪除資料庫 db.dropDatabase() ``` 我們可以設定一筆資料給尚未創建的Collection,當我們賦予資料時Collection會即時被創建,如下我們透過`use demo`使用demo資料庫,並且在該資料庫新增一筆資料,該資料檔案格式為Json,且創建在一個名為employee的Collection,此時employee尚未被創建,當我們新增資料後會自動創建。 ```bash= db.employee.insertOne(json資料) ``` 我們可以在Json資料那個欄位中帶入以下資料 ```json= { "first_name": "Robin", "last_name": "Jackman", "title": "Software Engineer", "salary": 3000, "Internship": true } ``` ![](https://i.imgur.com/D8M2RFa.png) 透過以下指令可以進行Collection的查詢 ```bash= # 查詢指令 db.employee.find() # 美化查詢結果 db.employee.find().pretty() # 指定ID的添加資料 db.employee.insertOne({"a":1, "b":2, "_id":想指定的id不可重複}) # 刪除指定資料 db.employee.deleteOne(filter) # 刪除id=1的 db.employee.deleteOne({"_id":1}) # 刪除a=1的資料(a=1),若資料有多筆則優先刪除先進者。 db.employee.deleteOne({"a":1}) # 刪除多筆a=1的資料(a=1) db.employee.deleteMany({"a":1}) # mongo中的比較符號$gt,當我們要刪除salary>2000的資料時 db.employee.deleteMany({"salary":{"$gt":2000}}) # 插入多個資料 db.employee.insertMany([Json資料]) # 查詢多筆資料透過filter,indOne內建pretty所以不需要特別加 db.employee.find({"title" : "Software Architect"}).pretty() # 透過updateOne更新資料,需要增加$set修飾字 db.employee.updateOne(filter,{$set:{要修改的內容}}) # 更新資料為salary:200 db.employee.updateOne({"_id" : ObjectId("6033cb0212576a5889dd3dae")},{$set:{"salary":300}}) # 如果單純使用update則如同PATCH,一次更新多個,且不需$set db.employee.update({"_id" : ObjectId("6033cb0212576a5889dd3dae")},{"salary":300}) # 如果使用repleaceOne則相當於更新一個的update db.employee.replaceOne(filter,data) # find 最多只會顯示20筆資訊,輸入it可以顯示更多 db.employee.find() # 返回特定資訊(節省效能),第一個filter為空代表返回全部,屬性設1代表返回內容。 db.employee.find({}, {first_name:1, last_name:1, salary:1}) # 透過一個filter查詢Json中陣列內含的值 db.employee.find({hobby:"book"}).pretty() # 透過多個filter查詢Json中陣列內含的值,利用中括弧。 db.employee.find({hobby:["book","movie"]}).pretty() # 透過$in 查詢包含其中一個參數,此範例查詢包含book或movie db.employee.find({hobby:{$in:["book","movie"]}}).pretty() # 查詢內含嵌套的資訊 db.employee.findOne({"嵌套的欄位":值}) db.employee.findOne({"contact.phone":1111}) # 懶人請複製 db.myFristDB.insertMany([{name:"A", age:10},{name:"B",age:12},{name:"C",age:13}]) ``` 針對特定對象查詢特定欄位。 ```bash= # filter直接查詢所有,再透過查詢後的結果輸出title值 db.employee.findOne({}).title # 如果對象有內嵌資料則可以透過多個.來訪問 db.employee.findOne({}).contact.phone ``` 透過typeof查詢欄位型態 ```bash= typeof db.employee.findOne({}).contact.phone ``` 時間的兩種類型 ```bash= # ISODate var a = new Date() # Timestamp (1970) var b = new Timestamp() ``` 數字型態 ```bash= # 宣告長整數 var a = NumberLong(1233) ``` 透過程式語言操作mongoDB須參考官方Driver。 不推薦使用Insert語法,因為不會回傳ID回來。 若一次插入大量資料到db裡面時,此時若有相同的id,前面的插入不會失敗,但是在該相同id的資料後插入的資料會失敗。 ![](https://i.imgur.com/0Sa08HT.png) 透過find來檢視 ![](https://i.imgur.com/aD1VfC2.png) 透過ordered來改變預設模式,我們可以將ordered設為false讓他走訪每一個。 ```bash= db.test.insertMany([{_id:2, b:1}, {_id:1, c:1}, {_id:3, d:4}],{ordered:false}) ``` 再進行一次查詢後發現,成功添加。 ![](https://i.imgur.com/ukvJsJm.png) ## 常用操作 ### mongoImport 在使用mongoImport之前,需要先到[官網下載](https://www.mongodb.com/try/download/database-tools)mongo database tools,並將bin資料夾的所有內容複製到mongodb的bin裡面。 ![](https://i.imgur.com/1PXCMLr.png) 如果我們要引入Json檔案時,可以透過mongoimport將檔案的絕對路徑進行複製,透過`-d`指定資料庫,`-c`指定collection(table),`--jsonArray`則代表該Json是有多筆資料並以陣列形式存放,`--drop`則表示若該collection已存在則刪除該collection。 ```bash= mongoimport /Users/chenshiyu/Downloads/mongoImport.json -d test -c demo --jsonArray --drop ``` ### 簡介method、filter、operator ![](https://i.imgur.com/Tl7xoFA.png) 透過$and 尋找多條件 ```bash= db.movie.find({$and:[{條件1:值},{條件2:值}]}) # 搜尋評分>7 且 評分<8 db.movie.find({$and:[{imdb_score:{$gt:7}},{imdb_score:{$lt:8}}]}).count() ``` nor 就是 or 的相反 $exits = True 代表存在 查詢單個內嵌資料的語法 ```bash= db.people.find({"欄位.內部欄位":"值"}) db.people.find({"hobby.type":"Movie"}) ``` 查詢內部欄位資料數量的語法 ```bash= # 透過$size修飾字來查詢hobby數量為2的資料 db.people.find({hobby:{$size:2}}) ``` 透過多條件查詢內部欄位的語法 ```bash= # 透過$elemMatch 查詢內嵌欄位的資訊 db.people.find({欄位:{$elemMatch:{內部欄位1:值,內部欄位2:值}}}) db.people.find({hobby:{$elemMatch:{type:"Movie", rating:3}}}) ``` ### 排序 我們可以透過sort函數來將想排序的資料進行排序,sort排序是針對find出來的結果來進行。 ```bash= # 1則代表升冪、-1則代表降冪 db.movie.find().sort({排序欄位:1}).pretty() db.movie.find().sort({imdb_score:1}).pretty() # 多條件排序,排序完分數用年份進行排序 db.movie.find().sort({imdb_score:1,yeas:1}).pretty() # 透過limit()顯示超過20筆資訊(預設限制) db.movie.find().sort({imdb_score:1,yeas:1}).pretty().limit(30) # 透過skip忽略掉前幾個(分頁用) db.movie.find().sort({imdb_score:1,yeas:1}).pretty().limit(3).skip(3) # 若我們今天要將limit出來的結果進行排序,可以使用aggregate() db.movie.aggregate([{$limit:3},{$sort:{imdb_score:1}}]) ``` ### 更新 我們可以透過updateOne or updateMany來新增或修改一個或多個資料。 ```bash= db.people.updateOne({filter},{$set:{想更改的欄位:想更改的值}}) # 找到資料內名稱為Jack的資料(1筆),並將他的phone設為特定值 db.people.updateOne({name:"Jack"},{$set:{phone:1234}}) # 找到hobby.type內符合Movie的資料(多筆),並將他的phone設為特定值 db.people.updateMany({"hobby.type":"Movie"},{$set:{phone:8899}}) ``` 我們可以透過$rename來重新命名key的名稱,目前key為phone。 ![](https://i.imgur.com/tJlGTwy.png) ```bash= db.people.updateMany({filter},{$rename:{原key:"新名稱"}}) db.people.updateMany({},{$rename:{phone:"phoneNumber"}}) ``` ![](https://i.imgur.com/tX2CM2f.png) ### 刪除key 刪除key ```bash= # 我們可以透過$unset來刪除資料中的key,刪除key同時資料也會刪除。 db.people.updateMany({filter},{$unset:{想刪除的key:1}}) db.people.updateMany({},{$unset:{phoneNumber:1}}) ``` ### 運算 增加的指令 ```bash= # 我們可以透過inc來增加特定欄位的值(做運算) db.people.updateOne({filter},{$inc:{欄位:值}}) # 針對名稱為Jack的資料,將年齡增加一歲,若要扣則增加負數 db.people.updateOne({name:"Jack"},{$inc:{age:1}}) ``` 取最大最小並修改的操作符 ```bash= # 若filter尋找對象的值大於max所設定,則不進行更改,反之改為設定值 db.people.updateOne({name:"Jack"}, {$max:{age:35}}) # $min相同道理 db.people.updateOne({name:"Jack"}, {$min:{age:5}}) ``` ### 多條件查詢 多條件查詢 ```bash= db.people.find( { hobby: { $elemMatch: { $and: [ { type:"Movie" }, { rating:{$gte:3} } ] } } } ) ``` 針對多條件查詢出來的結果進行增加值 ```bash= # 後面串接$inc,這裡要注意的是我們需要透過.$.欄位來設值。 db.people.updateMany( { 欄位: { $elemMatch: { $and: [ {內部欄位: "值"}, { 內部欄位2: { filter } } ] } } }, { $inc: { "欄位.$.想修改的內部欄位": 1 } } ) # 將rating +1 db.people.updateMany({hobby:{$elemMatch:{$and:[{type:"Movie"}, {rating:{$gte:3}}]}}}, {$inc:{"hobby.$.rating":1}}) ``` upsert針對未創建的資料進行創建。 ```bash= # 若我們今天update一個未存在資料庫的資訊,則資料不會被新增 db.people.updateOne({name:"Max"}, {$set:{name:"Max", age:30}}) ``` ![](https://i.imgur.com/PVDzVCc.png) ```bash= # 我們可以將upsert設為true,若資料未存在,則會自動新增 db.people.updateOne({name:"Max"}, {$set:{name:"Max", age:30}},{upsert:true}) ``` ![](https://i.imgur.com/teNvxS6.png) ### deleteMany 我們可以透過deleteMany清空所有資料,Collection還是會留著。 ```bash= # collection會留著 db.people.deleteMany({}) # collection會刪除 db.people.drop() # 刪除整個database db.dropDatabase() ``` ## 進階操作 ### Index 為什麼要用index來尋找,因為當我們使用條件搜索時,會走訪所有的資料,因此會花費大量的資源。 將collection表進行排序並建立好index的表,透過指標的指向來對應資料,查詢時我們只需要針對index做搜尋就可以以更快的方式找到資料。 ![](https://i.imgur.com/GeFJ6S3.png) #### Explain查詢效能 透過explain()我們可以知道查詢的細節,如果加入"executionStats"則可以顯示花費時間等更多詳細資訊,下圖可發現總花費22毫秒。 ```bash= db.test.explain("executionStats").find({year:{$gte:2015}}) ``` ![](https://i.imgur.com/YzfwXzo.png) #### 創建index ```bash= # 針對特定欄位建立index,其數字1與-1差別在於升冪、降冪。 db.test.createIndex({欄位:數字}) db.test.createIndex({year:1}) ``` 建立完成後,再進行一次查詢可發現,查詢速度變更快了,從原本的22毫秒變為6毫秒,我們可以看到在。 ![](https://i.imgur.com/cJSGOmT.png) index搜尋未必快,當我們搜尋大量的數據時,因為index還需要映射到collection中,所以會導致效能大幅降低。 ```bash= # 搜尋2015年以前的影片 db.test.explain("executionStats").find({year:{$lt:2015}}) ``` 在有Index的狀況下需要花費56秒 ![](https://i.imgur.com/R5jVOto.png) #### 刪除index 嘗試刪除index,再進行一次搜尋 ```bash= db.test.dropIndex({year:1}) ``` 僅僅花費23毫秒,比index搜尋還快。 ![](https://i.imgur.com/w0KvERH.png) 取得Index,系統默認會採用_id當成index。 ```bash= db.people.getIndexes() ``` #### 創建一個unique的index ```bash= # 我們可以設定name的欄位為unique,當插入新的資料為重複名稱時會插入失敗。 db.people.createIndex({name:1}, {unique: true}) ``` ![](https://i.imgur.com/8LwzftN.png) #### TTL index 我們可以建立TTL index來將資料進行控管,設定一個TTL時間,若超時則資料庫會自動刪除,通常用在log。 ```bash= # 插入一筆資料,並建立一個欄位createAt, new Date()會存當下時間。 db.ttl.insertOne({name:"Jack",createAt: new Date()}) # 建立ttl index db.ttl.createIndex({createAt:1}, {expireAfterSeconds: 10}) # 過10秒後搜尋會發現資料自動被刪除 db.ttl.find() ``` ![](https://i.imgur.com/Sg9gjvG.png) ### compound index 聯合索引 #### 建立聯合索引 透過多個索引鍵來設定index。 ```bash= db.people.createIndex({name:1, age:1}) ``` #### 聯合索引的查詢 當我們設定聯合索引後,如果是單獨針對「第一個」index進行查詢,則也會套用聯合索引的查詢方式。 ```bash= db.people.explain().find({name:"Jack"}) ``` 我們會發現stage還是index搜索,且index名稱是我們剛剛設定的聯合索引。 ![](https://i.imgur.com/LzAwKpN.png) #### 聯合索引的誤區 如果單獨查詢index則會發現,並不會套用聯合索引的查詢。 ![](https://i.imgur.com/yBmlvXu.png) 我們可以透過以下指令刪除聯合索引 ```bash= db.people.dropIndex({name:1, age:1}) ``` #### 字串的索引 創建字串索引 ```bash= # 針對索引字串設定text db.movie.createIndex({title:"text"}) ``` 當我們要進行搜索時,搜索的值一定要是關鍵字,若不是則會無結果。 ```bash= # 搜尋單一個關鍵字結果 db.movie.find({$text:{$search: "Avengers"}}) # 搜尋多關鍵字結果,搜尋結果會出現包含關鍵字1 或 關鍵字2 db.movie.find({$text:{$search: "關鍵字1 關鍵字2"}}) # 若想搜尋一串的關鍵字則可以透過\"關鍵字\" db.movie.find({$text:{$search:"\"The Amazing\""}}) # 希望搜尋結果符合大小寫規範可以設定$caseSensitive為true db.movie.find({$text:{$search:"\"The Amazing\"", "$caseSensitive": true}}) # 刪除字串index,需要先透過getIndexes抓到name,再透過以下指令 db.movie.dropIndex("title_text") # 透過設定background在後台創建index,當資料量太大的時候創建index需要大量時間 db.people.createIndex({name:1},{background:true}) ``` ### 地理位置存取 google map 先緯度後經度 GeoJSON 先經度後緯度 [GeoJson](https://zh.wikipedia.org/wiki/GeoJSON)專門用來存取地理訊息,Type 分為很多種,可參考網址內容,這裡我們採用的Type為Point代表一個座標位置。 ![](https://i.imgur.com/ie90joj.png) ```bash= db.country.insertOne( { name:"ndhu", position: { type:"GeoJsonType", coordinates:[經度,緯度] } } ) db.country.insertOne({name:"ndhu", position:{type:"Point", coordinates:[121.5435267,23.9416943]}}) ``` #### 為GeoJSON創建Index 我們需要透過內建的"2dsphere"來創建GeoJSON的index。 ```bash= db.restaurant.createIndex({location:"2dsphere"}) ``` #### 找到範圍內的所有餐廳 透過\$nearSphere、\$geometry、$maxDistance來尋找300公尺內的所有餐廳。 ```bash= # 拆解來看$nearSphere需要兩個參數,分別為$geometry與$maxDistance。 # $geometry 需要一個座標位置,包含類別與座標。 db.restaurant.find( { location: { $nearSphere: { $geometry: { type:"Point", coordinates:[-73.9614385,40.6631119] }, $maxDistance:300 } } } ) db.restaurant.find({location:{$nearSphere:{$geometry:{type:"Point", coordinates:[-73.9614385,40.6631119]},$maxDistance:300}}}) ``` #### 查看某點是否位於區域內 當我們今天創建了一個多邊形的區域(Polygon),我們可以先將資料建為index後,來查詢某一個點是否在一整個區域內。 ```bash= db.neighborhood.createIndex({geometry: "2dsphere"}) db.neighborhood.findOne( { geometry: { $geoIntersects: { $geometry: { type:"Point", coordinates:[ 121.5381638, 23.894204 ] } } } } ) ``` 若是返回null則代表不存在於該區域內。 ### 聚合 aggregate需要傳入一個陣列,這個陣列內包含許多元素,每一個元素執行完的結果傳給下一個元素執行。 #### $match查詢 ```bash= # 我們可以單純放入一個元素來查詢符合條件的資料,這裡放入$match db.order.aggregate( [ {$match:{欄位:"值"}} ] ) db.order.aggregate([{$match:{status:"finished"}}]) ``` ![](https://i.imgur.com/D4FD8ST.png) #### $group分組 ```bash= # 如果放入兩個元素,則第一個元素執行完的結果會傳給第二個元素。 # $group 代表將搜尋的結果分組為變數名稱1與變數名稱2 # $sum 則是針對特定欄位進行加總 db.order.aggregate( [ { $match: {status:"finished"} }, { $group: { _id:"$欄位名稱", 變數名稱1(自訂):{$sum:"$欄位名稱"} } } ] ) db.order.aggregate( [ { $match:{status:"finished"} }, { $group: { _id:"$name", total:{$sum:"$amount"} } } ] ) ``` ![](https://i.imgur.com/ziRb19V.png) #### $sort排序 ```bash= # 結合上述查詢、分組再加上排序 # 將分組出來的結果進行排序 db.order.aggregate( [ { $match:{status:"finished"} }, { $group: { _id:"$name", total:{$sum:"$amount"} } }, { $sort:{total:-1} } ] ) ``` ![](https://i.imgur.com/XfUPYaW.png) 求每個導演的票房總和 ```bash= db.movie.aggregate( [ { $group: { _id:"$director_name", total:{$sum:"$gross"} } }, { $sort:{total:-1} } ] ) ``` ![](https://i.imgur.com/SoyLydP.png) 求每個導演平均分 ```bash= db.movie.aggregate( [ { $group: { _id:"$director_name", avg:{$avg:"$imdb_score"} } }, { $sort:{avg:-1} } ] ) ``` #### lookup 在這裡我們有兩張表,分別是product只記錄商品價格與名稱,另一張表則記錄orders,如下所示。 product ![](https://i.imgur.com/QG7DEua.png) orders ![](https://i.imgur.com/IBJ8OWf.png) 如果今天我們有一個需求是在orders中,找到price of product >20的訂單,那麼我們可以透過$lookup的方式來進行尋找。 ```bash= db.product.aggregate([ { $lookup: { from: "orders", localField: "_id", foreignField: "pid", as: "inventory_docs" } } ]) ``` * from: 需要與目標關聯的表,在這裡是orders * localField: 需要關聯的鍵,在這裡是_id * foreignField: 與目標關聯的鍵,在這裡是_pid * as: 變數名稱 #### project project是一種pipline技術,有先後順序之分,會依序執行前面所承接下來的結果。 透過project來定義要返回的值 ```bash= db.order.aggregate( [ {$match:{status:"finished"}}, {$project:{name:1,_id:0}} ] ) ``` ![](https://i.imgur.com/k7VWrds.png) 在project新增內容 ```bash= # 我們可以在project裡面自訂新變數,並指派一個陣列透過中括弧 # 內容值可以是欄位中的任何資訊 db.order.aggregate( [ {$match:{status:"finished"}}, { $project: { name:1, _id:0, 自訂變數:["$欄位1","$欄位2"] } }, ] ) db.order.aggregate( [ {$match:{status:"finished"}}, { $project: { name:1, _id:0, content:["$amount","$name"] } }, ] ) ``` ![](https://i.imgur.com/S0MC0nd.png) #### 字串轉大寫 查詢結果轉大寫 ```bash= # 透過$toUpper將結果轉為大寫 db.order.aggregate( [ {$match:{status:"finished"}}, { $project: { name:{$toUpper:"$name"} } }, ] ) ``` #### 字串串接 ```bash= # 透過$concat進行字串串接,其內容都必須為字串 # 透過$toString可以將數字轉為string db.order.aggregate( [ {$match:{status:"finished"}}, { $project: { name:{$toUpper:"$name"}, new:{ $concat: [ "$欄位1", "想合併的字串", "$欄位2" ] } } }, ] ) db.order.aggregate( [ {$match:{status:"finished"}}, { $project: { name:{$toUpper:"$name"}, new:{ $concat: [ "$name"," spand ", {$toString: "$amount"} ] } } }, ] ) ``` ![](https://i.imgur.com/JxR30mB.png) #### 條件判斷 ```bash= # 透過$cond來撰寫條件 # $gt接收一個陣列,陣列包含兩個參數,前大於後回傳true db.order.aggregate( [ {$match:{status:"finished"}}, { $project: { name:{$toUpper:"$name"}, new: { $concat: [ "$name"," spand ", {$toString: "$amount"} ] }, flag: { $cond: { if:{$gt:["$amount",200]}, then: "big", else: "smaill" } } } }, ] ) ``` ![](https://i.imgur.com/e1hc1GI.png) #### 取得內容數量 ```bash= # 透過$size取得欄位長度。 db.test.aggregate( [ { $project:{ _id:0, name:1, numHobby: {$size:"$hobby"}, hobby:1 } } ] ) ``` ### 切片 ```bash= # 透過$slice進行資料切片,若是取的數量大於資料數量則取全部(不會錯) db.test.aggregate( [ { $project: { _id:0, name:1, numHobby: { $size:"$hobby" }, hobby: { $slice:["$欄位", 從第幾個開始, 取幾個] } } } ] ) db.test.aggregate( [ { $project:{ _id:0, name:1, numHobby: { $size:"$hobby" }, hobby: { $slice:["$hobby", 0, 2] } } } ] ) ``` ![](https://i.imgur.com/YIcNQaG.png) ### 分類 ```bash= # 我們可以透過bucket將資料進行分類,指定groupBy為想分類的欄位 # 將boundries設定為分類的界線,資料1:包含界線1不包含界線2 # output則是針對每個界線的資料 db.movie.aggregate( [ { $bucket: { groupBy: "$欄位", boundaries: [界線1,界線2,界線n], output: { 變數1:{針對每個界線執行內容}, 變數2:{針對每個界線執行內容} } } } ] ) # $sum 顯示每個分類的總和 db.movie.aggregate( [ { $bucket: { groupBy: "$year", boundaries: [1990, 1995, 2000, 2005, 2010, 2015, 2020], output: { numMoives:{$sum:1}, avgScore:{$avg:"$imdb_score"} } } } ] ) ``` ![](https://i.imgur.com/4vUGCbG.png) ### 聚合中的skip & limit ```bash= # skip與limit不能對調,如果搜尋出來的結果先limit在skip會沒有輸出。 db.movie.aggregate( [ { $group: { _id:"$director_name", avg_imdb:{$avg:"$imdb_score"} } }, { $sort:{avg_imdb:-1} }, { $skip:20 }, { $limit:10 } ] ) ``` ![](https://i.imgur.com/wHr4kt7.png) ### 輸出 將查詢結果輸入到新的Collection中。 ```bash= show collections ``` ![](https://i.imgur.com/5yDhbDg.png) ```bash= # 我們可以透過out關鍵字將結果輸出到新的collection中 db.movie.aggregate( [ { $group: { _id:"$director_name", avg_imdb:{$avg:"$imdb_score"} } }, { $sort:{avg_imdb:-1} }, { $out: "newCollection" } ] ) ``` 執行完畢後,我們透過`show collection`來查詢會發現多了一個collection。 ![](https://i.imgur.com/6NAyvLs.png) 我們可以透過`find()`來檢視該資料,我們會發現是剛才的結果。 ![](https://i.imgur.com/BSqynZD.png) ### geo的聚合 操作之前需要先建立index ```bash= db.restaurant.createIndex({location: "2dsphere"}) ``` 返回前10筆距離最近的餐廳 ```bash= # distanceField 設定為distance代表返回的距離的名稱為distance # geoNear有提供num:值,用法與limit一樣,可以取代limit。 db.restaurant.aggregate( [ { $geoNear: { near: { type: "Point", coordinates: [-73.8701, 40.7523] }, maxDistance: 100000, distanceField: "distance" } }, { $project: { _id:0, name:1, distance:1 } }, { $limit:10 } ] ) ``` ![](https://i.imgur.com/W3LuybN.png) ### 資料庫備份與恢復 ```bash= # 切換一個新的資料夾 # 執行mongodump即可 mongodump ``` ![](https://i.imgur.com/0Yj9kXn.png) #### 備份恢復 ```bash= # 切換到當初備份的資料夾執行以下指令即可完成 mongorestore ``` ![](https://i.imgur.com/7uBN5i1.png) #### 針對特定Collection進行備份 ```bash= mongodump -d 資料庫 -c Collection mongodump -d test -c movie ``` #### 備份恢復指定Collection ```bash= mongorestore --nsInclude 資料庫.欄位 dump mongorestore --nsInclude test.movie dump ``` #### 一鍵恢復並刪除重複 ```bash= mongorestore --drop ``` ![](https://i.imgur.com/gltcT6z.png) ## 認證 ### windows操作 windows 停止mongo ```bash= net stop MongoDB ``` 修改mongod.cfg檔案 location:`MongoDB/server/4.0/bin` 增加以下 ```bash= security: authorization:"enable" ``` windows 啟動mongo ```bash= net start MongoDB mongo ``` ### Mac操作 開啟認證模式,我們需要先停止mongo運行。 ```bash= # 找到進程號 ps -ef | grep mongod kill 22764 ``` 重新運行以下指令,加入了`--auth` ```bash= mongod --dbpath "/Users/chenshiyu/mongodb/data" --logpath "//Users/chenshiyu/mongodb/log/mongo.log" --fork --auth ``` 加入完後,我們做了任何操作都會被視為無權限。 ### 增加使用者 ```bash= use admin db.createUser( { user:"admin", pwd:"admin", roles:[ { role:"readWrite", db: "admin" }, { role: "userAdminAnyDatabase", db: "admin" } ] } ) ``` ![](https://i.imgur.com/ow1FTH4.png) ### 登入用戶 當我們創建完使用者後,我們可以切換到相對應的資料庫去進行登入。 ```bash= use admin # 登入 db.auth("admin","admin") ``` ![](https://i.imgur.com/QBUPs7I.png) 當我們切換到其他資料庫並進行資料的新增時,則會發現使用者的權限不足,因為我們當初只設定該使用者只能操作admin的資料庫。 ```bash= # 切換到未授權資料庫 use test # 插入一筆新資料 db.new.insertOne({a:1}) ``` 我們會發現出現權限不足的錯誤 ![](https://i.imgur.com/EXb8y7H.png) ### Roles #### Database User Roles 所有資料庫都遵循這些角色 * read * readWrite #### Database Administration Roles 所有資料庫都遵循這些角色 * dbAdmin:可以查詢資料庫,但無法增刪改 * userAdmin:可以創建用戶與權限管理 * dbOwner:可以增刪查改,擁有所有權限,包含userAdmin #### backup & restore Roles 如果被賦予這些角色才有辦法進行backup or restore * backup * restore #### All-Database Roles 被賦予的權限可以在所有資料庫執行 userAdminAnyDatabase 代表賦予userAdmin功能,並可以在任何資料庫存取 dbAdminAnyDatabase 也是同理 * readAnyDatabase * readWriteAnyDatabase * userAdminAnyDatabase * dbAdminAnyDatabase ### 開始創建新用戶並進行存取 當我們透過`auth`登入完admin後,切換至要操作的資料庫。 ```bash= use test # 創建兩個用戶,user1讀寫權限、user2只有讀取 db.createUser( { user:"user1", pwd:"user1", roles: [ { role:"readWrite",db:"test" } ] } ) db.createUser( { user:"user2", pwd:"user2", roles: [ { role:"read",db:"test" } ] } ) ``` 創建完兩個用戶之後,我們可以透過以下指令查詢所有用戶 ```bash= db.getUsers() ``` ![](https://i.imgur.com/pjWodwE.png) 登出指令 ```bash= # 需要先回到原本的資料庫 use admin # 執行登出 db.logout() ``` ![](https://i.imgur.com/DuolW2l.png) 切換回test資料庫,並登入剛剛所創建的user1 ![](https://i.imgur.com/FcVyEQp.png) ```bash= # 登入完成後我們就可以使用插入指令了 db.test.insertOne({A:1}) ``` ![](https://i.imgur.com/vSyix8p.png) 同一時間最好就只有一個用戶登入,不然會報錯。 #### 用戶的創建與更新 userAdmin才能進行資料庫使用者的更新。 ```bash= # 首先先將原本user進行登出 db.logout() # 切換到admin資料庫 use admin # 登入admin db.auth("admin","admin") # 切換到創建test的資料庫 use test # 我們可以透過getUser("使用者名稱")來取得詳細權限 db.getUser("user1") # 透過updateUser,這個方法會進行覆寫,因此也需要將原本的屬性添加上 db.updateUser( "user1", { roles: [ { role: "readWrite", db: "test" }, { role: "userAdmin", db: "test" } ] } ) ``` 透過剛剛創建的user1 刪除用戶 ```bash= db.dropUser("user2") ``` ![](https://i.imgur.com/g28UeQJ.png) 創建用戶 ```bash= db.createUser( { user:"user3", pwd:"user3", roles: [ { role:"read",db:"test" } ] } ) ``` ![](https://i.imgur.com/S9O9QCN.png) #### mongoimport認證設定 ```javascript= mongoimport json的絕對路徑 -d 目標資料庫 -c 目標資料表 --jsonArray --authenticationDatabase admin --username "yourName" --password "your password" ``` ## 工具 ### mongoDB圖形化管理工具 [Robo 3T Download](https://robomongo.org/download) 當我們下載完後,執行安裝,會出現以下視窗,我們需要按下create創建一個連線,並在在該連線下建立Authentication ![](https://i.imgur.com/eYw6qmB.png) 創建完連線後,便可以看到旁邊出現資料庫相關資訊 ![](https://i.imgur.com/WeUtJCP.png) [mongoDB compass](https://www.mongodb.com/try/download/compass) ```bash= # 我們先將admin權限放大至root db.updateUser( "admin", { roles:[ { role: "readWrite", db:"admin" }, { role: "userAdminAnyDatabase", db:"admin" }, { role: "root", db:"admin" } ] } ) ``` 我們可以點擊`Fill in connection fields individually`來依序填入資料。 ![](https://i.imgur.com/JaVD702.png) 當我們成功連線進來後可以進入test中的Moive來查看。 ![](https://i.imgur.com/1WZntPx.png) 我們可以在Filter的位置輸入相關的filter來過濾,再按下Find。 ```bash= {year:2015, imdb_score:{$gt:7}} ``` ![](https://i.imgur.com/W6JcDKd.png) 我們可以透過展開option取得更多操作方式 ![](https://i.imgur.com/Hn9h27k.png) 我們可以透過切換到index頁面來新增index ![](https://i.imgur.com/7mJCSug.png) 我們可以到Aggregation中新增聚合,選擇group並將參數填入 ![](https://i.imgur.com/t85B2Qn.png) 按下下方的add Stage,繼續新增條件。 ![](https://i.imgur.com/BASfbnv.png) ## 最終章 ### Replication(主從複製) ![](https://i.imgur.com/PdIJ7qK.png) 當我們運行的機器出現問題時,這時候我們需要由其他Secondary的機器來成為Primary,此時所運用的技術就是Relication,機器之間透過Heartbeat彼此監控,當資料需要同步時則透過Replication同步。 在這裡我們用一台機器模擬多台機器,透過不同port產生服務,再利用`--replSet`將他們設為同一個Set,再執行以下指令之前我們須先刪除原本的進程。 ```bash= mkdir -p ~/db/data1 mkdir -p ~/db/log1 mongod --dbpath ~/db/data1 --port 27021 --replSet set1 --fork --logpath ~/db/log1/mongodb.log mkdir -p ~/db/data2 mkdir -p ~/db/log2 mongod --dbpath ~/db/data2 --port 27022 --replSet set1 --fork --logpath ~/db/log2/mongodb.log mkdir -p ~/db/data3 mkdir -p ~/db/log3 mongod --dbpath ~/db/data3 --port 27023 --replSet set1 --fork --logpath ~/db/log3/mongodb.log mkdir -p ~/db/data4 mkdir -p ~/db/log4 mongod --dbpath ~/db/data4 --port 27024 --replSet set1 --fork --logpath ~/db/log4/mongodb.log ``` ![](https://i.imgur.com/2VDZp9m.png) 由於我們剛剛修改了預設Port,因此在這裡我們連線時需要設定`--port 27021` ```bash= mongo --port 27021 ``` 這時候我們需要進行初始化配置,讓當前節點變為primary ```bash= rs.initiate() ``` ![](https://i.imgur.com/GxO2ukl.png) 透過以下指令來查看set狀態 ```bash= rs.status() ``` 我們會發現此時的成員只有一個。 ![](https://i.imgur.com/rJOGACJ.png) 添加成員 ```bash= rs.add("localhost:27022") rs.add("localhost:27023") rs.add("localhost:27024") ``` ![](https://i.imgur.com/KSRgSTW.png) 查詢後會發現有四個成員 ```bash= rs.status() ``` 移除節點 ```bash= # 當我們移除節點時,會將進程也一併銷毀 rs.remove("localhost:27024") ``` 重新加入27024並將設定為仲裁節點,仲裁節點(arr),需要透過`rs.addArb`來添加。 ```bash= rm -rf db/data4 rm -rf db/log4 mkdir -p ~/db/data4 mkdir -p ~/db/log4 mongod --dbpath ~/db/data4 --port 27024 --replSet set1 --fork --logpath ~/db/log4/mongodb.log # 重新連回primary節點 mongo --port 27021 rs.addArb("localhost:27024") ``` ![](https://i.imgur.com/qvt0c7Z.png) 嘗試Replication ```bash= # 切換到test並新增一筆資料 db.demo.insertOne({A:1}) # 退出(假裝出狀況),刪除27021的進程 kill 46716 # 登入其中一個節點並執行查詢status mongo --port 27022 rs.status() ``` 我們會發現,27023變為primary ![](https://i.imgur.com/HTCwWRX.png) 我們可以離開當前mongodb切換到primary ```bash= # 切換到primary mongo --port 27023 # 查看資料庫資料是否在 db.demo.findOne() ``` ![](https://i.imgur.com/aa88G4w.png) 再次啟動以前的primary會發現他已經變為Secondary ```bash= mongod --dbpath ~/db/data1 --port 27021 --replSet set1 --fork --logpath ~/db/log1/mongodb.log mongo --port 27021 ``` ## Sharding 切片 分為Vertical Scaling、Horizontal scaling **Vertical Scaling** 只用單一一台主機,將該台主機的硬體設備弄至最優。 優點:簡單 缺點:不具有可持續性 **Horizontal scaling** 透過增加伺服器數量來分擔效能。 優點:良好擴展性 缺點:複雜資料庫軟體架構 Shard 專門存取Database資料,一個Shard可能由多個主機所組成。 Config Servers 用於存取MetaData ![](https://i.imgur.com/ACPHJm8.png) 一個Collection可能會被切成多個Shard ![](https://i.imgur.com/kFvkQvP.png) ### 建立一個Shard系統 首先我們必須先終止先前的所有mongo進程,並刪除所有db/* ```bash= rm -rf ~/db/* # 配置一台config server,將replSet名稱設定為config mkdir -p ~/db/config/data mkdir -p ~/db/config/log mongod --port 27022 --dbpath ~/db/config/data --configsvr --replSet config --fork --logpath ~/db/config/log/mongodb.log # 進入mongo27022並初始化 mongo --port 27022 rs.initiate() # 透過mongos配置router # 設定--configdb 為要連接的db # configdb 指向先前設定好的replSet的名稱config # 啟動mongos之前,最好先登入27022 & run rs.initiate() mkdir -p ~/db/mongos/log mongos --configdb config/localhost:27022 --port 27021 --fork --logpath ~/db/mongos/log/mongodb.log # 創建shard1 # --shardsvr 代表為 shard mkdir -p ~/db/shard0/data mkdir -p ~/db/shard0/log mongod --port 27023 --dbpath ~/db/shard0/data --shardsvr --fork --logpath ~/db/shard0/log/mongodb.log # 創建shard2 mkdir -p ~/db/shard1/data mkdir -p ~/db/shard1/log mongod --port 27024 --dbpath ~/db/shard1/data --shardsvr --fork --logpath ~/db/shard1/log/mongodb.log # 進入 mongo --port 27021 # 加入shard sh.addShard("localhost:27023") sh.addShard("localhost:27024") # 查詢shard status db.printShardingStatus() # 更新chunk大小 # 可以把chunk想成切片的最大尺寸 use config db.setting.save({_id:"chunksize", value:1}) # 啟動一個名為testdb sh.enableSharding("testdb") # 指定某個Collection進行分片,將testkey變為shard key sh.shardCollection("testdb.testcollection", {testkey: 1}) ```