MongoDB查詢與游標詳解
游標定義:是一種能從數據記錄的結果集中每次提取一條記錄的機制
游標作用:可以隨意控制最終結果集的返回,如限制返回數量、跳過記錄、按字段排序、設置游標超時等。
MongoDB中的游標
對於MongoDB每個查詢默認返回一個游標,游標包括定義、打開、讀取、關閉。
MongoDB游標生命周期
游標聲明 var cursor=db.collection.find({xxx}) (MongoDB單條記錄的最大大小是16M)
打開游標 cursor.hasNext() 游標是否已經迭代到了最后(正在訪問數據庫)
讀取游標 cursor.Next() 獲取游標下一個文檔(正在訪問數據庫)
關閉游標 cursor.close() 通常迭代完畢會自動關閉,也可以顯示關閉
MongoDB游標常見方法
cursor.batchSize(size) 指定游標從數據庫每次批量獲取文檔的個數限制
cursor.count() 統計游標中記錄總數
cursor.explain(verbosity) 輸出對應的執行計划
cursor.forEach() 采用js函數forEach對每一行進行迭代
cursor.hasNext() 判斷游標記錄是否已經迭代完畢
cursor.hint(index) 認為強制指定優化器的索引選擇
cursor.limit() 指定游標返回的最大記錄數
cursor.maxTimeMS(time) 指定游標兩次getmore間隔的最大處理時間(毫秒)推薦
cursor.next() 返回游標下一條記錄
cursor.noCursorTimeout() 強制不自動對空閑游標進行超時時間計算(默認10分鍾)慎用
MongoDB shell下游標示例 1
隱式游標
db.t2.find() // 默認迭代20次,其后采用it手動迭代
DBQuery.shellBatchSize = 10 調整游標每批次返回的記錄數
顯示游標
var cursor = db.test.find() 游標定義,此時不會正在訪問數據庫
while (cursor.hasNext()){
printjson(cursor.next())}
或
db.test.find().forEach(function(e){printjson(e)}) 匿名游標迭代
MongoDB shell下游標示例 2
游標方法hint()
db.person.find({age:1,name:"andy"}).explain()
db.person.find({age:1,name:"andy"}).hint("age_1_name_1").explain()
游標方法maxTimeMS(time)
DBQuery.shellBatchSize = 100000
db.t2.find({type:3}).maxTimeMS(1)
修改游標超時時間
db.adminCommand( { setParameter: 1, cursorTimeoutMillis: 300000 } )
MongoDB游標最佳實踐
游標超時時間與batchsize大小需計算好,避免在getmore時發生不必要的超時
如果業務只需要第一批迭代則查詢時可指定singleBatch:True及時關閉游標
MongoDB游標狀態信息
db.serverStatus().metrics.cursor
MongoDB索引原理及優化
MongoDB索引原理![]()
db.index.find({}).showRecordId()
可以理解為每條記錄對應的映射地址
MongoDB索引類型:

db.test.createlndex({socre:1},{background:true,name:"xx_index"})默認創建索引加庫級別排它鎖,可指定 background為true避免阻塞,默認索引名詞:字段名_1 /_-1使用background:true構建索引過程依然會阻塞(同DB下)db.collection.drop(),repairDatabase等命令
可執行db.currentOp()查看索引構建進度也可使用db.killOp()強制中斷索引創建。
當副本集/分片節點索引創建被強制中斷后可通過指定indexBuildRetry參數控制節點重啟后是否自動重建索引,
同時只有當索引構建完畢后才能被對應的查詢語句利用
對單列索引而言,升序和降序都能夠用到索引db.records.find({score:2})db.records.find({ score : {$gt :10}})
嵌套單列索引:
db.test.find()
{"_id": xxxxxxx,"score:100", "location":{contry:"china",city:beijing}}
在嵌入式文檔上創建索引
db.test.createIndex({"location":1},{background:true})
如下查詢會被用到
db.test.find({location:{contry:"china",city:"beijing"}})
在嵌入式字段上創建索引
db.test.createIndex({"location.city":1},{background:true})
如下查詢會被用到
db.test.find({"location.city":"beijing"})
db.test.find({"location.contry":"chna","location.city":"beijing"})
在嵌入式文檔上執行相應匹配命令時,字段順序和嵌入式文檔必須完全匹配
多列索引
db.test2.createIndex(
{"userid" : 1, "score" : -1, " age " :1},{background:true})
索引排序可以簡記為:單列索引正反向排序都不受影響,多列索引則是乘以(-1)的排序可以使用相同的索引,即1,1和-1,-1可以使用相同的索引, -1,1和1,-1可以使用相同的索引
最左前綴原則
for (var i = 0 ;i<500000;i++){ db.test10.insert({userid:Math.round(Math.random()*40000),score:Math.round(Math.random()*100),age:Math.round(Math.random()*125)});}
創建索引:db.test10.createindex({"userid":1,"score":-1,"age":1},{background:true})
以下查詢可以使用到索引
db.test10.find({userid:1,score:83,age:20});
db.test10.find({score:83,userid:1});
db.test10.find({age:20,score:83,userid:1});
db.test10.find({age:20,userid:1});
以下查詢不能使用到索引
db.test10.find({score:83,age:20});
db.test10.find({score:83});
db.test10.find({age:20});
db.test10.find({age:20,score:83});
強制索引
db.test.find({userid:28440,score:88,age:118}).hint("userid_1_score_-1_age_1") ;
索引交集
普通索引跟hash索引的組合
db.test.createIndex({age:1});
db.test.createIndex({age:"hashed"});
db.test.createIndex({score:1});
db.test.createIndex({score:"hashed"});
db.test.find({score:88,age:118}).explain()
是否采用索引交集的標志:
通過explain()查看執行計划的時候會出現AND_SORTED 或 AND_HASG階段(stage)
or與索引db.test.find( { $or: [ { score: 88 }, { age: 8 } ] } ).explain()索引覆蓋db.test.find({userid:681,score:33},{_id:0,age:1}).explain(1)多列索引與排序db.test.find({userid:1,score:{$gt:25}},{_id:0}).sort({userid:1}); //索引可以優化排序db.test.find({userid:1,score:{$gt:25}},{_id:0}).sort({score:-1}); //索引可以優化排序db.test.find({userid:1,score:{$gt:25}},{_id:0}).sort({score:1}); //索引可以優化排序db.test.find({userid:1,score:{$gt:25}},{_id:0}).sort({age:1}); //索引無法優化排序db.test.find({userid:1,score:{$gt:25}},{_id:0}).sort({age:-1});//索引無法優化排序db.test.find({userid:1,score:{$gt:25}},{_id:0}).sort({userid:1,score:-1}); // 可以優化排序db.test.find({userid:1,score:{$gt:25}},{_id:0}).sort({userid:1,score:1}); //不可以優化排序db.test.find({userid:1,score:{$gt:25}},{_id:0}).sort({userid:1,score:-1,age:1}); //索引可以優化排序db.test.find({userid:1,score:{$gt:25}},{_id:0}).sort({userid:1,score:-1,age:-1}); //索引無法優化排序db.test.find({userid:1,score:53,age:{$gt:29}}).sort({userid:1,score:-1,age:1}); //索引可以優化排序db.test.find({userid:1,score:53,age:{$gt:29}}).sort({userid:-1,score:1,age:-1}); //索引可以優化排序db.test.find({userid:1,score:53,age:{$gt:29}}).sort({userid:1,score:-1,age:-1});//索引無法優化排序
多鍵索引
要索引一個包含數組值的字段,MongoDB會為數組中的每個元素創建一個索引鍵,這些多鍵索引支持針對數組字段的高效查詢
多鍵索引可以在包含標量值(例如字符串、數字)和嵌套文檔的數組上創建
db.multi_key.createIndex({ratings:1},{background:true})
db.multi_key.insert({ _id: 5, type: "food", item: "aaa", ratings: [ 5, 8, 9 ] })
db.multi_key.insert({ _id: 6, type: "food", item: "bbb", ratings: [ 5, 9 ] })
db.multi_key.insert({ _id: 7, type: "food", item: "ccc", ratings: [ 9, 5, 8 ] })
db.multi_key.insert({ _id: 8, type: "food", item: "ddd", ratings: [ 9, 5 ] })
db.multi_key.insert({ _id: 9, type: "food", item: "eee", ratings: [ 5, 9, 5 ] })
限制:
不許創建兩個數組組合索引
不允許創建hash多key索引
不能指定為shard key索引
文本索引
一個集合最多只能創建一個文本索引
db.reviews.createlndex({ comment: "text" })
可以對多個字段創建文本索引
db.reviews.createlndex({subject:"text", comment: "text" })
2dsphere索引
2d索引
Hash索引
哈希索引通過索引字段的哈希值來維護條目
使用哈希分片鍵對集合進行分片會導致數據分布更加隨機和均勻
哈希索引使用散列函數來計算索引字段的哈希值。哈希索引不支持多鍵(即數組)索引
創建哈希索引:
db.coll.createindex({_id:"hashed"})
不可創建具有哈希索引字段的復合索引,或者對哈希索引指定唯一約束。但是,可以在一個字段上同時創建哈希索引和升序/降序(即非哈希)索引
索引屬性
唯一索引
強制索引字段不含有重復值,默認情況下MongoDB會為_id字段創建唯一性索引
關鍵字:{unique:true}
唯一性索引創建:
db.unique1.createIndex({"user_id":1},{unique:true})
db.unique2.createIndex({"name":1,"birthday":1},{unique:true})
限制:
如果集合已經包含違反索引唯一約束的數據,則MongoDB無法再指定的索引字段上創建唯一索引
不能在Hash索引上指定唯一約束
唯一索引對缺失列的處理
如果文檔在唯一索引中沒有索引字段的值,則索引將為此文檔存儲null值。由於唯一的約束,MongoDB將只允許一個缺失索引字段的文檔。
如果有多個文檔沒有索引字段的至或缺少索引字段,則在添加唯一索引時將失敗,並報出重復鍵錯誤。
對集合的部分文檔進行索引 :db.t.createIndex({ category: 1 },{ partialFilterExpression: { _id: { $gt: 2 } } } )
partialFilterExpression支持的選項:
等值表達式(如 field: value或使用$eq操作符)
$exists:true 表達式
$gt,$gte,$lt,$lte表達式
$type 表達式
第一層級的$and操作符
部分索引的使用
db.t.find({"category":"F array",_id:{$gt:1}}).explain() //無法利用部分索引
db.t.find({"category":"F array",_id:{$gt:3}}).explain() //可以利用部分索引
查詢要使用部分索引則查詢條件必須與索引表達式相同或是其子集
部分索引與唯一性
db.users.insert( { username: "david", age: 25 } )
db.users.insert( { username: "amanda", age: 26 } )
db.users.insert( { username: "andy", age: 30 } )
db.users.createIndex({ username: 1 },{ unique: true, partialFilterExpression: { age: { $gte: 20 } } })
再次插入用戶名為andy年齡為13(不在$gte:20的范圍)的文檔
db.users.insert( { username: "andy", age: 13} ) //插入成功
限制
_id索引不可以是部分索引
shard key索引不可以是部分索引
稀疏索引
指僅僅包含具有索引字段的文檔,稀疏索引可以認為是部分索引的子集
稀疏索引的創建
db.collection.insert({ y: 1 } );
db.collection.createIndex( { x: 1 }, { sparse: true } );
稀疏索引與hint
db.collection.find().hint( { x: 1 } ).count();
db.collection.find().hint( { x: 1 } ) ;
稀疏索引與唯一性
db.collection.createIndex( { z: 1 } , { sparse: true, unique: true } )
db.collection.insert({y:2}) // 正常
db.collection.insert({y:3}) // 正常
TTL索引
TTL索引在索引字段值超過指定的秒數后過期文檔; 即,到期閾值是索引字段值加上指定的秒數。
如果字段是數組,並且索引中有多個日期值,則MongoDB使用數組中的最低(即最早)日期值來計算到期閾值。
如果文檔中的索引字段不是日期或包含日期值的數組,則文檔將不會過期。
如果文檔不包含索引字段,則文檔不會過期。
在后台創建TTL索引時,TTL索引可以在構建索引時刪除文檔。如果在前台構建TTL索引,則在索引構建完畢后立即刪除過期文檔
TTL索引的創建
db.exp.insert({lastDate:new Date()})
db.exp.createIndex({"lastDate":1},{expireAfterSeconds:60})
在線修改TTL過期時間
db.runCommand({collMod:"exp",index{keyPattern:{lastDate:1},expireAfterSeconds:120}})
查詢數據庫下的所有索引
db.getCollectionNames().forEach(function(collection) {
indexes = db[collection].getIndexes();
print("Indexes for " + collection + ":");
printjson(indexes);});
索引統計信息
db.serverStatus(scale)的統計輸出
metrics.queryexecutor.scanned:查詢和查詢計划評估期間掃描的索引項的總數。蓋計數器與explain()輸出中totalkeysexamamed意思相同
metrics.operation.scanAndOrder:表示無法使用索引排序的查詢總次數
db.users.stats(scale)的統計輸出
totalindexSize:所有索引的總大小。scale參數影響輸出。如一個索引使用前綴壓縮(WiredTiger的默認),則返回的大小為索引的壓縮大小
indexSizes:指定集合上每個現有索引對應的鍵和大小。scale參數影響輸出
db.stats(scale)的統計輸出
indexes:在此數據庫中所有集合的索引總數目
indexSize:在此數據庫中創建的所有索引的總大小。scale參數影響輸出
索引設計原則
1、每個查詢原則上都需要創建對應索引
2、單個索引設計應考慮滿足盡量多的查詢
3、索引字段選擇及順序需要考慮查詢覆蓋率及選擇性
4、對於更新及其頻繁的字段上創建索引需慎重
5、對於數組索引需要慎重考慮未來元素個數
6、對於超長字符串類型字段上慎用B數索引
7、並發更新較高的單個集合上不宜創建過多索引
MongoDB通用優化建議與實踐
MongoDB查詢緩存邏輯
索引選擇基於采樣代價模型,查詢第一次執行如果有多個執行計划則會根據模型選出最優計划並緩存
語句執行會根據執行計划的表現對緩存進行重用或重建,如多次迭代都沒有返回足夠的文檔則可能會觸發重構
當MongoDB創建或刪除索引時,會將對應集合的緩存執行計划清空並重新選擇
如果MongoDB重新啟動或關閉,則查詢計划緩存會被清理然后依據查詢進行重建
MongoDB查詢緩存操作
db.eof.find({x:1}); db.eof.find({x:3}).sort({y:1}); 執行查詢模擬查詢計划緩存
db.eof.getPlanCache().listQueryShapes(); 查詢對應集合的查詢計划緩存指紋
db.eof.getPlanCache().getPlansByQuery({"x":1},{},{"y":1}); 通過指紋查看查詢計划緩存信息
注:查詢指紋或叫形狀是由query條件、sort條件及projection(投影)組成
db.eof.getPlanCache().clearPlansByQuery({"x":1},{},{"y":1}) 通過指定指紋清空對應查詢計划緩存
db.eof.getPlanCache().clear() 清空對應集合所有執行計划的緩存信息
查詢計划詳解
支持查詢計划的操作
aggregate()
count()
distinct()
find()
group()
remove()
update()
findAndModify()
查詢計划語法:
1、db.collection.explain(verbosity).<method(...)> 返回游標mongo shell 默認迭代
2、db.collection.<method(...)>.explain(verbosity) 返回json文檔化結果
查詢計划詳解:
queryPlanner模式
MongoDB通過查詢優化器對查詢評估后選擇一個最佳的查詢計划(默認模式)
executionStats模式
MongoDB通過查詢優化器對查詢進行評估並選擇一個最佳的查詢計划執行后返回統計信息
對於寫操作則返回關於更新和刪除操作的統計信息,但並不真正修改數據庫數據
對於非最優執行計划不返回對於統計信息
allPlansExecution模式
與上述兩種模式的差別是除返回兩種模式的信息的同時還包含非最優計划執行的統計信息
ceshi27020_mongo:PRIMARY> db.eof.find({x:2100}).explain()
{ "queryPlanner" : { //查詢計划信息
"plannerVersion" : 1, //查詢計划版本
"namespace" : "test.eof", //查詢集合
"indexFilterSet" : false, //查詢過濾器
"parsedQuery" : { //查詢具體條件
"x" : {
"$eq" : 2100
}
},
"winningPlan" : { //最優計划
"stage" : "FETCH", //獲取文檔階段
"inputStage" : { // 過濾條件
"stage" : "IXSCAN", //索引掃描階段
"keyPattern" : { //要遍歷的索引
"x" : 1
},
"indexName" : "idx_x", //索引名稱
"isMultiKey" : false, //是否是多key索引
"isUnique" : false, //是否是唯一索引
"isSparse" : false, //是否是稀疏索引
"isPartial" : false, //是否是部分索引
"indexVersion" : 1, //索引版本號
"direction" : "forward", //索引掃描方向(forward對應1,backward對應-1)
"indexBounds" : { //索引掃描的邊界
"x" : [ "[2100.0, 2100.0]"] } } }, "rejectedPlans" : [ ] },
"serverInfo" : {
"host" : "LeDB-VM-124064213",
"port" : 27020,
"version" : "3.2.20",
"gitVersion" : "a7a144f40b70bfe290906eb33ff2714933544af8" }, "ok" : 1}