減少網路往返次數是提升 MongoDB 效能的關鍵。批次處理資料,尤其在遠端伺服器上,能大幅降低網路延遲的影響。選擇合適的 batchSize 值,平衡單次請求資料量和網路往返次數,對效能至關重要。此外,應用程式伺服器與資料函式庫伺服器之間的物理距離也應盡量縮短。索引的使用在查詢最佳化中扮演重要角色,但並非所有情況都適用。當查詢結果集較小時,索引能有效提升效能;而當結果集佔比較大時,集合掃描可能更有效率。資料分佈的有序性也會影響索引的效能。MongoDB 的查詢最佳化器通常會選擇使用索引,但開發者可使用 hint 來強制指定集合掃描或特定索引。排序操作的最佳化包含建立索引和使用 allowDiskUse() 避免記憶體不足。

最佳化網路往返次數以提升 MongoDB 效能

在進行 MongoDB 資料查詢時,減少網路往返次數(network round trips)是最佳化效能的關鍵之一。減少網路延遲可以大幅提升應用程式的整體效能。

批次處理資料

在處理大量資料時,批次處理是一種有效的最佳化方法。假設我們需要從一個龐大的集合(collection)中取出每第100個檔案(document),有兩種實作方式:

方法一:逐一檢索需要的資料

const cursor = useDb.collection(mycollection).find()
.batchSize(1);
let counter = 0;
let i = 0;
while (await cursor.hasNext()) {
    i++;
    if (i % 100 === 0) {
        const doc = await cursor.next();
        counter++;
    }
}

方法二:批次檢索所有資料並在應用層過濾

const cursor = useDb.collection(mycollection).find()
.batchSize(10000);
let counter = 0;
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
    if (doc._id % divisor === 0) {
        counter++;
    }
}

內容解密:

  1. 在方法一中,我們設定 batchSize(1),表示每次只檢索一個檔案,這會導致大量的網路往返次數。
  2. 在方法二中,我們設定 batchSize(10000),表示每次檢索10000個檔案,雖然增加了單次請求的資料量,但大幅減少了網路往返次數。
  3. 當資料函式庫伺服器位於遠端時(如 MongoDB Atlas),方法二的效能遠優於方法一,因為減少了網路延遲的影響。

圖表分析:最佳化網路往返次數的效果

此圖示展示了兩種方法在本地伺服器和遠端伺服器(Atlas)上的效能比較。

應用架構最佳化

將應用程式伺服器佈署在與資料函式庫伺服器相同的資料中心或網路機架上,可以有效減少網路延遲。因此,在設計應用架構時,應盡量縮短應用程式與資料函式庫之間的物理距離。

索引與掃描的選擇

除了減少網路往返次數,使用索引也是最佳化查詢效能的重要手段。然而,並非所有情況下使用索引都是最佳選擇。

何時使用索引?

  • 當查詢結果集較小時,使用索引可以顯著提升查詢效能。
  • 當資料分佈有序(clustered)時,索引的效能更佳。

何時進行集合掃描?

  • 當查詢結果集佔比集合的大部分時,集合掃描可能比索引掃描更快。
  • 當資料未被快取(cache)時,集合掃描的效能可能較差。

圖表分析:索引與集合掃描的效能比較

此圖示展示了索引掃描和集合掃描在不同資料分佈情況下的效能比較。當資料分佈隨機時,約8%的查詢結果集佔比是索引掃描和集合掃描的效能轉折點;而當資料分佈有序時,索引掃描的效能優勢可以持續到約95%的查詢結果集佔比。

MongoDB 查詢調優:索引檢索與排序最佳化

在 MongoDB 中,查詢效能的最佳化至關重要,而索引的使用是提升查詢效率的關鍵因素之一。本文將探討 MongoDB 中的索引檢索、查詢最佳化器(Optimizer)以及排序操作的最佳化方法。

索引檢索的最佳實踐

雖然很難指定一個通用的閾值來決定何時使用索引檢索,但以下幾點是普遍適用的:

  • 當需要存取集合中的大部分或全部檔案時,全集合掃描(Full Collection Scan)通常是最快的存取路徑。
  • 當需要從大型集合中檢索單一檔案時,根據該屬性的索引將提供更快的檢索路徑。
  • 在這兩個極端之間,很難預測哪種存取路徑更快。

值得注意的是,索引檢索與集合掃描之間沒有固定的平衡點。如果只存取少數檔案,索引檢索是首選;如果存取了幾乎所有的檔案,則全集合掃描更為合適。在這兩個極端之間,實際效能可能會有所不同。

使用 Hints 覆寫最佳化器

MongoDB 的最佳化器結合了啟發式規則和實驗來決定最佳的存取路徑。通常,它會嘗試多種不同的計劃,然後選擇最適合特定查詢「形狀」的計劃。然而,最佳化器偏向於使用現有的索引。

例如,以下查詢檢索了集合中的所有檔案,因為沒有客戶出生於 1800 年代!儘管如此,MongoDB 仍然選擇了根據索引的路徑:

var exp = db.customers.explain('executionStats').find({ dateOfBirth: { $gt: new Date("1900-01-01T00:00:00+08:00") } });
mongoTuning.executionStats(exp);

內容解密:

  1. db.customers.explain('executionStats'):對 customers 集合啟用執行統計資訊的解釋計劃。
  2. find({ dateOfBirth: { $gt: new Date("1900-01-01T00:00:00+08:00") } }):查詢 dateOfBirth 大於指定日期的檔案。
  3. mongoTuning.executionStats(exp):輸出執行統計資訊,顯示查詢計劃的詳細資訊。

執行計劃顯示,IXSCAN 步驟檢索了集合中的所有 411,121 行資料,表明使用索引並不是最佳選擇。

我們可以透過新增 .hint({ $natural: 1 }) 來強制 MongoDB 使用集合掃描:

var exp = db.customers.explain('executionStats').find({ dateOfBirth: { $gt: new Date("1900-01-01T00:00:00+08:00") } }).hint({ $natural: 1 });
mongoTuning.executionStats(exp);

內容解密:

  1. .hint({ $natural: 1 }):強制 MongoDB 使用集合掃描來解析查詢。
  2. 輸出結果顯示 COLLSCAN(集合掃描)耗時 16 毫秒,掃描了 411,121 篇檔案。

我們也可以使用 hint 來指定 MongoDB 使用特定的索引。例如:

var exp = db.customers.explain('executionStats').find({ Country: 'India', dateOfBirth: { $gt: new Date("1990-01-01T00:00:00+08:00") } }).hint({ dateOfBirth: 1 });
mongoTuning.executionStats(exp);

內容解密:

  1. .hint({ dateOfBirth: 1 }):強制 MongoDB 使用 dateOfBirth 索引來執行查詢。
  2. 輸出結果顯示 IXSCAN 使用 dateOfBirth_1 索引,耗時 6 毫秒,掃描了 63,921 個鍵值。

排序操作的最佳化

如果查詢包含排序指令且沒有對排序屬性建立索引,MongoDB 必須先擷取所有資料,然後在記憶體中對結果進行排序。在所有資料排序完成之前,無法傳回查詢結果的第一行,因為在排序完成之前,我們無法確定排序結果中的第一個檔案。

因此,非索引排序通常被稱為阻塞排序(Blocking Sort)。阻塞排序可能會比索引排序更快,如果你需要整個排序後的資料集。然而,使用索引可以幾乎立即獲得前幾個檔案,在許多應用程式中,使用者希望快速看到排序後的資料的第一頁,而可能永遠不會翻閱整個資料集。在這些情況下,索引排序非常理想。

此外,如果阻塞排序耗盡記憶體,則會失敗。你可能會遇到類別似以下的錯誤:

Executor error during find command: OperationFailed: Sort operation used more than the maximum 33554432 bytes of RAM. Add an index, or specify a smaller limit.

從 MongoDB 4.4 版本開始,你可以透過在查詢中新增 allowDiskUse() 修飾符來執行「磁碟排序」。

db.customers.find().sort({ dateOfBirth: 1 }).allowDiskUse(true);

內容解密:

  1. .allowDiskUse(true):允許 MongoDB 在排序操作中使用磁碟空間,避免記憶體不足的問題。

建立索引以最佳化排序操作

如果在排序條件上建立索引,則執行計劃將只顯示 IXSCAN 和 FETCH:

var plan = db.customers.explain().find().sort({ dateOfBirth: 1 });
mongoTuning.quickExplain(plan);

內容解密:

  1. db.customers.explain():對 customers 集合啟用解釋計劃。
  2. .find().sort({ dateOfBirth: 1 }):查詢並按 dateOfBirth 升序排序。
  3. 輸出結果顯示 IXSCAN 使用 dateOfBirth_1 索引,接著進行 FETCH 操作。

對於同時包含篩選條件和排序條件的查詢,需要在篩選條件和排序條件上建立複合索引,並且篩選條件應該在前。例如:

db.customers.find({ Country: 'Japan' }).sort({ dateOfBirth: 1 });

為了最佳化這個查詢,需要在 { Country: 1, dateOfBirth: 1 } 上建立複合索引。

db.customers.createIndex({ Country: 1, dateOfBirth: 1 });

內容解密:

  1. db.customers.createIndex({ Country: 1, dateOfBirth: 1 }):在 CountrydateOfBirth 上建立複合索引,以支援篩選和排序操作。

MongoDB查詢調優:索引與排序的最佳實踐

索引設計對查詢效能的影響

在MongoDB中,正確的索引設計對於查詢效能至關重要。當我們需要對資料進行排序和篩選時,索引的建立順序會直接影響查詢的效率。

建立支援篩選與排序的索引

假設我們需要根據Country欄位進行篩選,並根據dateOfBirth進行排序。單純地建立一個只包含排序欄位的索引是不足夠的,例如:

db.customers.createIndex({dateOfBirth:1});

這種索引只能支援排序操作,而無法有效支援篩選條件。要同時支援篩選和排序,我們應該建立一個包含篩選條件欄位在前、排序欄位在後的複合索引:

db.customers.createIndex({Country:1, dateOfBirth:1});

索引對排序效能的影響分析

使用索引可以顯著提高取得前幾筆排序資料的效能,尤其是在資料量龐大的情況下。然而,如果需要取得所有排序後的資料,非索引排序(blocking sort)可能反而更快。

圖示:索引對排序效能的影響

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title MongoDB 查詢效能調優策略

package "資料庫架構" {
    package "應用層" {
        component [連線池] as pool
        component [ORM 框架] as orm
    }

    package "資料庫引擎" {
        component [查詢解析器] as parser
        component [優化器] as optimizer
        component [執行引擎] as executor
    }

    package "儲存層" {
        database [主資料庫] as master
        database [讀取副本] as replica
        database [快取層] as cache
    }
}

pool --> orm : 管理連線
orm --> parser : SQL 查詢
parser --> optimizer : 解析樹
optimizer --> executor : 執行計畫
executor --> master : 寫入操作
executor --> replica : 讀取操作
cache --> executor : 快取命中

master --> replica : 資料同步

note right of cache
  Redis/Memcached
  減少資料庫負載
end note

@enduml

此圖示說明瞭根據查詢需求選擇合適的排序方式。

內容解密:

  1. 查詢需求:首先評估查詢是需要全部資料還是僅需部分資料。
  2. 是否需要全部資料:根據需求決定使用非索引排序還是索引排序。
  3. 非索引排序:若需全部資料,考慮使用非索引排序,並可調整internalQueryExecMaxBlockingSortBytes引數來最佳化記憶體使用。
  4. 索引排序:若只需部分資料,使用索引排序並建立適當的複合索引。

選擇或建立適當的索引

建立一個理想的索引應該包含:

  1. 篩選條件中的所有欄位
  2. 排序條件中的欄位
  3. 投影(projection)中的欄位(如果實際可行)

最佳索引範例

db.customers.createIndex(
    {Country:1, 'views.title':1, LastName:1, Phone:1},
    {name:'CntTitleLastPhone_ix'}
);

執行以下查詢時,可以觀察到最佳化的執行計畫:

var exp = db.customers.explain('executionStats')
    .find(
        { Country: 'Japan', 'views.title': 'MUSKETEERS WAIT' },
        { Phone: 1, _id: 0 }
    )
    .sort({ LastName: 1 });
mongoTuning.executionStats(exp);

輸出結果顯示了IXSCANPROJECTION_COVERED階段,表明查詢被索引完全覆寫。

內容解密:

  1. IXSCAN:表示使用了索引掃描。
  2. PROJECTION_COVERED:表示查詢被索引完全覆寫,無需存取原始檔案。
  3. 執行計畫分析:透過分析執行計畫,可以確認查詢是否有效利用了索引。

篩選策略

不等於($ne)條件的處理

當使用$ne運算元時,MongoDB仍然可以利用索引來解析查詢。然而,如果$ne條件匹配了大部分資料集,使用索引可能並不是最佳選擇。

範例查詢:

var exp = db.enron_messages.explain('executionStats')
    .find({ 'headers.From': { $ne: 'eric.bass@enron.com' } });
mongoTuning.executionStats(exp);

輸出結果顯示,雖然查詢使用了索引,但仍需要進行大量的檔案擷取(FETCH)操作。

內容解密:

  1. IXSCAN:MongoDB利用了headers.From上的索引進行掃描。
  2. FETCH:由於查詢條件並未被索引完全覆寫,因此需要進一步擷取檔案。
  3. 效能考量:若$ne條件匹配大量資料,可能導致效能不佳,此時可考慮強制進行集合掃描(collection scan)。

MongoDB 查詢調優深入解析

在 MongoDB 的查詢最佳化過程中,瞭解不同查詢條件下的索引使用和執行效率至關重要。本文將探討 $ne$or$in、陣列查詢和正規表示式查詢的效能特點,並提供最佳化建議。

$ne 查詢與索引掃描

當使用 $ne 查詢條件時,MongoDB 通常會選擇索引掃描(IXSCAN)。然而,如果 $ne 條件涵蓋了大多數資料,索引掃描的效率可能還不如集合掃描(COLLSCAN)。這是因為索引掃描需要多次存取索引和檔案,而集合掃描只需遍歷整個集合一次。

####效能比較

實驗結果顯示,當 $ne 條件涵蓋大部分資料時,集合掃描明顯快於索引掃描。相反,當 $ne 條件過濾出較少資料時,索引掃描表現更佳。

// 查詢條件涵蓋大部分資料
var exp = db.iotData.explain('executionStats').find({a: {$gt: 0}});
mongoTuning.executionStats(exp);
// 結果:IXSCAN (ms:83 keys:1000000) + FETCH (ms:193 docs:1000000)

// 使用集合掃描
var exp = db.iotData.explain('executionStats').find({a: {$gt: 990}}).hint({$natural: 1});
mongoTuning.executionStats(exp);
// 結果:COLLSCAN (ms:1 docs:1000000)

內容解密:

  1. 查詢條件涵蓋大部分資料:當查詢範圍廣泛時,MongoDB 預設使用索引掃描,但此時集合掃描更有效率。
  2. 使用集合掃描:透過 hint({$natural: 1}) 強制使用集合掃描,顯著提升效能。
  3. 範圍查詢最佳化:僅在查詢範圍狹窄時使用索引;否則,集合掃描更為高效。

$or$in 查詢最佳化

對於單一屬性上的 $or$in 查詢,MongoDB 的處理方式相似。然而,當 $or 涉及多個屬性時,情況變得複雜。如果所有屬性都有索引,MongoDB 會對每個屬性進行索引掃描,然後合併結果。

多屬性 $or 查詢

var exp = db.enron_messages.explain('executionStats').find({
  $or: [
    { 'headers.To': 'eric.bass@enron.com' },
    { 'headers.From': 'eric.bass@enron.com' }
  ]
});
mongoTuning.executionStats(exp);
// 結果:IXSCAN (headers.From_1) + IXSCAN (headers.To_1) + OR + FETCH

內容解密:

  1. 多屬性 $or 查詢:MongoDB 對每個屬性進行獨立的索引掃描,並合併結果。
  2. 索引的重要性:確保 $or 中的所有屬性都有索引,以避免退化為集合掃描。
  3. 效能最佳化:為所有涉及的屬性建立索引,以充分利用索引掃描的優勢。

陣列查詢與索引

MongoDB 支援對陣列元素的豐富查詢操作,並且可以透過索引高效解析。例如,查詢特定收件人的電子郵件可以使用陣列索引。

陣列查詢範例

var exp = db.enron_messages.explain('executionStats').find({
  'headers.To': {
    $eq: ['jim.schwieger@enron.com', 'thomas.martin@enron.com']
  }
});
mongoTuning.executionStats(exp);
// 結果:IXSCAN (headers.To_1) + FETCH

內容解密:

  1. 陣列查詢:MongoDB 可以利用索引來高效查詢陣列元素。
  2. 索引支援:相同的索引可以支援 $eq$all 操作。
  3. 限制:像 $size 這樣的運算子無法從索引中受益,可能導致集合掃描。

正規表示式查詢

正規表示式允許對字串進行進階匹配。然而,其效能取決於表示式的複雜度和索引的使用。

正規表示式查詢範例

var exp = db.customers.explain('executionStats').find({LastName: /HARRIS/});
mongoTuning.executionStats(exp);

內容解密:

  1. 正規表示式查詢:可用於進階字串匹配。
  2. 效能考量:正規表示式的效能取決於其複雜度和是否能有效利用索引。
  3. 最佳化建議:盡可能簡化正規表示式,並確保相關欄位有適當的索引。

圖表說明

此圖示展示了不同查詢條件下,索引掃描和集合掃描的效能比較。

  • 當查詢範圍廣泛時,集合掃描優於索引掃描。
  • 當查詢範圍狹窄時,索引掃描表現更佳。

透過理解這些原則,並結合實際應用場景進行最佳化,可以有效提升 MongoDB 資料函式庫的整體效能。