MongoDB 的索引機制對於高效能查詢至關重要。理解不同索引型別的特性,例如文字索引和地理空間索引,才能針對不同查詢場景進行最佳化。文字索引在多詞彙搜尋時,每個詞彙都會觸發一次索引掃描,因此需控制搜尋詞彙數量。對於長文字短語,正規表示式查詢或全集合掃描可能更有效率。地理空間索引則需根據球面或平面資料選擇 2dsphere 或 2d 索引。此外,網路傳輸次數也是效能瓶頸之一。利用投影限制回傳欄位、調整 batchSize 控制每次傳輸的檔案數量,以及合併多個查詢,都能有效減少網路往返。最後,對於不常變動的資料,例如電影標題,使用快取機制可以避免重複查詢,顯著提升應用程式效能。
文字索引效能分析
在傳統索引中,我們可以使用集合掃描來替代索引查詢。然而,若沒有文字索引,則完全無法進行全文檢索。因此,使用全文索引幾乎是必然的選擇。
不過,文字索引有一些效能特徵需要特別注意。首先,MongoDB 會針對搜尋條件中的每個詞彙執行索引掃描。例如,當我們搜尋五個不同的單詞時,相應地會執行五次文字索引掃描:
mongo> var exp = db.bigEnron.explain('executionStats').find(
{ $text: { $search: 'Confirmation Rooms Credit card tax email ' } },
{ score: { $meta: 'textScore' }, body: 1 }
).sort({ score: { $meta: 'textScore' } }).limit(3);
mongo> mongoTuning.executionStats(exp);
1 IXSCAN ( body_text ms:229 keys:53068)
2 IXSCAN ( body_text ms:764 keys:217480)
3 IXSCAN ( body_text ms:748 keys:229382)
4 IXSCAN ( body_text ms:1376 keys:398325)
5 IXSCAN ( body_text ms:362 keys:108996)
6 IXSCAN ( body_text ms:181 keys:93970)
7 TEXT_OR ( ms:494636 docs:843437)
8 TEXT_MATCH ( ms:494709)
9 TEXT ( body_text ms:494746)
10 SORT_KEY_GENERATOR ( ms:494795)
11 SORT ( ms:495015)
12 PROJECTION_DEFAULT ( ms:495072)
內容解密:
- IXSCAN 代表索引掃描,每一行對應一次針對特定詞彙的索引掃描。
- TEXT_OR 和 TEXT_MATCH 是處理文字搜尋匹配的階段。
- SORT_KEY_GENERATOR 和 SORT 表示對結果進行排序的過程。
- 從結果可以看出,搜尋詞越多,執行的索引掃描次數越多,查詢時間越長。
如圖 5-6 所示,搜尋詞的數量與文字搜尋的時間成正比。因此,在必要時應限制搜尋詞的數量,以保持合理的回應時間。
精確短語搜尋的效能考量
即使搜尋精確短語,MongoDB 仍然會對短語中的每個詞彙執行一次索引掃描,因為索引本身並不知道詞彙之間的順序關係。如果搜尋較長的精確文字短語,使用正規表示式查詢和全集合掃描可能更為高效。
例如,查詢文字 “are you going to be at the game tonight”:
mongo> var exp = db.bigEnron.explain('executionStats').find(
{ $text: { $search: '"are you going to be at the game tonight"' } }
);
mongo> mongoTuning.executionStats(exp);
1 IXSCAN ( body_text ms:354 keys:62838)
2 IXSCAN ( body_text ms:2136 keys:515760)
3 IXSCAN ( body_text ms:146 keys:39721)
4 OR ( ms:2767)
5 FETCH ( ms:379793 docs:563201)
6 TEXT_MATCH ( ms:383409)
7 TEXT ( body_text ms:383517)
內容解密:
- MongoDB 只對 “game”、“going” 和 “tonight” 三個詞進行了索引掃描。
- 相比之下,全集合掃描的查詢時間更短:
mongo> var exp = db.bigEnron.explain('executionStats').find(
{ body: /are you going to be at the game tonight/ }
);
mongo> mongoTuning.executionStats(exp);
1 COLLSCAN ( ms:102289 docs:2897816)
Totals: ms: 145925 keys: 0 Docs: 2897816
內容解密:
- COLLSCAN 表示全集合掃描。
- 全集合掃描只花費了約 145 秒,而文字索引查詢花費了約 414 秒。
文字索引的其他效能考量
- 由於詞幹提取方法的影響,文字索引可能會非常龐大並且需要很長時間來建立。
- MongoDB 建議系統具備足夠的記憶體來容納文字索引,以避免在搜尋過程中出現大量的 IO 操作。
- 在使用排序查詢時,即使是複合文字索引,也無法利用文字索引來決定結果的順序。
地理空間索引簡介
現代的位置感知應用通常需要在地圖資料上進行搜尋。這些搜尋可能包括在某個區域內查找出租房源、尋找附近的場所,或者根據拍攝地點對照片進行分類別。許多裝置在我們移動的過程中會被動地捕捉大量的位置資料,這些資料通常被稱為地理空間資料。
MongoDB 提供了查詢這些資料的方法以及特定的索引型別來最佳化查詢。
地理空間資料範例
{
"_id": ObjectId("578f6fa2df35c7fbdbaed8c4"),
"recrd": "",
"vesslterms": "",
"feature_type": "Wrecks - Visible",
"chart": "US,U1,graph,DNC H1409860",
"latdec": 9.3547792,
"londec": -79.9081268,
"gp_quality": "",
"depth": "",
"sounding_type": "",
"history": "",
"quasou": "",
"watlev": "always dry",
"coordinates": [-79.9081268, 9.3547792]
}
內容解密:
- 這是一個遺留格式的地理空間資料範例,包含經緯度資訊。
- MongoDB 也支援 GeoJSON 格式,用於表示更複雜的空間資訊。
GeoJSON 格式範例
{
"_id": ObjectId("578f6fa2df35c7fbdbaed8c4"),
"recrd": "",
"vesslterms": "",
"feature_type": "Wrecks - Visible",
"chart": "US,U1,graph,DNC H1409860",
"latdec": 9.3547792,
"londec": -79.9081268,
"gp_quality": "",
"depth": "",
"sounding_type": "",
"history": "",
"quasou": "",
"watlev": "always dry",
"location": {
"type": "Point",
"coordinates": [-79.9081268, 9.3547792]
}
}
內容解密:
- GeoJSON 格式指定了資料型別和值,可以表示單點或多點的坐標陣列。
- 這種格式允許定義更複雜的空間資訊,如線和多邊形。
地理空間查詢範例
db.shipwrecks.find({
coordinates: {
$near: {
$geometry: { type: "Point", coordinates: [-79.9081268, 9.3547792] }
}
}
})
內容解密:
- 使用
$near運算子查詢靠近特定點的檔案。 $geometry指定了查詢點的 GeoJSON 表示形式。
MongoDB地理空間索引詳解與效能最佳化
MongoDB支援地理空間索引,讓開發者能夠高效地查詢和分析地理空間資料。本文將探討MongoDB的地理空間索引,包括其型別、建立方法、效能最佳化以及限制。
地理空間索引型別
MongoDB提供兩種地理空間索引:2dsphere和2d。
2dsphere索參照於索引球面上的資料,如地球表面的點。這種索引適用於大多數地理空間應用。2d索參照於索引二維平面上的資料,如傳統的地圖。這種索引適用於不需要考慮地球曲率的應用。
建立地理空間索引
建立地理空間索引非常簡單,只需指定要索引的欄位和索引型別。例如:
db.shipwrecks.createIndex({ "coordinates": "2dsphere" })
在建立地理空間索引之前,請確保欄位中的資料是有效的GeoJSON物件或座標對。
地理空間查詢運算元
MongoDB提供了多種地理空間查詢運算元,包括:
$near:查詢鄰近某個點的資料。$nearSphere:查詢鄰近某個點的資料,考慮地球曲率。$geoWithin:查詢某個區域內的資料。
大多數地理空間查詢運算元需要對應的地理空間索引才能正常工作。
地理空間索引效能最佳化
雖然地理空間索引是查詢地理空間資料的必要條件,但它們不一定能提高查詢效能。以下是一些最佳化地理空間查詢效能的方法:
- 使用
$geoWithin運算元時,建立對應的地理空間索引可以提高查詢效能。 - 使用
$near或$nearSphere運算元時,指定minDistance和maxDistance引數可以限制MongoDB需要檢查的檔案數量,從而提高查詢效能。 - 如果需要對查詢結果進行排序,使用
$geoWithin或$geoNear聚合階段可以避免不必要的排序操作。
程式碼範例:使用 $near 運算元查詢鄰近某個點的資料
db.shipwrecks.find({
coordinates: {
$near: {
$geometry: {
type: "Point",
coordinates: [-79.908, 9.354]
},
$minDistance: 1000,
$maxDistance: 10000
}
}
}).limit(1).pretty();
內容解密:
- $near運算元:用於查詢鄰近某個點的檔案。
- $geometry:指定要查詢的點的GeoJSON物件。
- $minDistance和$maxDistance:限制查詢結果的距離範圍。
- limit(1):限制查詢結果的數量為1。
- pretty():格式化輸出結果,使其易於閱讀。
地理空間索引限制
- 無法建立覆寫查詢(covered query)。
- 在分割集合(sharded collection)中,地理空間索引不能用作分割鍵(shard key)。
2d索引型別不支援GeoJSON資料。- 建立多個地理空間索引可能會影響聚合操作的行為。
總之,MongoDB的地理空間索引為開發者提供了高效查詢和分析地理空間資料的能力。透過選擇合適的索引型別、建立正確的索引以及最佳化查詢效能,開發者可以充分利用MongoDB的地理空間功能。
查詢調優
在幾乎所有的應用程式中,大部分的資料函式庫時間都花在資料檢索上。檔案只能被插入或刪除一次,但通常會在更新之間被讀取多次,甚至更新也必須在執行操作之前檢索資料。因此,我們的 MongoDB 調優工作大多集中在查詢資料上,特別是 find() 陳述式,它是 MongoDB 資料檢索的主要工具。
快取結果
在過去,當 Guy 主要使用根據 SQL 的資料函式庫時,一位睿智的人告訴他「最快的 SQL 陳述式是你永遠不會傳送到資料函式庫的陳述式」。換句話說,如果可以避免,就不要向資料函式庫傳送請求。即使是最簡單的請求,也涉及網路往返,可能還需要 IO 操作——所以,除非絕對必要,否則不要與資料函式庫互動。
這個原則同樣適用於 MongoDB。我們經常向資料函式庫請求相同的資訊——即使我們知道這些資訊不會改變。例如,考慮以下簡單的函式:
function recordView(customerId, filmId) {
let filmTitle = db.films.findOne({ _id: filmId }, { Title: 1 }).Title;
db.customers.update({ _id: customerId }, {
$push: {
views: {
filmId,
title: filmTitle,
viewDate: new ISODate()
}
}
});
}
我們在 films 集合中查詢電影標題——這很合理。但是,電影標題永遠不會改變,在任何給定的日子裡,一些電影會被多次觀看。所以,為什麼要再次回到資料函式庫去取得已經處理過的電影的標題?
內容解密:
這段程式碼展示了一個簡單的函式 recordView,它首先根據 filmId 從 films 集合中查詢電影標題,然後更新 customers 集合中的 views 欄位,將觀看記錄插入其中。這段程式碼的問題在於,每次呼叫 recordView 時,都會查詢資料函式庫以取得電影標題,即使這個標題不會改變。
快取實作
下面的程式碼將電影標題快取在本地記憶體中。我們永遠不會第二次向資料函式庫詢問電影標題:
var cacheDemo = {};
cacheDemo.filmCache = {};
cacheDemo.getFilmId = function(filmId) {
if (filmId in cacheDemo.filmCache) {
return cacheDemo.filmCache[filmId];
} else {
let filmTitle = db.films.findOne({ _id: filmId }, { Title: 1 }).Title;
cacheDemo.filmCache[filmId] = filmTitle;
return filmTitle;
}
};
cacheDemo.recordView = function(customerId, filmId) {
let filmTitle = cacheDemo.getFilmId(filmId);
db.customers.update({ _id: customerId }, {
$push: {
views: {
filmId,
title: filmTitle,
viewDate: new ISODate()
}
}
});
};
內容解密:
這段程式碼實作了一個簡單的快取機制,用於儲存電影標題。getFilmId 函式首先檢查電影標題是否已經在快取中,如果是,則直接傳回快取中的標題;如果不是,則查詢資料函式庫,將標題存入快取後傳回。這樣,可以避免重複查詢資料函式庫,提高效能。
快取實作明顯更快。圖 6-1 顯示了執行每個函式 1000 次的耗時比較。
快取考量
以下是實作快取時需要考慮的一些因素:
- 快取會消耗客戶端程式的記憶體。在許多環境中,記憶體很豐富,被考慮用於快取的表格相對較小。然而,對於大型集合和記憶體受限的環境,快取策略的實作可能會因導致應用層或客戶端的記憶體短缺而降低效能。
- 當快取相對較小時,順序掃描(即從第一個條目到最後一個條目檢查快取中的每個條目)可能會帶來足夠的效能。然而,如果快取較大,順序掃描可能會開始降低效能。為了保持良好的效能,可能需要實作高階搜尋技術,如雜湊或二分搜尋。在前面的例子中,快取由電影 ID 有效索引,因此無論涉及多少部電影,都將保持高效。
- 如果在程式執行期間更新被快取的集合,則除非實作一些複雜的同步機制,否則更改可能不會反映在快取中。因此,本地快取最適合靜態集合。
最佳化查詢效能:減少網路傳輸次數
在應用程式中,資料函式庫往往是效能瓶頸的來源之一,主要原因在於資料需要在網路中傳輸。每次應用程式存取資料函式庫中的資料時,這些資料都必須跨越網路傳輸。在極端的情況下,如果資料函式庫位於另一個大陸的雲端伺服器上,傳輸距離可能長達數千英里。
網路傳輸需要時間,通常遠遠超過CPU處理所需的時間。因此,減少網路傳輸次數——或稱為網路往返次數——對於降低查詢時間至關重要。
投影(Projections)
投影允許我們指定查詢結果中應包含的屬性。MongoDB程式設計師經常不指定投影,因為應用程式通常只是丟棄不需要的資料。但這對網路往返次數的影響非常巨大。考慮以下查詢:
db.customers.find().forEach((customer)=>{
if (customer.LastName in results )
results[customer.LastName]++;
else
results[customer.LastName]=1;
});
我們正在統計客戶姓氏的數量。注意,我們只使用了customers集合中的LastName屬性。因此,我們可以新增投影以確保結果中只包含LastName:
db.customers.find({},{LastName:1,_id:0}).forEach((customer)=>{
if (customer.LastName in results )
results[customer.LastName]++;
else
results[customer.LastName]=1;
});
內容解密:
db.customers.find():這是MongoDB的查詢語法,用於查詢customers集合中的檔案。{LastName:1,_id:0}:這是投影設定,LastName:1表示包含LastName欄位,_id:0表示排除_id欄位。forEach((customer)=>{...}):遍歷查詢結果,對每個客戶檔案執行給定的函式。
在慢速網路上,效能差異非常驚人——投影使吞吐量提高了十倍。即使在與資料函式庫伺服器相同的主機上執行查詢(從而減少了往返時間),效能差異仍然很大。
批次處理(Batch Processing)
MongoDB自動管理每個網路封包中包含的檔案數量。批次大小限制為16MB的BSON檔案大小,但由於網路封包通常遠小於此,這一限制通常並不重要。然而,預設情況下,MongoDB只會在初始批次中傳回101個檔案,這意味著有時資料可能會跨越兩次網路傳輸。
當使用遊標檢索資料時,可以使用batchSize子句指定每次操作中擷取的行數。例如:
var myCursor=db.millions.find({},{n:1,_id:0})
.batchSize(batchsize);
while (myCursor.hasNext()) {
myCursor.next();
count+=1;
}
內容解密:
batchSize(batchsize):設定批次大小,控制每次網路請求從MongoDB資料函式庫檢索的檔案數量。while (myCursor.hasNext()):遍歷遊標,只要還有檔案未處理就繼續迴圈。myCursor.next():取得遊標中的下一個檔案。
修改batchSize的有效性很大程度上取決於底層驅動程式的實作。在MongoDB shell中,預設的batchSize已經設定為最高值。然而,在NodeJS驅動程式中,batchSize預設為1000。因此,在NodeJS程式中調整batchSize可能會帶來效能提升。
避免程式碼中的過多網路往返
batchSize()幫助我們在MongoDB驅動程式中透明地減少網路開銷。但有時,最佳化網路往返的唯一方法是調整應用程式邏輯。例如,考慮以下邏輯:
for (i = 1; i < max; i++) {
cursor = useDb.collection(mycollection).find({
// 查詢條件
});
// 處理遊標
}
內容解密:
for (i = 1; i < max; i++):迴圈變數i從1到max-1。cursor = useDb.collection(mycollection).find({...}):在迴圈內執行查詢,每次迴圈都建立新的遊標。
這種邏輯可能導致過多的網路往返。最佳化方法是盡可能減少查詢次數,例如透過調整查詢條件或合併多個查詢到一次操作中。