MongoDB 學習
MongoDB 簡介
MongoDB是一個文檔數據庫,但文檔并不是一般理解的pdf, word文檔,而是JSON對象,因為文檔來自于“JSON Document”(JSON文檔),所以MongoDB是存JSON對象的數據庫,比如{"greeting”: "hello world"}。說起文檔,想到的應該是JSON對象,所以文檔中的鍵都是字符串,任何utf-8字符都可以,但不能包含\0(空字符),因為它表示鍵的結束,最好也不要包含$和 .,因為它們有特殊的意義, 只在某些特殊場合使用。文檔中的值卻有多種類型,1,String: utf-8 編碼;2,Number類型,默認是double類型,{"x": 3}, {"x": 3.14}, 如果使用整數,要使用NumberInt 和NumberLong. {"x": NumberInt("3")};高精度的小數NumberDecimal, 128個bit,小數點后面34位,它們的參數最好用字符串。3,布爾類型;4,對象類型,也稱嵌入式文檔,比如下面的address的值就是Object類型或嵌入式文檔
{
"name": "John Doe",
"address": {
"street": "123 Park Street",
"city": "Anytown",
"state": "NY"
}
}
5,數組類型; 6 Null 類型;7, ObjectId類型,MongoDB中的每一條文檔,都有一個"_id"屬性,表示這條文檔的唯一性。它的值可以是任意類型,默認是ObjectId類型,插入文檔時,沒有插入"_id", MongoDB會自動生成_id,{"_id": ObjectId()};8,Date類型, MongoDB把時間存儲為64位的整數,代表Unix時間的毫秒數。所以在JS中,Date類可以用于MongoDB的date類型,調用new Date(),比如{"x": new Date()}。9,正則表達式,{"x": /\d/}。Mongodb是類型敏感和大小寫敏感,{"count" : 5}和{"count" : "5"}, {"count" : 5} 和{"Count" : 5}都是兩條不同的文檔,但一條文檔中不能有相同的key值。{"greeting" : "Hello, world!", "greeting" : "Hello, MongoDB!"}是不允許的。
MongoDB并不是直接存儲文檔,而是把文檔放到集合中。通常來講,放到一起的東西都要有共同點,比如集合中的每一條文檔(JSON對象)都有相同的屬性(形狀),但MongoDB并沒有要求這一點,下面兩條完全不一樣的文檔就可以放在同一個集合中
{"greeting" : "Hello, world!", "views": 3}
{"signoff": "Good night, and good luck"}
這也稱MongoDB無Schema。那是不是意味著,所有的文檔都放到一個集合中?也不是,首先文檔的大小有限制,一個文檔最大16MB。再者文檔也有嵌套層級的限制,最多100級,最后,也是最重要的一點,不相關的內容放到一起,不好用 ,尤其是查詢。在MongoDB的文檔中,不存在的字段,總是被看成是null。如果要查詢字段的值不等于某個值,null和某個值也不相等,也會查出這條文檔。最好相同形狀(Schema)的文檔放到一個集合中。集合名,也是任意的utf-8字符, 但不能是空字符串,也不能包含\0, 不要使用 system.開頭,不要包含$符。集合名稱有一個慣例,就是使用命名空間的子集合,用. 號隔開。比如 blog.posts, blog.authors.
多個集合放在一起就組成了數據庫,一個應用的所有數據都放到一個數據庫中。數據庫的名字,是不區分大小寫的,最長是64個字節。把數據庫的名字和集合的名字合在一起,就形成了一個完整的集合的名字, 稱為命名空間。如果cms數據庫下面有blog.posts集合,那么命名空間就是cms.blog.posts, 命名空間的最大長度是120個字節,但在實際中,最好不要超過100個字節。
所以在使用MongoDB數據庫時,要先有數據庫,再有集合,最后才在集合中增刪除改查文檔。
安裝和連接MongoDB服務器
在Linux下,官網下載 .tgz 文件(mongodb-linux-x86_64-ubuntu2004-7.0.11.tgz),默認下載到了下載目錄,進入到下載目錄
tar -zxvf mongodb-linux-x86_64-ubuntu2004-7.0.11.tgz # 解壓 mv mongodb-linux-x86_64-ubuntu2004-7.0.11 mongodb # 重命名 mv mongodb /opt #移動到/opt 目錄 sudo ln -s /opt/mongodb/bin/* /usr/local/bin/ # 創建命令的軟鏈接 sudo mkdir -p /data/db/ # 創建mongodb的默認數據存儲路徑,mongodb啟動時自動尋找
sudo mongod 啟動服務。安裝圖形化界面工具MongoDB Compass,它是一個.deb 包,直接雙擊就能安裝 。打開軟件,

點擊Save,輸入名稱,把這個連接保存起來,點擊Connect 進行連接,彈出的界面中,最底部有一個_MONGOSH,點擊,彈出命令行shell,就可以輸入命令。

shell自動連接到test數據庫。如果要創建或切換數據庫,就要使用use 數據庫名,比如 use mflix。如果mflix數據庫存在,則切換到mflix。如果數據庫不存在,則先創建再切換,但此時,mflix數據庫只是在內存中,不會在硬盤中創建數據庫。如果show dbs, 是查不到mflix數據庫的。只有在向數據庫中插入數據的時候,才會在硬盤真正創建數據庫。切換成功,這個數據庫連接就賦值給全局變量db,通過db變量就可以操作數據庫了。在shell中,輸入db,按enter,就可以看到連接或操作的是哪個數據庫。db.[集合名]就可以獲取當前數據庫下面的任意一個集合,就可以進行增刪除改查了。
MongoDB 數據庫操作
創建集合:可以通過createCollection()方法,比如db.createCollection('movies'),也可以通過向不存在的集合中插入文檔的方式來創建集合。當向一個集合中插入文檔時,MongoDB會先檢查這個集合是否存在,如果不存在,則先創建這個集合,再執行插入操作。
db.movies.insertOne( {title: "Yahya A"} )
movies集合創建成功,同時插入數據。如何在shell中多行輸入呢?第一行輸入命令,以左括號(結束,再shift+enter 換行,想要輸入多少行內容,就按多少shift+enter,想要結束命令,輸入右括號),按enter, 執行命令。

show collections 可以顯示數據庫中有多少個集合。如果沒有集合,則什么都不顯示。
插入文檔:除了insertOne() 插入一條文檔外,還有insertMany()插入多條文檔。insertMany() 接受數組,數組中的每一個元素都是一條文檔
db.movies.insertMany( [ { "title": "Blacksmith Scene", "rated": "UNRATED", "year": 2020, "awards": { "wins": 1, "nominations": 0, "text": "1 win." }, "comments": [{ "rating": 6.2, "votes": 1189 }] }, { "title": "The Great Train Robbery", "languages": [ "English" ], "rated": "TV-G", "year": 2022, "awards": { "wins": 1, "nominations": 0, "text": "1 win." }, "comments": [{ "rating": 7.4, "votes": 9847 }] }, { "rated": "PASSED", "title": "In Old Arizona", "year": 2024, "languages": [ "English", "Spanish", "Italian" ], "awards": { "wins": 1, "nominations": 4, "text": "Won 1 Oscar. Another 4 nominations." }, "comments": [ { "rating": 5.8, "votes": 576 }, { "rating": 7.2, "votes": 600 } ] } ] )
插入多條文檔,會有一個問題,如果中間的數據出錯,怎么辦?默認有序插入(一條一條插入),如果出錯了,出錯的那一條數據的前面的數據,都會插入到數據庫里面,后面的數據就不會插入到數據庫里面。不過,插入的方式可以配置,insertMany接受第二個參數,是個對象,它有一個ordered屬性,值是true, 表示有序插入,值為false,表示無序插入的,默認值是true。無序插入,就是能插入的都插入,只有出錯的數據不會插入。
查詢文檔:find()和findOne() 。find()返回符合條件的所有文檔,findOne返回符合條件的一條文檔。使用方式都一樣, 兩個可選參數(查詢條件和投影),投影就是要顯示哪些文檔字段,查詢是整條文檔都查詢出來,但我們只需要文檔的某些字段,就是投影。find()如果沒有查詢條件,就是使用默認參數{},find()或find({}) 將返回集合中的所有文檔,但通常都會有查詢條件。查詢條件是個對象,鍵表示查詢哪個字段,值表示符合什么條件。條件可簡單,可復雜,簡單的話,就是基本類型,db.movies.find({"title" : "Blacksmith Scene"})。復雜的話,就是個對象,使用各種操作符,正則表達式,數組和嵌入式文檔等。
比較操作符: $eq(等于),$ne(不等于),$gt(大于), $gte(大于等于), $lt(小于), $lte(小于等于),使用方式是 {字段名:{操作符: 值}},比如,
db.movies.find({ "rated": { $eq: "UNRATED" } })
要注意的是$ne,會查出不包含查詢字段的文檔,
db.movies.find({ "rated": { $ne: "UNRATED" } })
/* 結果中有{ _id: ObjectId("6653070b0a2ddb2e92ae720d"), title: 'Yahya A' },但它并沒有rated的字段*/
在MongoDB中,如果文檔中沒有某個字段時,這個字段的值為null,null和查詢條件并不相等,所以就出現了,為了避免這種情況,需要再加一個查詢條件$exists: true
db.movies.find({ "rated": { $ne: "UNRATED", $exists: true } })
如果查詢字段是某些值中的一個值,使用$in, $nin,
db.movies.find({"rated": {$in: ["TV-G", "PASSED"]}})
$nin和$ne是一個道理,不包含rated字段的文檔也會包含進來,還是要加$exists: true
db.movies.find({ rated: { $nin: ["UNRATED", "PASSED"], $exists: true } })
db.movies.find({ $and: [{ "title": "Blacksmith Scene" }, { "rated": "UNRATED" }] // $or: [{ "title": "Traffic in Souls" }, { "title": "Blacksmith Scene" }] // $nor: [{ "title": "Traffic in Souls" }, { "title": "Blacksmith Scene" }] })
db.movies.find({ "title": "Blacksmith Scene", "rated": "UNRATED" })
$not 則是把查詢條件進行取反,不符合查詢條件的文檔才會查出來,
db.movies.find({ "rated": { $not: { $eq: "UNRATED" } } })
查詢結果仍然存在不包含rated字段的文檔,和$ne,$nin一樣的道理。還有就是查詢字段的值是不是null ,如果只寫db.movies.find({"z" : null}) ,不包含z字段的文檔也會返回,因此需要檢查字段的值是null,并且存在這個字段。db.movies.find({"z" : {$eq : null, $exists : true}})
正則表達式, 有以下兩種格式,推薦使用第一種,就是如果有options,使用$options字段,而不是使用正則有達式字面量。只有在$in 的情況下,才使用第二種。
db.movies.find( { "title": { $regex: /the great/, $options: "i" } } // $options字段 ) db.movies.find( { "title": { $regex: /The Great/i } } // 正則表達式字面量語法 )
如果字段的值是數組,想查詢該字段是不是包含某個值,可以像查詢普通字段的一樣,{字段名:值},數組中包含該值的所有文檔都會被查出來
db.movies.find( {"languages" : "English"} // 字段languages的值(數組)中包含"English"的文檔都會查出來。 )
如果要查詢該字段是不是包含多個元素,可能會想到使用[]把元素全列出來,
db.movies.find({"languages" : ["English", "Spanish"]})
db.movies.find({languages : {$all : ["English", "Spanish"]}})
如果想查出數組某個位置上的元素是不是某個值,使用key.index的形式,
db.movies.find({"languages.0" : "English"}) // 數組位置1上的元素是"English"的文檔,會被查出來
如果數組的元素是對象,要查詢對象的某個屬性是不是某個值,使用字段名.對象屬性名的方式,
db.movies.find( { "comments.votes": 576 } // comments是數組, 每一個元素都是對象,只要對象的屬性votes是"576",整條文檔就會查出來 )
如果查詢
db.movies.find(
{"comments.votes" : 576, "comments.rating": 5.8}
)
這時可以使用$elemMatch,減少寫commnets
db.movies.find(
{"comments": {
"$elemMatch": {
"votes": 576, "rating": 5.8
}
}}
)
數組和范圍查詢:如果查詢條件是"x" : {"$gt" : 10, "$lt" : 20}}, 文檔 {x: [5, 25]} 卻能匹配到, 因為,25大于10, 滿足第一個條件,5小于20,滿足第二個條件, 范圍查詢對于數組來言也就沒有意義了。還是使用$elemMatch, 數組的每一個元素在不在這個范圍內,它只對單個元素起作用。{"x" : {"$elemMatch" : {"$gt" : 10, "$lt" : 20}}}。 $elemMatch, 只對key的值是數組類型起作用。
如果字段的值是對象類型,想要查詢字段是不是包含某個對象,可以和普通字段一樣進行查詢,
db.movies.find(
{"awards":
{"wins": 1}
}
)
但沒有查出任何數據,還是因為精確匹配。awards字段的值只能有 {"wins": 1},并且順序也要一樣。通常,只想確定awards字段是不是有個wins屬性,它的值是不是2,這時,可以采用對象的屬性的方式,
db.movies.find(
{"awards.wins": 1}
)
默認情況下,find會把整條文檔的所有字段都返回,但通常只需要某幾個字段,這就要用第二個參數,指定返回哪些字段,它是一個對象,需要的字段屬性值是1, 不需要的屬性值是0
db.movies.find(
{rated: "UNRATED" },
{ title: 1 }
);
默認情況下,_id都會返回,如果不想要,可以設為0。但需要注意的是,除了_id外,不能在投影(project)中 混合使用包含或不包含這兩種操作,要么在投影中列出所有包含的字段, 要么在投影中列出所有不包含的字段。數組字段需要單獨處理,因為,默認情況下,查詢數組的某個元素,整個數組的內容都會返回,db.movies.find({"comments.votes" : 576, "comments.rating": 5.8} ) 返回整個comments數組。當返回的文檔中有數組時,字段的整個數組返回
{ _id: ObjectId("66533c610a2ddb2e92ae7213"),
rated: 'PASSED',
title: 'In Old Arizona',
languages: [ 'English', 'Spanish', 'Italian' ],
awards:
{ wins: 1,
nominations: 4,
text: 'Won 1 Oscar. Another 4 nominations.' },
comments: [ { rating: 5.8, votes: 576 }, { rating: 7.2, votes: 600 } ] }
可以限定返回的數組的元素的個數,find的第二個參數中用$slice。
db.movies.find(
{
"comments": {
"$elemMatch": {
"votes": 576, "rating": 5.8
}
}
},
{
"comments": { "$slice": -1 } // 取前幾條,數字幾,就取幾條
// "comments": {"$slice" : -10} // 取后幾條,數字幾,就取幾條
// "comments": {"$slice" : [10, 20]}, 取10-20條, 如果沒有這么多元素, 能取多少取多少
}
)
盡管find的第二個參數只聲明了comments, 但還是返回整條文檔的所有字段。如果只想返回commnets字段, 要使用$
db.movies.find(
{
"comments": {
"$elemMatch": {
"votes": 576, "rating": 5.8
}
}
},
{
"comments.$" : 1
}
)
不過這有個問題,它只返回匹配到的第一條數據。$只返回一個元素,如果字段的數組中有多個元素匹配成功,它只返回第一個。
其實,find()方法的執行結果是封裝到一個集合中的,并且返回一個游標(迭代器),指向這個集合。需要迭代或遍歷集合,才顯示查詢到的文檔。但在mongo shell中,我們并沒有使用游標,也顯示了執行結果,那是因為Mongo shell中執行 find() 時,會自動迭代游標,展示前20條文檔。當查詢的數據量過大時,它會顯示 type it for more, 這就是因為find其實返回的是一個游標,而不是數據,只不過shell默認遍歷20條數據給你。 如果要在編程語言中,就需要手動迭代游標了。游標使用也比較簡單,直接把find()的返回值賦值給一個變量
var movies = db.movies.find({"rated" : "UNRATED"})
游標有next()方法,移動游標到下一個位置,并把文檔返回。默認情況下,游標在集合的開始位置。當第一次調next()方法時,游標移動到集合的第一條文檔,然后把該文檔返回。再調一次,游標移動到第二條位置,文檔返回。當游標移動到集合中的最后一條文檔,再調next方法,就會報錯。所以在調用next之前,先調用hasNext()方法。
movies.hasNext()
movies.next()
while(movies.hasNext()){
printjson(movies.next())
}
除了hasNext()和next()方法外,游標還有forEach(), limit(), 和skip(),sort() 和count()方法。forEach(), 用于遍歷集合
movies.forEach(printjson)
limit() 限制返回的結果的條數,movies.limit(1),相當于db.movies.find({"rated": "UNRATED"}).limit(1)
skip()跳過幾條數據,comments.skip(1),相當于db.movies.find({"rated": "UNRATED"}).skip(1)
sort() 用于排序。它接受一個對象,鍵為按哪個字段進行排序,值為-1或1,1表示升序,-1表示降序。movies.sort({"year": 1}),相當于db.movies.find({"rated" : "UNRATED"}).sort({"year": 1}) .官網https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order/,有比較詳細地說明。排序的比較大小,也是使用該規則。mongodb是弱類型,同一個字段可以取不同類型的值。

count()返回集合中有多少條文檔。movies.count(), 相當于db.movies.find({"rated": "UNRATED"}).count()
需要注意的是,count() 默認會忽略limit和skip方法的返回,如果不想忽略,則給它傳遞一個參數true。 注意skip() 要先于limit()函數執行。sort函數則是在skip和limt函數之前執行,所以執行順序永遠是sort(), skip(), limit()
整個集合也有一個count()方法,它返回集合中有多少條文檔。如果沒有什么參數,就返回整個集合有多少條文檔。db.movies.count() 返回整個movies集合中有多少條文檔,此時,MongoDB不會真正地一條一條文檔去數,而是查詢集合的元信息,所以有時會不準確。如果有參數,就返回符合查詢條件的文檔數,此時MongoDB 是真正地從集合中數出來了, db.movies.count({"num_mflix_comments" : 6})。在mongodb4.0中,這兩種不同的行為拆分成了兩個函數, countDocuments() 和 estimatedDocumentCount(). countDocuments() 它必須接受一個查詢參數, 如果查詢整個集合,參數是{}. countDocuments({}) , 它總是查詢整個集合,然后返回值。estimatedDocumentCount 則是 根據集合的元信息,它不會對整個表進行查詢。它不接受參數,只返回整個集合的文檔數。
distinct() 函數,它接受一個字段,找出這個字段中所有不相同的值,返回包含這些值的數組。 比如電影中的評分,用戶肯定有相同的評分,找出一共幾種類型的評分,就要使用distinct(). db.movies.distinct("rated") 返回[ 'G', 'TV-G', 'UNRATED' ], 'UNRATED'只出現了一次。它還有一個可選的第二個參數,就是查詢條件,比如查詢某一年的評分 db.movies.distinct('rated', {year: 2020})
替換文檔:replaceOne()替換一條文檔,兩個參數一個是查詢條件,查出要替換的那條文檔,一個是要替換成的新文檔。需要注意的是,_id 字段是不能替換的 ,它是不可變的。替換的時候,新文檔要么不寫_id, 要么保證_id和原文檔中_id 一致。安全的替換方式,使用_id 找到要替換的文檔,新文檔中不包含_id。假設title 為Yahya A的_id 為6653070b0a2ddb2e92ae720d
db.movies.replaceOne( {"_id" : ObjectId("6653070b0a2ddb2e92ae720d")}, {"title": "Traffic in Souls"} )
替換文檔時,如果沒有找到要替換的文檔,MongoDB什么都不會做。如果找到文檔就替換,找不到文檔,就插入這條文檔,這叫upsert。 This operation is called an update (if found) or insert (if not found), which is further shortened to upsert, 給replaceOne設置第三個參數,{upsert: true}。
db.movies.replaceOne( {"_id" : 1}, {"name": "Gertie the Dinosaur"}, {upsert: true} )
找不到{"_id": 1 }這條文檔,所以就插入了這條新文檔。findOneAndReplace() 默認會返回替換之前的文檔,除可以配置sort,project外,還可以配置{returnNewDocument: true}, 返回替換后的文檔
db.movies.findOneAndReplace( {"_id" : 1}, { title: 'In the Land of the Head Hunters' }, { projection: {"_id": 0, "title": 1}, returnNewDocument: true } ) // 返回 { title: 'In the Land of the Head Hunters' }
更新文檔: 替換是整條文檔的替換,但在大多數情況下,只需要更新文檔的某個或某些字段。updateOne更新一條文檔, updateMany更新多條文檔,它們都接受兩個參數, 一個是查詢條件,一個是要更新的字段及其新值,這里要注意,更新不是給字段賦新值,而是使用更新操作符。直接給要更新的字段賦新值,它會替換掉整條文檔。mongodb 默認的更新是先刪除,再插入。更新操作符是鍵,它的值才是要更新的字段和和要設置的值。語法就是 {更新操作符:{字段名:值, 字段名: 值,........}},更新操作符有很多,列舉幾個常用的:
$set: 給指定字段設一個新值,如果字段不存在,則在原文檔中創建這個字段,字段的值就為設定的值。
// 由于之前沒有rated 字段,所以新增rated 字段,并設置值為"UNRATED" db.movies.updateOne( {"title" : "Traffic in Souls"},
{$set : {"rated" : "UNRATED"}}
)
// rated 字段已存在,更新值為"PG"
db.movies.updateOne( {"title" : "Traffic in Souls"}, {$set : {"rated" : "PG"}} )
$unset: 刪除字段。如果指定的字段不存在, 則什么都不做,如果指定的字段存在,由于是刪除字段,給它隨便賦個值就可以了。
// 刪除rated字段 db.movies.updateOne( {"title" : "Traffic in Souls"}, {$unset : {"rated" : 1}} )
$inc: 增加或減少字段的值,$mul: 對字段乘一個數,它們都只對數字類型的字段起作用。如果字段不存在,則會創建這個字段,不過$inc設置這個字段的值為增加的數,$mul 設置這個字段的值為0, 設置的數值需要注意,只寫數字的話,是double類型。inc 之后,結果也變成了double類型,如果想inc int類型,要寫 $inc: {"scoring": NumberInt("2")}
// 沒有字段,就新增 db.movies.updateOne( {"title" : "Traffic in Souls"}, {$inc : {"scoring" : 2}} // 新增scoring字段, 值為2 ) db.movies.updateOne( {"title" : "In the Land of the Head Hunters"}, {$mul : {"scoring" : 2}} // 新增scoring字段,值為0 ) // 有字段 db.movies.updateOne( {"title" : "Traffic in Souls"}, {$mul : {"scoring" : 3}} // 有字段, 就相乘, 2*3, scoring字段的值為6 ) db.movies.updateOne( {"title" : "In the Land of the Head Hunters"}, {$inc : {"scoring" : 2}} // 有字段, 就相加, 0+2, scoring字段的值為2 )
$rename: 重命名字段,如果重命名的字段存在,就重命名,如果不存在,則什么都不做。
db.movies.updateOne( { "title": "Blacksmith Scene" }, { $rename: { "title": "name" } } ) db.movies.updateOne( { "name": "Blacksmith Scene" }, { $rename: { "name": "title" } } )
但如果重命名后的字段名,正好原文檔中也有一個字段名與之相同,原文檔中的字段將會被覆蓋掉,因為先執行刪除除操作,再執行添加操作。$min, $max 先比較,再更新。$min設定的字段的值與原文檔中的字段的值相比較,比如$min設置的值比原文檔中的值小,就改變原文檔中的值,否則,文檔不變。$max正好相反,它設置的值比原文檔中的值大,就改變原文檔中的值,否則文檔不變。$min取兩者中的小值做為文檔的值,$max取兩者中的大值做為文檔的值。如果$min 和$max設置的字段不存在,則創建字段。
// 沒有字段,新增字段 db.movies.updateOne( { "title": "Blacksmith Scene" }, { $min: { "scoring": 5 } } ) db.movies.updateOne( { "title": "In Old Arizona" }, { $max: { "scoring": 8 } } ) // 有字段,比較之后更新 db.movies.updateOne( { "title": "Blacksmith Scene" }, { $min: { "scoring": 4 } } // 4比5小, 修改 ) db.movies.updateOne( { "title": "In Old Arizona" }, { $max: { "scoring": 9 } } // 9比8大, 修改 )
如果某個字段的值是數組類型,對這個字段的更新,就涉及到對數組的操作,增刪改查。$push: 向字段的數組末尾添加元素,如果字段不存在,則會創建新字段。{$push : {字段名 :要添加的元素}}
db.movies.updateOne( { "title": "Traffic in Souls" }, { $push: { "languages": "English" } } //languages字段不存在, 新增 -> languages: ["English"] ) db.movies.updateOne( { "title": "Traffic in Souls" }, { $push: { "languages": "French" } } // languages字段已存在,追加 -> languages: ["French"] )
如果想向數組中一次添加多個值,需配合$each ,$push : {<field_name> : {$each : [<element 1>, <element2>, ..]}}
// 沒有字段,新增字段 db.movies.updateOne( { "title": "In the Land of the Head Hunters"}, { $push: { "languages": {$each: ["French", "English", "Russia"]} } } )
添加多個值,但數組又不能超過固定的長度,要使用$slice,比如$slice: 3。當數組插入元素后,如果長度小于3, 則什么也不用做,如果長度大于3, 就從數組頭開始,截取3個。$slice 可以是-3, 就是數組長度超出3后,從數組末尾倒著截取3個。
db.movies.updateOne( { "title": "Blacksmith Scene" }, { $push: { "languages": { $each: ["French", "English", "Russia", "Chinese"], $slice: 3 } } }, )
截取的時候,還可以先排個序。再添加一個$sort字段, $sort: 1 正向排序,$sort: -1 倒序。此時,按排序好的內容進行插入,如果超出固定的長度,再slice。$slice和$sort 要配合$each一起使用,不能單獨使用。
$addToSet: 和$push一樣,只不過,重復的元素不會添加,讓數組變成了set
db.movies.updateOne( { "title": "Traffic in Souls" }, { $addToSet: { "languages": { $each: ["Russia", "English", "Russia", "Russia"], // 只插入"Russia", } } }, )
數組元素的刪除:$pop:取值1, 刪除數組最后一個元素,取值為-1, 刪除最前面一個元素
db.movies.updateOne( { "title": "Traffic in Souls" }, { $pop: { "languages": -1 } } // 刪除"English" )
$pullAll 接受一個數組,刪除掉這個數組中包含的元素。
db.movies.updateOne( { "title": "Traffic in Souls" }, { $pullAll: { "languages": ["Russia", "French"] } } // 刪除"Russia" 和"French" )
$pull: 給它提供特定的值或特定的篩選條件,用于刪除特定的元素 {$pull: {字段名: {值或 篩選條件}} 。
db.movies.updateOne( { "title": "Blacksmith Scene" }, { $pull: { "languages": "English" } } // 刪除"English" )
需要注意的是,$pull會刪除所有匹配元素。如果有個數組arr是 [1, 1, 2, 1],$pull: {arr: 1},三個元素都會刪除,只剩下[2]一個元素。如果刪除的數組元素也是數組,就要使用$elementMatch,
數組元素的更新,可以使用元素的下標位置。
db.movies.updateOne( { "title": "In the Land of the Head Hunters" }, { $set: { "languages.0": "English"} } // 使用更新操作符,設置languages數組0下標位置為"English" )
但大多數情況下,并不會提前知道元素的位置,只能先查詢到元素,再更改元素,這時可以使用位置操作符$,$指的是數組中第一個符合篩選條件的數組元素的占位符,使用$時,updateOne的查詢條件中要有數組的篩選條件,并且,位置操作符$只能更新一條匹配元素
db.movies.updateOne( { "title": "In the Land of the Head Hunters", "languages": "English" // languages(查詢條件中要有數組的篩選條件) }, { $set: { "languages.$": "Russia" } } // languages.$ 如果languages數組中第1,2,3個元素都是English,那只有第一個元素被更新成Russia )
如果更新數組中所有匹配元素,使用$[] , $[]指代數組中匹配查詢條件的所有元素
db.movies.updateOne( { "title": "In the Land of the Head Hunters", "languages": "Russia" }, { $set: { "languages.$[]": "English" } } // languages.$[] 就是匹配篩選條件的所有數組元素 )
updateOne第三個參數arrayFilters,也可以更新數組元素的值。
db.movies.updateOne( { "title": "In the Land of the Head Hunters" }, { $set: { "languages.$[elem]": "Russia" } }, // 聲明了elem變量,表示匹配到每一個的數組元素,$set設置值。 { arrayFilters: [{ "elem": { $eq: "English" } }] }// 使用elem變量,定義篩選條件 )
數組中元素是對象(嵌套文檔類型)時,有些細節要注意一下。比如push的時候,$sort 不能是1或-1了,要指定按照哪個字段進行排序, 比如sort: {votes: 1}
// push 一個嵌入式文檔 db.movies.updateOne( { "title": "Traffic in Souls" }, { $push: { "comments":{ votes: 2, author: "Lily" } } }, ) // push多個嵌入式文檔,$sort不能是1或-1了,要指定按照哪個字段進行排序, 比如sort: {votes: 1} db.movies.updateOne( { "title": "Traffic in Souls" }, { $push: { "comments": { $each: [ { votes: 6, author: "John" }, { votes: 5, author: "Make" } ], $sort: { votes: 1 } // 指定按votes 進行排序 } } }, )
$pull刪除元素的時候,{$pull: {字段名: {值或 篩選條件}} , 這里的值或篩選條件,是對象類型,鍵為對象元素的屬性,值為值或篩選條件
db.movies.updateOne( { "title": "Traffic in Souls" }, { $pull: { "comments": { "votes": { $gt: 5 } } } // 數組元素對象的屬性,votes 大于5, // 也可以 $pull: { "comments": { "votes": 6 } } } )
更新數組元素的某個對象的值時
db.movies.updateOne( { "title": "Traffic in Souls" }, { $inc: { "comments.0.votes": 1 } // 數組下標后面可以加某個屬性,修改數組第幾個元素(對象)的哪個屬性 } ) db.movies.updateOne( { "title": "Traffic in Souls", "comments.author": "Lily" }, { $set: { "comments.$.author": "Lucy" } // 修改匹配到的數組元素的哪個屬性 } ) db.movies.updateOne( { "title": "Traffic in Souls" }, { $set: { "comments.$[elem].author": "LiLy" } // elem 指的是匹配的每一個對象 }, { arrayFilters: [{ "elem.votes": { $lt: 5 } }] } )
$elementMatch, 也要是個對象,比如 hobby: [{title: 'sport', frenquency: 3}]。使用$elementMatch 也要用對象,db.collection.find({hobby: {$elementMatch: {title: 'sports', frenquency: {$gt: 3}}}})
如果某個字段的值是對象類型或嵌入式文檔,更新該字段,也是使用更新操作符,只不過,更新操作符的值(對象)的鍵,使用的是屬性的方式
db.movies.updateOne( { "title": "Blacksmith Scene" }, { $set: { "awards.wins": 2 }} )
findOneAndUpdate() 返回更新之前的文檔,如查沒有查找到,返回null, 設置{"returnNewDocument" : true} 返回更新后的文檔
db.movies.findOneAndUpdate( { "title": "Blacksmith Scene" }, { $set: { "awards.text": "1 win." } } )
更新操作也有upsert,如果找到文檔就更新,如果沒有找到文檔就插入,第三個參數{upsert: true}
db.movies.updateOne( { "title": "Gertie the Dinosaur" }, { $set: { "awards.wins": 2 } }, { upsert: true } )
集合中沒有文檔的title 是"Gertie the Dinosaur”,所以就插入了一條新文檔。有時候,字段只需要在文檔創建之初設置值,在后續的更新中,值就不會發生變化,這時更新的時候要使用$setonInsert, 只有在插入文檔時才設置值,更新文檔,值不會發生變化。
// 第一次執行,插入文檔,創建createdAt db.movies.updateOne( { "title": "The Perils of Pauline" }, { $setOnInsert: { "createdAt": new Date() } }, { upsert: true} ) // 第二次執行,文檔已存在,不需要插入,什么都不會發生 db.movies.updateOne( { "title": "The Perils of Pauline" }, { $setOnInsert: { "createdAt": new Date() } }, { upsert: true} )
刪除文檔:deleteOne()和deleteMany()都接受一個查詢條件,只不過前者只刪除符合條件的一條文檔,后者刪除符合查詢條件所有文檔
db.movies.deleteOne({"title": "Yahya A"})
db.movies.deleteMany({"title": "The Great Train Robbery"})
如果deleteMany()的參數是{},沒有任何條件,則會刪除所有文檔,但集合仍然存在。如果清空整個集合,要用drop(), db.movies.drop(),集合都不存在了。
findOneAndDelete: 刪除符合條件的一條文檔,但會返回刪除掉的文檔,如果查詢條件匹配多條文檔,可以使用sort進行排序,刪除掉一條文檔,還可以使用投影,指定返回的字段。
db.movies.findOneAndDelete(
{"title": "Blacksmith Scene"},
{sort: {"_id" : -1}, projection: {"_id": 0, "title": 1}}
)
返回
{ title: 'Blacksmith Scene' }
索引
說到索引,可能會想到書本的索引,按章節排序,每章節下有大標題,小標題,每個標題的后面跟著頁數,表明具體的內容在那里。索引是獨立于書本內容的,目的是為了提高查詢效率。知道第幾章節,就能馬上查到多少頁,直接到那一頁查詢內容就可以了。數據庫的索引就像書本的索引,獨立于數據庫中的集合,是一個單獨的數據存儲結構,可以把它想像成一個map,鍵就是排序好的某個字段或某些字段,值就是相對應的具體的文檔內容的地址。索引就是排序和指針, 指向原來的整條文檔。當查詢文檔時,如果查詢字段有索引,先查索引,通過索引定位到原始的文檔。索引是排序好的,所以非常快。如果沒有索引,那就要整個集合中一條文檔一條文檔進行匹配,直到找到符合條件的文檔。假設插入10000條user數據
for (let i = 0; i < 10000; i++) {
db.users.insertOne(
{
"i": i,
"username": "user" + i,
"age": Math.floor(Math.random() * 120),
"created": new Date()
}
);
}
然后找出"username" 是 "user101"的文檔, 為了能夠查看執行的情況,可以使用explain
db.users.find({"username": "user101"}).explain("executionStats")
executionStats里面totalDocsExamined: 10000,表示為了找到符合條件的文檔,MongoDB查詢了10000條數據,也就是查詢了整個users集合。查詢username, 可以創建username的索引。創建索引是createIndex
db.users.createIndex({"username" : 1})
重新執行 db.users.find({"username": "user101"}).explain("executionStats"),結果是totalDocsExamined: 1,MongoDB只查詢了users集合中的1條數據,非常快。創建username的索引,就相當于
["user0"] -> 776 ["user1"] -> 768 ["user11"] -> 880 ["user12"] -> 881 ["user13"] -> 883 ... ["user20"] -> 890 ["user21"] -> 891 ... ["user34"] -> 1008 ["user35"] -> 1009
索引對字段的值進行了排序,然后指向一個記錄標識符(record identifier)。由于排序,索引中能快速找到username 等于”user101",進而得到一個記錄標識符,MongoDB的存儲引擎使用記錄標示符來定位集合中的文檔,進而找到這條文檔的在集合中的具體位置,MongoDB直接到集合中讀取這條文檔就可以了。當進行query操作時,mongodb會查找索引,和query 進行匹配,看能不用用索引,然后建立幾個查詢的plan,比如3個。然后再建立一個winning condition, 比如查找100條記錄,然后三個方法競爭去跑,看哪個先到達條件,哪個就是winning 的方法,Mongodb就會用這個方法執行真正的query,并把這個方法緩存起來。當collection 發生變化后,比如新增100條記錄,新建了索引,等,緩存的記錄會被清除
如果查詢條件是{"age": 20, "username": "user101"}呢? 就要對age和username兩個字段創建索引,這是復合索引。復合索引是對兩個或兩個以上的字段創建的索引
db.users.createIndex({"age" : 1, "username" : 1})
創建索引后,
[0, "user1000"] -> 8623513776 [0, "user1002"] -> 8599246768 [0, "user1003"] -> 8623560880 ... [0, "user10041"] -> 8623564208 [1, "user1001"] -> 8623525680 [1, "user1002"] -> 8623547056 [1, "user1005"] -> 8623581744 [2, "user1001"] -> 8623535664
涉及到復合索引,就要想想怎么設計復合索引。先看幾個query查詢,看看能不能用上復合索引{age: 1, username: 1}
db.users.find({"age" : 21}).sort({"username" : -1})
這是一個等式查詢(age等于21),并且只查詢單個值(21),age有索引,可以快速找到age等于21, 再看sort,按username進行排序,username在索引也排序好了,MongoDB可以直接跳轉到最后一個{age: 21},然后遍歷索引。
[21, "user1001"] -> 8623530928 [21, "user1002"] -> 8623545264 [21, "user1003"] -> 8623545776 [21, "user1004"] -> 8623547696 [21, "user1005"] -> 8623555888
排序的方向沒有關系,MongoDB可以從任意一個方向遍歷索引。再看第二種查詢
db.users.find({"age" : {"$gte" : 21, "$lte" : 30}})
這是范圍查詢,查找匹配多個值的文檔,由于age是索引,將返回如下
[21, "user100154"] -> 8623530928 [21, "user100266"] -> 8623545264 [21, "user100270"] -> 8623545776 ... [21, "user999390"] -> 8765250224 [21, "user999407"] -> 8765252400 [21, "user999600"] -> 8765277104 [22, "user100017"] -> 8623513392 ... [29, "user999861"] -> 8765310512
再看第三種
db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username" :1})
多值查詢帶排序,通過age索引能找快速找到age,但就像上面一樣,username 并沒有排序,需要重新排序,這種查詢就不是很有效率。 索引{age: 1, username: 1},只有在age相同的情況下,再按username進行排序,age不同,useraname之間沒有進行排序。怎樣設計組合索引
db.users.find({ age: { $gt: 50 }, username: "user100" })
.sort({ age: 1 })
當有單值查詢和多值查詢時,通常單值查詢返回的數據是少的,可以以它為基礎建立索引,再由于sort 和多值查詢都依賴age,可以給age建索引,
db.students.createIndex({username:1, age:1})
當設計復合索引時,需要平衡查詢中的多值查詢條件,單值查詢條件和排序條件。查詢語句再變一下,db.users.find({ age: { $gt: 50 }, username: "user100" }) .sort({ created: 1 }),這會導致MongoDB 按created進行排序。如果能用索引避免排序,就盡量避免排序,但需要做一個權衡,為了避免排序,需要對比更多的age字段來查找滿足條件的文檔。
db.users.createIndex({username:1, created:1, age:1})
等值過濾條件放到第一位,排序的過濾條件放到第二位,最后是多值過濾條件。 等值過濾條件在前面,可以最大程序縮小范圍。當sort 排序的時候,如果有多個條件,比如,.sort({age: 1, username: -1}), 這時創建索引的時候,要注意方向。
隱式索引:
當創建{username:1, created:1, age:1} 復合索引時,先對username 進行排序,如果username相等,再對created進行排序,created再相等,要按age進行排序,就相當于創建了 {username: 1} ,{username: 1, created: 1} 和{username:1, created:1, age:1}索引
涵蓋索引
當創建索引后,如果只查詢和顯示索引中包含的字段,MongoDB 就沒有必要再到集合中獲取額外的字段,直接使用索引中的數據就可以了。默認情況下,MongoDB的查詢都會返回_id, 如果沒有給_id 創建索引,就要使用投影去掉_id
db.users.find({username: "user100" }, {_id: 0, username: 1}).explain("executionStats")
totalDocExamined 是0,MongoDB直接從索引中讀取數據,沒有查詢集合。
唯一索引
用于創建索引的字段的值,在集合中的所有文檔中都不相同,字段不會出現重復值。
db.users.createIndex({"username" : 1}, {"unique" : true} )
部分索引
想為某個字段創建索引,但文檔中可能存在,也可能不存在該字段,那就要使用部分索引,比如僅對存在的字段創建唯一索引。創建索引時,加partialFilterExpression
db.movies.ensureIndex({"scoring":1},{"partialFilterExpression": {"scoring": {$exists: true}}, "unique": true})
其它索引
MongoDB默認會為 _id字段創建索引,對數組字段創建索引,也稱為多鍵索引, 因為MongoDB會為數組中的每一個元素創建索引。嵌入式文檔創建索引,就是屬性方式,db.movies.createIndex({"awards.wins":1})
索引的操作
getIndexes() 查看集合中存在的索引,db.movies.getIndexes() 。dropIndex()刪除索引,參數是索引的名稱或創建索引的方式,怎么創建的,怎么刪除,比如db.movies.dropIndex("scoring_1") 或db.movies.dropIndex({'awards.wins': 1})
但是索引也有代價,插入,更新和刪除索引字段時,需要更長的時間。因為MongoDB不僅需要更新集合文檔,還需要更新索引,所以創建索引,最關鍵的是為哪個字段創建索引。
聚合框架
MongoDB的聚合框架是基于流水(pipeline)的概念。流水(pipeline)的概念和現實中的流水線沒有什么區別。在流水線上,第一個工位接收原材料,做完后交給下一個工位,每一個工位做一個工作,做完之后,交給下一個工位,最后產出一個產品。對于MongoDB來說,每一個工位相當于一個stage,當文檔經過stage時,對文檔進行數據處理,上一個stage的輸出文檔,變成下一個stage的輸入,最后產出想要的文檔集合。所以pipeline是一個數組,包含多個stage,stage是通過提供的操作符來進行數據處理,$match 過濾條件。$project需要什么字段(reshape the documents), $limit限制返回的數據條數,$sort 排序,$skip跳過。聚合框架,相比find, 在每一個階段,能提供更多的控制。Simply put, aggregation is a method for taking a collection and, in a procedural way, filtering, transforming, and joining data from other collections to create new, meaningful datasets.

stage對數據的處理是基于流的方式,It takes in a stream of input documents one at a time, processes each document one at a time, and produces an output stream of documents one at a time

var pipeline = [ {$stage-operator : parameters} { . . . }, {....}, {....} ] var cursor = db.[集合].aggregate(pipeline, options) // 返回 a cursor containing the search result.
使用聚合框架,需要大量的數據,為此MongoDB的官網提供了一些樣例數據(sample_airbnb等),可以導入到本地的數據庫學習使用。在MongoDB Compass 左下方顯示數據庫的基本信息,點擊mflix,點擊movies, 右側顯示

點擊 ADD Data,選Import File,再點擊Select a file, 選擇movies.json, 再點擊右下角的Import,就可以導入成功。同樣的方法也可以導入comments, users等數據
用聚合框架來實現find功能,點開__MONGOSH,use mflix,
db.movies.aggregate([ { $match: { year: 1915} }, { $sort: { runtime: -1 } }, { $skip: 2 }, { $limit: 5 }, { $project: { _id: 0, title: 1 } }, ])
$match,或者說,aggregation的第一步可以使用索引。聚合框架可以實現更高級的功能,先看$project,它可以reshape 形狀,
db.movies.aggregate([ { $match: { year: 1915} }, { $project: { _id: 0, title: 1, imdb_rating: "$imdb.rating" // $加上原文檔中的字段,稱為字段路徑(field paths),取原文檔中該字段的值。 } }, ])
對數組也可以使用$elementMatch, 還可以使用表達,$project: {fullName: {$concat: [{$toUpper: "$name.firstName"}, " ", {$toUpper: "$name.lastName"}]}}
$group:以某個條件對集合進行分組或聚合。分組和現實中的分組沒有區別,比如一群學生按班級進行分組,一班,二班等。分組之后,只知道這組是幾班,而不知道每個組員具體的信息,比如,姓名和年齡。所以對于MongoDB來說,每一組都形成一條單獨的文檔,分組就要形成新的_id, 按什么條件分組,_id就是什么,比如{$group: {_id: "$rated"}} 就是以電影評分進行分組,新生成的文檔的_id 就是評分的值。$rated告訴MongoDB使用文檔中rated字段的值。

那分組有什么用呢? 可以用來統計信息,比如每個組多少個人?平均分多少? 這些信息就是$group對象的其它字段,要想獲取這些數據,就要用累加器表達式,格式:field: { accumulator: expression}。field:給想要的信息起個名字,因為它是計算出來的,原文檔中沒有。accumulator 就是MongoDB支持的操作,express 就是accumulator的輸入,對什么進行accumulate
db.movies.aggregate( [ { $group: { _id: "$rated", "numTitles": { $sum: 1 }, } } ])
$group 的_id 可以接受一個對象,按照多個條件進行group。
$lookup: 多集合查詢,比如找出一個movie 的comments。movie在一個集合中,comments 在另一個集合中,所以要兩個集合一起查詢,關聯
db.movies.aggregate([ { $match: { title: "The Saboteurs" } }, { $lookup: { from: "comments", localField: "_id", foreignField: "movie_id", as: "comments" } }, { $project: { title: 1, comments: 1 } } ])
$lookup 的四個字段
- from: 關聯哪一個集合進行查詢
- localField: 用哪個字段去關聯,
- foreignField: 被關聯的集合中,用哪個字段和關聯字段進行匹配
- as: 關聯查詢完成后,給查詢到的數據起個名字
$lookup 關聯查詢,查到的匹配文檔會封裝到數組中,沒有查到文檔,就是空數組,然后嵌入到主查詢文檔中,comments 是一個數組,嵌入到 { $match: { title: "The Saboteurs" } } 查詢到的文檔中。
{ _id: ObjectId("573a13e9f29313caabdcc269"),
title: 'The Saboteurs',
comments:
[ { _id: ObjectId("5a9427658b0beebeb697b894"),
name: 'Khal Drogo',
email: 'jason_momoa@gameofthron.es',
movie_id: ObjectId("573a13e9f29313caabdcc269"),
text: 'Qui placeat magni quisquam reiciendis. Quo dolores quidem quam odit sunt. Dolores nam temporibus consequatur voluptates iste illo voluptatem. Facilis asperiores sequi corrupti quam.',
date: 1988-12-27T23:28:01.000Z },
{ _id: ObjectId("5a9427658b0beebeb697b897"),
name: 'Rast',
email: 'luke_barnes@gameofthron.es',
movie_id: ObjectId("573a13e9f29313caabdcc269"),
text: 'Recusandae placeat tempore occaecati magni velit eveniet ipsam. Velit enim voluptates nobis ipsa dignissimos non.',
date: 2016-08-21T16:26:52.000Z },
{ _id: ObjectId("5a9427658b0beebeb697b89a"),
name: 'Victor Patel',
email: 'victor_patel@fakegmail.com',
movie_id: ObjectId("573a13e9f29313caabdcc269"),
text: 'Optio accusamus similique tempore praesentium aliquid repellat. Illum exercitationem quae rem est. Quas fuga magnam aspernatur cupiditate quos.',
date: 2013-04-02T22:36:11.000Z } ] }
localField使用的字段,還可以是個數組字段。比如 book:{authors: [1,2,3]},就可以用authors字段去關聯author表。$lookup: {from: 'author', localField: "authors", foreignField: _id, as: "creator"}
$unwind :將文檔中的某一個數組類型字段進行展開, 數組中有幾個元素,就會生成幾條文檔,每一條文檔都包含數組的一個元素 ,所以它的參數是要拆分字段的路徑。{a: 1, b: 2, c: [1, 2, 3, 4]} 對c進行unwind
{"a" : 1, "b" : 2, "c" : 1 }
{"a" : 1, "b" : 2, "c" : 2 }
{"a" : 1, "b" : 2, "c" : 3 }
{"a" : 1, "b" : 2, "c" : 4 }

db.movies.aggregate([
{ $match: { title: "The Saboteurs" } },
{
$lookup: {
from: "comments",
localField: "_id",
foreignField: "movie_id",
as: "comments"
}
},
{ $unwind: "$comments"},
{
$project: {
title: 1,
"comments.text": 1 // 展開數組后,可以對每一個文檔進行單獨操作
}
}
])
$lookup 還有第二種使用方式:在lookup中創建新的pipeline
{ $lookup: { from: "comments", let: { movieId: "$_id" }, // 可以聲明多個變量, 用,隔開 pipeline: [{ $match: { $expr: { $eq: ["$movie_id", "$$movieId"] } } }], as: "comments" }, },
由于在新的pipeline中不能使用上一個stage傳下來的文檔,在這里,就是 { $match: { title: "The Saboteurs" } } 傳下來的文檔,所以要使用let聲明變量,變量的值就是傳下來的文檔中的值。 let: { movieId: "$_id" } 就是聲明了movieId變量,它的值是傳下來的文檔的_id字段的值。新的pipeline中,每一個stage都要用$expr來表達關聯關系,$expr中如果要使用let中聲明的變量,用$$變量名,如果要使用from集合中的某個字段的值,要用$字段名。$eq: ["$movie_id", "$$movieId"],表示 from集合(comments)中movie_id字段的值等于let中聲明的變量movieId的值,也是集合movies中_id的值。
創建companies集合,考慮字段數組中還包含數組的情況:
db.companies.insertOne({ "name": "Facebook", "category_code": "social", "founded_year": 2004, "funding_rounds": [ { "id": 4, "round_code": "b", "raised_amount": 27500000, "raised_currency_code": "USD", "funded_year": 2006, "investments": [ { "company": null, "financial_org": { "name": "Greylock Partners", "permalink": "greylock" }, "person": null }, { "company": null, "financial_org": { "name": "Meritech Capital Partners", "permalink": "meritech-capital-partners" }, "person": null } ] }, { "id": 2197, "round_code": "c", "raised_amount": 15000000, "raised_currency_code": "USD", "funded_year": 2008, "investments": [ { "company": null, "financial_org": { "name": "European Founders Fund", "permalink": "european-founders-fund" }, "person": null } ] } ], "ipo": { "valuation_amount": 104000000000, "valuation_currency_code": "USD", "pub_year": 2012, "stock_symbol": "NASDAQ:FB" } })
funding_rounds字段的值是數組,它還包含investments數組。當數組的元素是對象時,用字段名.對象屬性名的方式,數組中包含數組,就要繼續 .對象屬性名 進行查詢
db.companies.aggregate([ { $match: { "funding_rounds.investments.financial_org.permalink": "greylock" } }, { $project: { _id: 0, name: 1, ipo: "$ipo.pub_year", valuation: "$ipo.valuation_amount", funding_rounds: 1 } } ])
整個funding_rounds數組的內容全都返回回來。如果進行project,取出funding_rounds中需要的信息,比如permalink。把上面的funding_rounds: 1改成funders: "$funding_rounds.investments.financial_org.permalink" 進行查詢,返回的結果
{ name: 'Facebook', ipo: 2012, valuation: 104000000000, funders: [['greylock', 'meritech-capital-partners'], ['european-founders-fund']] }
funders是個二維數組,funding_rounds是個數組,investments是個數組,格式沒有變化,只是取出了financial_org.permalink的值,相當于map方法。這時可以想到對funding_rounds進行$unwind,funders變成了一維數組。
db.companies.aggregate([ { $match: { "funding_rounds.investments.financial_org.permalink": "greylock" } }, { $unwind: "$funding_rounds" }, { $project: { _id: 0, name: 1, funders: "$funding_rounds.investments.financial_org.permalink" } } ])
db.companies.aggregate([ { $match: { "funding_rounds.investments.financial_org.permalink": "greylock" } }, { $unwind: "$funding_rounds" }, { $match: { "funding_rounds.investments.financial_org.permalink": "greylock" } }, { $project: { _id: 0, name: 1, funders: "$funding_rounds.investments.financial_org.permalink" } } ])
如果上一個stage傳遞給$project的文檔中有數組字段時,比如funding_rounds,可以對數組字段進 $filter, $slice等操作,還可以使用 $max, $min, $sum, $avg
db.companies.aggregate([ { $match: { "funding_rounds.investments.financial_org.permalink": "greylock" } }, { $project: { _id: 0, name: 1, rounds: { $filter: { // $filter 對數據時行過濾 input: "$funding_rounds", as: "round", // 給$funding_rounds定義個別名,供下面的條件表達式使用。下面的$$round就用引用的round變量,也就是文檔中的funding_rounds。 cond: { $gte: ["$$round.raised_amount", 100000000] } } }, first_round: { $arrayElemAt: ["$funding_rounds", 0] }, // 數組第一個元素 last_round: { $arrayElemAt: ["$funding_rounds", -1] }, // 數組第三個元素 early_rounds: { $slice: ["$funding_rounds", 1, 3] }, // 取數組前三個元素 total_rounds: { $size: "$funding_rounds" }, // 數組的大小 total_funding: { $sum: "$funding_rounds.raised_amount" }, largest_round: { $max: "$funding_rounds.raised_amount" } } }, ])
模型設計
數據模型就是關注哪些信息,怎么表現出來?因為同一個對象,對于不同的應用程序,關注的信息可能不同,比如同一個人來說,保險公司關注的是年收入,理發店關心的是個性和喜好。模型設計就是根據需求,把關注的信息列出來,就是確定對象(實體)及其屬性。通常實體和實體存在各種各樣的關系,比如一個人有多個電話號碼,就是1對多的關系,還有1對1,多對多的關系。怎么表現出來?因為MongoDB存儲的是JSON,對象和屬性就用JSON對象表式,那關系怎么表示?通常情況下,1對1,以內嵌為主(嵌入式文檔),比如學生有學生卡,每一個學生卡都對應一個學生,把學生卡的信息內嵌到學生信息中,
{ "_id": ObjectId("62bc"), "first_name": "Sammy", "last_name": "shark", "id_card": { "number": "123-1234-123", "issued_on": "2020-01-23" } }
但對于某些不被經常查詢,但又特別大的屬性,比如二進制的頭像,查詢時整條文檔查詢出來,太占內存,可以把頭像放到單獨的集合中,用id引用。1對多時,考慮這個多有多大,10?20?再考慮independent acess, 多的元素是不是需要單獨獲取,比如學生的郵箱,它不會太多,并且也不會單獨獲取,這時就可以使用內嵌數組文檔,將需要一起訪問的內容存儲在一起
{ "_id": ObjectId("62bc"), "first_name": "Sammy", "last_name": "shark", "emails": [ { "email": "sammy@outlook.com", "type": "work" }, { "email": "sammy@gmail.com", "type": "home" } ] }
但多方的數據量比較大,或者多方的元素需要單獨查詢(獨立更改),比如學生的課程,這是就要把多的一方,放到一個單獨的集合中,可以在多的一方加one 的id,類似外鍵,也可以在1的一方,使用內嵌數組,數組元素都是引用進行關聯,這也稱為子引用。多對多,也可以使用內嵌數組文檔的方式,但通常使用子引用的方式。比如課程的集合
{ "_id": 1, "name": "python" } { "_id": 2, "name": "java" }
學生文檔
{ "_id": ObjectId("62bc"), "first_name": "Sammy", "last_name": "shark", "courses": [1,2] }
` 1對多有一個特殊的情況,那就是多方的數量有可能持續增長(隨著時間的推移),沒有封頂,比如學生的留言板,這就需要父引用,在多的一方的元素上,加引用,比如在流量文檔中,加學生文檔的引用。子引用始終數量有上限。
{ "_id": 3, "subject": "books", "message": "Hello, reconmmend a good inroductory books", "post_by": ObjectId("62bc") }
對于某些數據,有特定的設計模式可以參考。比如一分鐘1條數據的,

可以1小時一條數據,把一分鐘一條的數據放到1小時數據的數組中。

可以減少文檔數量, 減少索引占據的空間。再比如一些字段非常多的文檔,

可以把相似的列,放到一個數組中,稱為列轉行

靈活模型的文檔不同的版本管理,可以加一個version字段

復制集
單臺服務器容易出故障,那就多臺服務器組合在一起,每臺服務器都運行一個mongod進程,形成一個集群,對外提供服務。多臺服務器中,只有一臺是主服務器(主節點), 負責讀寫操作,其它都是副或從服務器(副節點),負責不停地從主節點進行備份和復制數據。各個節點之間相互通信,最重要的是心跳請求,進行健康檢測。默認情況下,每隔2秒發送一次,超過10s,就算請求超時,某個節點出故障。如果主節點出故障,副節點中,可以選出一個主節點,繼續提供讀寫服務,最多50個節點組合在一起,但具有投票權的節點最多7個,這就是MongoDB的復制集。
主副節點之間的數據同步依靠的是Oplog (Operation Log), 它是一個固定集合(Capped Collections)或環形隊列(circular buffer),存在主節點的local數據庫中。主節點上的寫操作完成后,會按照一定的格式,向Oplog寫入一條記錄(寫庫記錄),副節點不斷的從主節點獲取新的記錄并應用自己身上,這樣主副節點的數據就會保持一致。Oplog數據不斷增加,當容量達到配置上限時,從頭開始寫起。寫庫記錄在副節點上可以重復應用,即重復應用也會得到相同的結果。看一下三個節點的復制集

主節點把自己的接收并執行的寫記錄按照順序依次寫到Oplog中,副節點發送query請求到主節點,查詢Oplog中有沒有新的記錄,如果有,Oplog 中的記錄就會被復到到副節點,副節點也有自己的Oplog,記錄從主節點復制過來的記錄,然后,把這些記錄應用到自己身上,再把自己執行的記錄保存到自己的Oplog中,如果執行記錄失敗,副節點可能就不提供服務了。通常副節點是從主節點復制數據,但有時,副節點也會從另外一個副節點復制數據,這種類型的copy稱為鏈式復制(Chained Replication)。
復制集的主節點是通過選舉算法推選出來的,如果主節點出問題,則會再重新選舉一個主節點出來。在復制集的每一個節點中,都有選舉計算數,參與一個次選舉,選舉計數器加1。任意一個副節點,都會發起一次新的選舉。當然,實際上,每一個節點都有不同的優先級,優先級高的節點會優先發起選舉。發起選舉的主要目的是,詢問其它所有節點,是不是同意它成為新的主節點。發起選舉的過程是,它自己的選舉計數器加1,給自己投1票 ,然后向其它節點發起投票請求,收到投票請求的節點,先把自己的計數機加1, 再比較它和發起投票請求的節點的同步優先級(對比操作日志oplog),如果比它低,就會同意,如果比它高,就會反對。如果發起投票的節點,得到同意的票數超過復制集中節點個數的一半,它就會成了新的主節點。如果主節點再壞了,那就再發起投票。所以復制集的最小節點是3個,因為投票數要超過一半。超過復制集的成員的一半,稱為“大多數”。這里的復制集成員是復制集初始配置中定義的。如果復制集的成員為N,大多數為(N/2 + 1)

當一個副節點聯系不上主節點后,它就會向復制集的其他成員發起請求,它要當主節點。其它成員就會做一些考慮,它們能不能聯系上主節點?發起請求的節點的數據是不是最新的?復制集中是不是有其它優先級更高的節點?等。如果有任何一種情況,其它節點不會為該節點進行投票。如果沒有什么理由,其它節點會對它進行投票。如果發起請求的節點得到了大多數的投票,它就成了主節點,如果沒有,它還是副節點。
創建復制集。1, 創建3個數據文件夾(mkdir -p ~/data/rs{1,2,3})2,啟動3個mongo的實例,
sudomongod --replSet mdbDefGuide --dbpath ~/data/rs1 --port 27017sudomongod --replSet mdbDefGuide --dbpath ~/data/rs2 --port 27018sudomongod --replSet mdbDefGuide --dbpath ~/data/rs3 --port 27019
3, 連接一個mongod 實例。MongoDB 7 沒有了mongo 命令,需要下載新的mongosh,它是一個.deb的文件,雙擊安裝。安裝完成后,mongosh --port 27017,連接到27017 服務器,執行
rs.initiate({ _id: "mdbDefGuide", // _id: 命令行中 --replSet mdbDefGuide, 就是復制集的名字 。 members: [ {_id: 0, host: 'localhost:27017'}, {_id: 1, host: 'localhost:27018'}, {_id: 2, host: 'localhost:27019'} ] })
rs.status() 檢查復制集的狀態。如果27017 被選為了主節點,那么shell 就會顯示 mdbDefguide: primary. 如果27018當選主節點,需要退出shell,重新連接到主節點( mongosh --port 27018)。 隨便在主節點上添加數據,for (i=0; i<10; i++) {db.coll.insertOne({count: i})}, 然后,打開一個新的命令窗口,mongosh --port 27019,連接一個副節點,db.coll.find() 可以找到主節點寫入的數據。MongoDB 7,副節點可以直接讀取數據了,不用調setSlaveOk()了。
停止主節點,db.adminCommand({"shutdown" : 1})。 然后再rs.status,可以發現新的主節點選舉出來了。添加新節點:rs.add("localhost:27020"), 刪除新節點rs.remove("localhost:27017"). rs.config() 可以查看配置。
創建集群后,連接方式有變化,連接的url是服務器列表,比如"mongodb://localhost:27017, localhost:27018, localhost:27019, 建議多寫幾個,如果只寫一個,鏈接不上這一個,復制集就不能用了。
MongoDB事務
首先需要說明,MongoDB的事務只有在復制集中才能使用。事務的ACID是通過配置writeConcern和readConcern來實現的,一致性強調的是 各個節點之間的一致性。
writeConcern 決定一個寫操作落到多少個節點才算成功, 0表示不關心是否成功,1-n的數字,寫操作需要被復制到指定節點數才算成功,marjority: 寫操作要被復制到大多數節點才算成功。發起寫操作的程序將被阻塞到寫操作到達指定的節點數為止。默認情況下,寫操作,寫到主節點的內存中就算成功,。writeconcern:{w: “majority“},3節點的復制集,只有寫操作,寫到兩個節點,一個主節點,一個副節點才算成功。writeConcern 還有一個j是不是寫到journal文件才算成功, true表示是,false表示不是,只要寫到內存就算成功。設置timeout, 如果節點不響應,最多等多少時間,就算失敗了, 不是所有的節點都失敗,有的節點寫入成功,有的節點寫入失敗,這種error 最好記錄一下日志。
readConcern關注什么樣的數據可以讀,關注數據的隔離性,readConcern: available: 讀取所有可用的數據,local:讀取所有可用且屬于當前分片的數據,是默認值。majority: 讀取在大多數節點提交完成的數據,linearizable:可線性化讀取文檔,snapshot: 讀取最近快照的數據,類似關系型數據庫的可重復讀。 在復制集中,available和local 沒有區別,鏈接到哪個節點就從哪個節點讀取。majority 是鏈接到哪個節點就讀哪個節點,但是這個被鏈接到的節點要被其他節點通知,它才能知道,這個數據是不是在大多數節點上都提交完成。
readConcern(majority)和臟讀: 寫操作到達大多數節點之前都不是安全的,一旦主節點崩潰,而從節點還沒有復制到該次操作,剛才的寫操作就丟失了;把一次寫操作看作一個事務,從事務的角度看,可以認為事務被回滾了。所以從分布系統的角度來看,事務的提交被提升到了分布式集群的多個節點級別的提交,而不是單個節點的提交。如果在一次寫操作到達大多數節點前讀取了這個操作,然后因為系統故障該操作回滾了,則發生了臟讀。readConcern: majority 則有效避免臟讀。相當于讀已提交。
readConcern(snapshot): 事務中的讀,不出現臟讀,不出現不可重復讀,不出現幻讀,因為所有的讀都將使用同一個快照,直到事務提交為止,該快照才被釋放。實現可重復的的事務

readConcern和writeConcern除了配合事務外,還可以單獨使用,比如向主節點寫入一條數據,立即從從節點讀取這條數據,如何保證自己能夠讀取到剛剛寫入的數據
db.orders.insert({_id: 101, sku: "kit", q: 1}, {writeConcern: {w: 'majority' }})
db.orders.find({_id: '101'}).redPref("secondary").readConcern('majority')
redpref(readPreference)表示從哪里讀,主節點還是副節點。
分片
分片解決水平擴展問題,一個集合中的數據太大,在一個服務器上放不下,要放到多個服務器上。把數據分成一個一個的子集,把每一個子集存在一個服務器(分片)上,所有的分片合在一起,形成一個分片集群,每一個分片必須是復制集。一個完整的分片集群中除了分片之外,還有一個配置服務器和一個或多個mongos(路由,從配置服務器獲到信信息,查找哪個分片)。配置服務器也要是復制集,它存儲整個分片集群的元信息和配置信息。mongos 提供客戶端和數據庫之間的接口,隱藏了分片集群的實現細節,查詢分片集群和查詢沒有分片的集合使用的方法是一樣的。

MongoDB 腳本:mongo shell 是JS 解釋器,我們可以把要執行的mongo shell的命令寫到一個JS文件中,然后,用mongosh <script_name>.js 這個JS文件也稱為mongodb的腳本。 但在JS的文件中,并不能獲取到mongo shell 的全局變量,比如,db, show dbs, 不過,有對應的JS 函數

默認情況下,在哪個文件夾下啟動的mongosh, mongosh就在哪個文件夾下面查找腳本文件。在Linux下,打開終端窗口就是用戶目錄,在下面建一個mong.js文件,內容是
console.log(db.adminCommand('listDatabases'))
然后mongosh mong.js 就會執行腳本文件。

Audit log 和log是兩個不同的概念, 日志記錄是指在程序執行過程中發生的程序層面的事件,包含對開發、錯誤識別和修復有用的信息。日志記錄包含對開發人員有用的詳細程度的信息( Logging includes information about what happened in the level of detail that is useful for the developers.) 。Audit log 是業務層面的事件,指的是用戶執行的操作,Auditing answers the question of “Who did What and When?” in the system. We might also want to answer the question of “Why?” in the case of a root cause investigation. 下面是一個audit log


浙公網安備 33010602011771號