MongoDB 的索引掃描機制對於查詢效能至關重要。理解索引掃描的行為,才能根據實際情況選擇合適的索引策略。部分匹配查詢雖然可以使用索引,但如果範圍過大,效率反而不如全表掃描。對於不區分大小寫的搜尋,則需要建立帶有特定校對規則的索引才能有效提升效能。複合索引能有效減少檔案存取次數,但索引鍵的順序會影響其效率。覆寫索引能直接從索引中取得查詢結果,避免存取原始檔案,大幅提升查詢速度。最後,索引合併能結合多個索引的優勢,但通常不如針對查詢條件建立複合索引的效能表現。
索引掃描的效能影響
索引掃描(Index scans)是在MongoDB查詢最佳化中非常重要的概念。當查詢條件涉及索引欄位時,MongoDB可能會選擇使用索引掃描來提高查詢效率。然而,並非所有的索引掃描都能帶來效能的提升。
部分匹配查詢中的索引掃描
當進行字串條件的部分匹配查詢時,MongoDB會執行索引掃描。例如,在以下查詢中,對LastName建立的索引會被掃描,以找出所有大於或等於“HARRIS”且小於或等於“HARRIT”的條目。
mongo> var explainObj = db.customers.explain('executionStats').find({LastName: {$regex: /^HARRIS(.*)/}});
mongo> mongoTuning.executionStats(explainObj);
1 IXSCAN ( LastName_1 ms:0 keys:1366)
2 FETCH ( ms:0 docs:1365)
Totals: ms: 4 keys: 1366 Docs: 1365
內容解密:
- IXSCAN:表示進行了索引掃描,掃描的索引是
LastName_1,耗時0毫秒,掃描了1366個鍵。 - FETCH:在索引掃描後,MongoDB根據索引中的指標抓取了相關檔案,耗時0毫秒,抓取了1365個檔案。
- Totals:總耗時4毫秒,掃描了1366個鍵,傳回了1365個檔案。
索引掃描的效能問題
索引掃描並不總是高效。如果查詢範圍過大,索引掃描甚至可能比全表掃描(Collection scan)更慢。如圖5-2所示,當查詢範圍很廣時,全表掃描可能比索引掃描更高效;但當查詢範圍較窄時,索引掃描則更具優勢。
不區分大小寫的搜尋
在實際應用中,我們經常需要進行不區分大小寫的搜尋。例如,當我們不確定姓氏是否以大寫或小寫輸入時,可以使用以下查詢:
mongo> var e = db.customers.explain('executionStats').find({LastName: /^SMITH$/i}, {});
mongo> mongoTuning.quickExplain(e);
1 IXSCAN LastName_1
2 FETCH
乍看之下,似乎MongoDB使用了索引進行查詢,但進一步檢視executionStats會發現,雖然使用了索引,但MongoDB掃描了全部410,071個鍵。
mongo> var e = db.customers.explain('executionStats').find({LastName: /^SMITH$/i}, {});
mongo> mongoTuning.executionStats(e);
1 IXSCAN ( LastName_1 ms:8 keys:410071)
2 FETCH ( ms:8 docs:711)
Totals: ms: 293 keys: 410071 Docs: 711
內容解密:
- IXSCAN:對
LastName_1索引進行了掃描,耗時8毫秒,掃描了410,071個鍵。 - FETCH:抓取相關檔案,耗時8毫秒,抓取了711個檔案。
- Totals:總耗時293毫秒,掃描了410,071個鍵,傳回了711個檔案。
使用不區分大小寫的校對規則
要實作高效的不區分大小寫搜尋,可以建立一個具有不區分大小寫校對規則的索引。
db.customers.createIndex(
{ LastName: 1 },
{ collation: { locale: 'en', strength: 2 } }
);
使用相同的校對規則進行查詢,可以獲得正確的結果,且索引能夠被有效利用。
mongo> db.customers.find({ LastName: 'SMITH' }, { LastName: 1, _id: 0 }).collation({ locale: 'en', strength: 2 }).limit(1);
{
"LastName": "Smith"
}
檢視executionStats可以發現,索引被正確利用,並且只掃描了相關的鍵。
mongo> var e = db.customers.explain('executionStats').find({ LastName: 'SMITH' }).collation({ locale: 'en', strength: 2 });
mongo> mongoTuning.executionStats(e);
1 IXSCAN ( LastName_1 ms:0 keys:711)
2 FETCH ( ms:0 docs:711)
Totals: ms: 2 keys: 711 Docs: 711
內容解密:
- IXSCAN:對
LastName_1索引進行了掃描,耗時0毫秒,掃描了711個鍵。 - FETCH:抓取相關檔案,耗時0毫秒,抓取了711個檔案。
- Totals:總耗時2毫秒,掃描了711個鍵,傳回了711個檔案。
複合索引
複合索引是由多個欄位組成的索引。相比單一欄位的索引,複合索引通常更具選擇性,能夠指向更少的檔案。當查詢條件包含複合索引中的所有欄位時,查詢效能最佳。
複合索引的效能優勢
隨著複合索引中欄位數量的增加,查詢效能通常會提高。如圖5-3所示,當查詢條件包含LastName、FirstName和dateOfBirth時,建立包含這些欄位的複合索引能夠顯著減少檔案存取次數,從而提高查詢效能。
圖示說明
此圖示展示了隨著索引中欄位數量的增加,檔案存取次數的變化趨勢。
在沒有索引的情況下,需要掃描所有411,121個檔案。僅對LastName建立索引,就將檔案存取次數減少到6918次。進一步加入FirstName和dateOfBirth到索引中,能夠將檔案存取次數降至2次。最終,在索引中包含電話號碼(“tel”)欄位後,可以直接從索引中取得所需資訊,無需存取原始檔案。
複合索引鍵順序的重要性
在MongoDB中,複合索引是一種強大的工具,可以最佳化查詢效能。複合索引可以支援查詢中不包含所有索引鍵的情況,只要查詢中包含索引的前導屬性(leading attributes),就可以使用該索引。
舉例來說,如果我們在一個集合上建立了一個複合索引,鍵的順序是{LastName:1, FirstName:1, dateOfBirth:1},那麼這個索引就可以用來最佳化查詢LastName、LastName和FirstName的組合查詢。但是,如果查詢只包含FirstName或dateOfBirth,那麼這個索引就無法發揮作用。
複合索引的指導原則
在決定是否使用複合索引以及如何選擇索引鍵和順序時,以下指導原則可以幫助我們:
- 為集合中經常一起出現在
find()或$match條件中的屬性建立複合索引。 - 如果某些屬性有時會單獨出現在查詢條件中,將它們放在索引的前面。
- 一個複合索引如果能夠支援查詢中不包含所有屬性的情況,那麼它將更加有用。例如,建立索引
{"LastName":1, "FirstName":1}比建立索引{"FirstName":1, "LastName":1}更有用,因為查詢只針對LastName的情況比查詢只針對FirstName的情況更常見。 - 一個屬性的選擇性越強,將它放在索引的前面越有用。但是,需要注意的是,WiredTiger索引壓縮可以大大縮小索引的大小。當前導列的選擇性較低時,索引壓縮最為有效。
覆寫索引
一個覆寫索引是指能夠完全解析查詢的索引。換句話說,一個查詢如果能夠完全由索引來解析,那麼它就被稱為覆寫查詢。覆寫索引是一種強大的最佳化查詢機制,因為索引通常遠小於集合,所以不需要從集合中檢索檔案到記憶體中的查詢是非常高效的。
索引合併
在某些情況下,建立一個包含所有查詢條件的複合索引可能是不切實際的。幸運的是,MongoDB可以在多個索引之間執行索引合併(index merge)。例如,如果我們有一個查詢db.iotData.find({a:1,b:1}),並且在a和b上分別有一個索引,那麼MongoDB可以執行這兩個索引的交集。
對於$and條件,複合索引通常優於索引合併。但是,對於$or條件,索引合併往往是最佳解決方案。
部分索引和稀疏索引
在某些情況下,我們可能希望建立一個部分索引或稀疏索引,以減少索引的大小並提高記憶體效率。部分索引是指只維護資料子集的索引。例如,我們可以建立一個部分索引,只包含被轉發過的推文。
db.tweets.createIndex(
{ 'user.name': 1, retweet_count: 1 },
{ partialFilterExpression: { retweet_count: { $gt: 0 } } }
);
部分索引的使用
在使用部分索引時,我們需要在查詢中指定篩選條件,以確保MongoDB知道所需的所有資料都在索引中。
db.tweets.find(
{ 'user.name': 'Mean Magazine Bot', retweet_count: { $gt: 0 } },
{ text: 1 }
).sort({ retweet_count: -1 }).limit(1);
部分索引的優點
部分索引可以大大減少索引的大小,從而提高記憶體效率。但是,需要注意的是,部分索引只對特定的查詢有用,因此需要根據具體的使用場景進行設計。
MongoDB 索引最佳實踐與效能最佳化
索引型別與應用場景
MongoDB 提供了多種索引型別以滿足不同的查詢需求,包括單鍵索引、複合索引、稀疏索引和萬用字元索引等。每種索引型別都有其特定的使用場景和效能影響。
稀疏索引的應用與限制
稀疏索引(Sparse Indexes)是一種特殊的索引型別,它不會索引集合中所有檔案,特別是那些不包含被索引屬性的檔案。大部分情況下,稀疏索引與普通索引的效果相似,且可能顯著較小。然而,稀疏索引無法支援對被索引屬性的 $exists:true 查詢。
程式碼範例:稀疏索引查詢效能分析
// 建立稀疏索引
db.customers.createIndex({ updateFlag: 1 }, { sparse: true });
// 查詢 updateFlag 不存在的檔案
var exp = db.customers.explain().find({ updateFlag: { $exists: false } });
mongoTuning.quickExplain(exp); // 輸出:1 COLLSCAN
// 查詢 updateFlag 存在的檔案
var exp = db.customers.explain().find({ updateFlag: { $exists: true } });
mongoTuning.quickExplain(exp); // 輸出:1 IXSCAN updateFlag_1,2 FETCH
內容解密:
- 建立稀疏索引時,需指定
{ sparse: true }選項。 - 當查詢條件為
$exists: false時,MongoDB 無法利用稀疏索引,導致全集合掃描(COLLSCAN)。 - 當查詢條件為
$exists: true時,MongoDB 可以利用稀疏索引進行索引掃描(IXSCAN)。
萬用字元索引的應用與效能影響
萬用字元索引(Wildcard Indexes)是一種特殊的索引型別,它可以對子檔案的任意屬性建立索引。這種索引在某些場景下非常有用,例如當需要對動態或未知的屬性進行查詢時。
程式碼範例:萬用字元索引建立與效能比較
// 建立萬用字元索引
db.mycollection.createIndex({ "data.$**": 1 });
// 比較不同索引策略下的查詢效能
內容解密:
- 萬用字元索引可以對子檔案的任意屬性建立索引,無需預先知道屬性名稱。
- 萬用字元索引在查詢效能上與單屬性索引相似,但其維護成本較高。
- 在插入、更新和刪除操作中,萬用字元索引的效能開銷較大,甚至可能超過多個單屬性索引的總和。
索引的最佳實踐
- 謹慎使用萬用字元索引:雖然萬用字元索引提供了很大的靈活性,但其維護成本較高,應僅在必要時使用。
- 避免不必要的索引:每個索引都會對寫入操作產生額外的效能開銷,因此應定期檢查並移除不必要的索引。
- 使用 explain() 分析查詢計畫:透過
explain()方法,可以瞭解 MongoDB 如何執行查詢,並據此最佳化索引策略。
MongoDB全文索引的建立與應用
在建立需要處理大量文字資料的應用程式時,經常需要對數千份檔案中的大型文字欄位進行搜尋,以找到最合適的匹配結果。這時,文字索引就變得非常有用。在調整或建立文字索引時,瞭解MongoDB如何解讀該索引以及它將如何影響查詢至關重要。
字尾提取法
MongoDB使用一種稱為字尾提取法(suffix stemming)的方法來建立搜尋索引。字尾提取法涉及找到每個單詞開頭的共同元素(字首),形成搜尋樹的根。每個不同的字尾都會分支到自己的節點,可能進一步分支。這個過程建立了一棵可以從根(最常見的分享元素)到葉節點進行有效搜尋的樹,根到葉節點的路徑形成了一個完整的單詞。
內容解密:
字尾提取法的關鍵在於找出單詞的共同字首,並以此為基礎建立搜尋樹。例如,若有「finder」、「finding」和「findable」等單詞,字尾提取法會找出共同的根「find」,然後將字尾「er」、「ing」和「able」分別分支。這種方法使得搜尋過程更加高效。
建立文字索引
在MongoDB中,建立文字索引的語法與建立其他型別的索引相同。只需指定要建立索引的欄位,並將索引型別指定為"text":
db.listingsAndReviews.createIndex({description: "text"})
輸出結果:
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 4,
"numIndexesAfter" : 5,
"ok" : 1
}
內容解密:
這段程式碼在listingsAndReviews集合的description欄位上建立了一個文字索引。輸出結果顯示索引建立成功。
多欄位文字索引與複合索引
可以對多個欄位建立文字索引:
db.listingsAndReviews.createIndex({summary: "text", space: "text"})
也可以建立包含文字索引和其他型別索引的複合索引:
db.listingsAndReviews.createIndex({summary: "text", beds: 1})
內容解密:
這兩段程式碼分別在多個欄位上建立了文字索引和複合索引。複合索引結合了文字索引和其他型別的索引,能夠支援更複雜的查詢需求。
設定欄位權重
在建立文字索引時,可以為每個欄位指定權重,以表示該欄位相對於其他被索引欄位的重要性:
db.listingsAndReviews.createIndex({summary: "text", description: "text"}, {weights: {summary: 3, description: 2}})
內容解密:
這段程式碼在summary和description欄位上建立了文字索引,並分別為它們設定了權重。權重將用於決定$text查詢的結果。
使用$text運算元查詢
有了文字索引,就可以使用$text運算元進行查詢:
db.listingsAndReviews.findOne({$text: {$search: "oven kettle and microwave"}}, {summary: 1})
內容解密:
這段程式碼使用$text運算元在已建立文字索引的欄位中搜尋包含指定關鍵字的檔案。
投影文字搜尋分數
可以投影出檔案在文字搜尋中的分數,並根據分數排序,以取得最相關的結果:
db.listingsAndReviews.find(
{ $text: { $search: 'oven kettle and microwave' } },
{ score: { $meta: 'textScore' }, summary: 1 }
).sort({ score: { $meta: 'textScore' } }).limit(3);
內容解密:
這段程式碼不僅進行了文字搜尋,還投影出了每個檔案的搜尋分數,並根據分數排序,最後限制結果數量為3。
文字索引的限制
使用文字索引時需要注意以下限制:
- 文字索引始終是稀疏的,因此指定sparse無效。
- 複合索引中如果包含文字索引,則不能包含多鍵或地理空間欄位。
- 每個集合只能有一個文字索引。