返回文章列表

MongoDB 查詢與聚合管道調優實務

本文探討 MongoDB 查詢和聚合管道的效能調優技巧,涵蓋索引使用、正規表示式最佳化、不區分大小寫查詢、`$exists` 操作、集合掃描最佳化、聚合管道執行計劃分析、輔助指令碼使用、管道順序最佳化、自動管道最佳化、階段合併、投影最佳化、`$match` 重排序以及多集合聯接最佳化等關鍵技術,旨在幫助開發者提升

資料函式庫 效能調校

MongoDB 查詢效能的提升對於應用程式至關重要。除了建立適當的索引外,還需要針對不同型別的查詢進行特定的最佳化。例如,使用正規表示式查詢時,利用 ^ 符號錨定字串開頭可以有效減少索引掃描的範圍,從而提升查詢效率。對於不區分大小寫的查詢,建立對應的索引可以避免全索引掃描。$exists 操作雖然可以使用索引,但可能導致大量索引掃描,此時可以考慮使用稀疏索引進行最佳化。此外,集合掃描的效率也需要關注,透過垂直分割、分片和壓縮集合等手段可以減少集合大小,進而提升掃描效能。聚合管道方面,理解執行計劃至關重要,透過 explain() 命令和輔助指令碼可以分析各個階段的耗時,找出效能瓶頸。調整管道順序,盡早過濾資料,並利用 MongoDB 的自動最佳化功能,可以有效提升聚合管道的效率。同時,階段合併、投影最佳化和 $match 階段的重排序也是重要的最佳化手段。在多集合聯接的場景下,確保相關欄位有索引是提升效能的關鍵。

MongoDB 查詢調優

提升查詢效能的關鍵

在 MongoDB 中,查詢效能的最佳化至關重要。適當的索引使用和查詢陳述式的設計可以大幅提升資料檢索的效率。

正規表示式查詢的最佳化

使用正規表示式進行查詢時,若未正確設定,可能導致效能問題。以下是一個範例:

mongo> var exp = db.customers.explain('executionStats').find({LastName: /HARRIS/});
mongo> mongoTuning.executionStats(exp);
1 IXSCAN ( LastName_1 ms:9 keys:410071)
2 FETCH ( ms:12 docs:1365)
Totals: ms: 273 keys: 410071 Docs: 1365

內容解密:

  • 上述查詢掃描了全部 410,071 個索引專案,效率低下。
  • 使用 ^ 符號錨定字串開頭,可提升查詢效率。
mongo> var exp = db.customers.explain('executionStats').find({LastName: /^HARRIS/});
mongo> mongoTuning.executionStats(exp);
1 IXSCAN ( LastName_1 ms:0 keys:1366)
2 FETCH ( ms:0 docs:1365)
Totals: ms: 3 keys: 1366 Docs: 1365

內容解密:

  • 使用 ^ 後,僅掃描 1,366 個索引專案,大幅提升查詢效率。
  • 這是因為索引掃描變得更有效率。

不區分大小寫的查詢最佳化

進行不區分大小寫的查詢時,若未建立相應的索引,可能導致全索引掃描:

mongo> var e = db.customers.explain('executionStats').find({ LastName: /^Harris$/i }, {});
mongo> mongoTuning.executionStats(e);
1 IXSCAN ( LastName_1 ms:4 keys:410071)
2 FETCH ( ms:6 docs:635)
Totals: ms: 282 keys: 410071 Docs: 635

內容解密:

  • 上述查詢因未使用區分大小寫的索引,導致掃描全部索引專案。
  • 建立不區分大小寫的索引可解決此問題。

$exists 查詢的最佳化

$exists 操作可用索引最佳化:

mongo> var exp = db.customers.explain('executionStats').find({updateFlag: {$exists:true}});
mongo> mongoTuning.executionStats(exp);
1 IXSCAN ( updateFlag_1 ms:11 keys:411121)
2 FETCH ( ms:32 docs:411121)
Totals: ms: 525 keys: 411121 Docs: 411121

內容解密:

  • $exists 操作可利用索引,但可能導致大量索引掃描。
  • 可考慮建立稀疏索引以最佳化此類別查詢。
mongo> db.customers.createIndex({updateFlag:1},{sparse:true});
{
  "createdCollectionAutomatically": false,
  "numIndexesBefore": 1,
  "numIndexesAfter": 2,
  "ok": 1
}

內容解密:

  • 稀疏索引僅包含存在指定欄位的檔案,可提升 $exists:true 查詢效率。
  • 但無法最佳化 $exists:false 查詢。

最佳化集合掃描

儘管索引是最佳化查詢的重要手段,但在某些情況下,集合掃描可能更有效率。減少集合大小是提升集合掃描效能的主要方法:

  1. 垂直分割:將大且不常存取的元素移至另一個集合。
  2. 分片:透過多個叢集協作掃描,提升效能。
  3. 壓縮集合:執行 compact 命令以回收浪費的空間,但需注意此操作會阻塞相關資料函式庫的操作。

調整聚合管道的效能最佳化

在開始使用MongoDB時,大多數開發人員會從其他資料函式庫熟悉的基本CRUD操作(建立、讀取、更新、刪除)開始。插入、查詢、更新和刪除操作確實構成了大多數應用程式的主幹。然而,幾乎所有應用程式都存在複雜的資料檢索和操作需求,這些需求超出了基本的MongoDB命令所能實作的範圍。

MongoDB的find()命令非常靈活且易於使用,但聚合框架允許您將其提升到新的高度。聚合管道可以完成find()操作所能做的一切,甚至更多。正如MongoDB官方在部落格、市場宣傳材料甚至T恤上所說的那樣:聚合是新的find()

聚合管道允許您簡化應用程式碼,減少可能需要多次find()操作和複雜資料操作的邏輯。如果使用得當,單個聚合操作可以取代多個查詢及其相關的網路往返時間。

瞭解聚合管道的執行計劃

要有效地調整聚合管道,我們必須首先能夠有效地識別哪些聚合需要調整以及可以改進哪些方面。與find()查詢一樣,explain()命令是我們的最佳工具。要檢查查詢的執行計劃,我們在集合名稱後新增.explain()方法。例如,要解釋一個find(),我們可以使用以下命令:

db.customers.explain().find(
    { Country: 'Japan', LastName: 'Smith' },
    { _id: 0, FirstName: 1, LastName: 1 }
).sort({ FirstName: -1 }).limit(3);

我們可以用相同的方式解釋一個聚合管道:

db.customers.explain().aggregate([
    { $match: { Country: 'Japan', LastName: 'Smith' } },
    { $project: { _id: 0, FirstName: 1, LastName: 1 } },
    { $sort: { FirstName: -1 } },
    { $limit: 3 }
]);

然而,find()命令和聚合管道的執行計劃之間存在顯著差異。對於聚合管道,queryPlanner物件現在位於一個名為stages的陣列中,該陣列包含每個聚合階段作為單獨的物件。

執行計劃範例

{
    "stages": [
        {"$cursor": {
            "queryPlanner": {
                "winningPlan": {
                    "stage": "PROJECTION_SIMPLE",
                    "inputStage": {
                        "stage": "FETCH",
                        "inputStage": {
                            "stage": "IXSCAN"
                        }
                    }
                }
            }
        }},
        {"$sort": {
            "sortKey": { "FirstName": -1 },
            "limit": 3
        }}
    ]
}

在聚合管道的執行計劃中,queryPlanner階段揭示了將資料引入管道所需的初始資料存取操作。這通常代表支援初始$match操作的操作,或者如果沒有指定$match條件,則代表檢索集合中所有資料的集合掃描。

使用輔助指令碼簡化執行計劃分析

我們編寫了一個輔助指令碼來簡化聚合執行計劃的解釋,該指令碼包含在我們的調優指令碼套件中。mongoTuning.aggregationExecutionStats()方法將提供每個步驟所花費時間的頂級摘要。

使用範例

var exp = db.customers.explain('executionStats').aggregate([
    { $match: { "Country": { $eq: "Japan" } } },
    { $group: { _id: { "City": "$City" }, "count": { $sum: 1 } } },
    { $sort: { "_id.City": -1 } },
    { $limit: 10 }
]);

mongoTuning.aggregationExecutionStats(exp);

輸出結果如下:

1 IXSCAN ( Country_1_LastName_1 ms:0 keys:21368 nReturned:21368)
2 FETCH ( ms:13 docsExamined:21368 nReturned:21368)
3 PROJECTION_SIMPLE ( ms:15 nReturned:21368)
4 $GROUP ( ms:70 returned:31)
5 $SORT ( ms:70 returned:10)
Totals: ms: 72 keys: 21368 Docs: 21368

程式碼解密:

  1. IXSCAN:索引掃描,使用Country_1_LastName_1索引進行查詢,耗時0毫秒,檢查了21368個鍵,傳回21368個檔案。
  2. FETCH:根據索引掃描的結果取出檔案,耗時13毫秒,檢查了21368個檔案,傳回21368個檔案。
  3. PROJECTION_SIMPLE:簡單投影操作,耗時15毫秒,傳回21368個檔案。
  4. $GROUP:分組操作,按城市進行分組並計算數量,耗時70毫秒,傳回31個檔案。
  5. $SORT:排序操作,按城市逆序排序,耗時70毫秒,傳回10個檔案。

透過分析執行計劃和各階段的耗時,我們可以找出聚合管道的效能瓶頸,從而進行最佳化。

最佳化聚合管道的順序

聚合操作是由一系列階段構成的,這些階段以陣列的形式表示,並按照從前到後的順序執行。每個階段的輸出都會傳遞給下一個階段進行處理,而初始輸入則是整個集合。這些階段的順序性質使得聚合操作被稱為管道:資料在管道中流動,在每個階段被過濾和轉換,直到最終以結果的形式離開管道。最佳化這些管道最簡單的方法是盡早減少資料量;這將減少每個後續步驟的工作量。邏輯上來說,聚合操作中執行最多工作的階段應該在盡可能少的資料上進行操作,盡可能多的過濾應該在早期階段進行。

重點提示:盡早過濾,頻繁過濾!

在構建聚合管道時,盡早過濾資料可以降低 MongoDB 的整體資料處理負載。

MongoDB 會自動重新排列管道中的操作順序以最佳化效能——我們將在下一節中看到一些最佳化的例子。然而,對於複雜的管道,您可能需要自己設定順序。

有一種情況是自動重新排序無法實作的,即使用 $lookup 的聚合操作。$lookup 階段允許您連線兩個集合。如果您正在連線兩個集合,您可能會在連線之前或之後進行過濾。在這種情況下,嘗試在連線操作之前減少資料的大小非常重要,因為對於每個傳入 $lookup 操作的檔案,MongoDB 都必須嘗試在另一個集合中找到匹配的檔案。我們在連線之前過濾掉的每個檔案都會減少需要發生的查詢次數。這是一個明顯但關鍵的最佳化。

讓我們看一個生成“前 5 名”產品購買清單的聚合操作例子:

db.lineitems.aggregate([
  { $group: { _id: { "orderId": "$orderId", "prodId": "$prodId" }, "itemCount-sum": { $sum: "$itemCount" } } },
  { $lookup: { from: "orders", localField: "_id.orderId", foreignField: "_id", as: "orders" } },
  { $lookup: { from: "customers", localField: "orders.customerId", foreignField: "_id", as: "customers" } },
  { $lookup: { from: "products", localField: "_id.prodId", foreignField: "_id", as: "products" } },
  { $sort: { "count": -1 } },
  { $limit: 5 },
], { allowDiskUse: true });

內容解密:

  1. $group 階段:根據 orderIdprodId 分組,並計算每組的 itemCount 總和。
  2. $lookup 階段:分別與 orderscustomersproducts 集合進行連線操作,以取得相關資訊。
  3. $sort$limit 階段:對結果進行排序並限制輸出為前 5 名。

這個聚合管道相當大。事實上,如果沒有 allowDiskUse:true 標誌,它將產生記憶體不足的錯誤;我們將在本章後面討論為什麼會發生這個錯誤。

請注意,我們在對結果進行排序和限制輸出之前就連線了 orderscustomersproducts。結果是,我們必須對每個 lineItem 執行所有三個連線查詢操作。

我們可以——並且應該——將 $sort$limit 直接放在 $group 操作之後:

db.lineitems.aggregate([
  { $group: { _id: { "orderId": "$orderId", "prodId": "$prodId" }, "itemCount-sum": { $sum: "$itemCount" } } },
  { $sort: { "count": -1 } },
  { $limit: 5 },
  { $lookup: { from: "orders", localField: "_id.orderId", foreignField: "_id", as: "orders" } },
  { $lookup: { from: "customers", localField: "orders.customerId", foreignField: "_id", as: "customers" } },
  { $lookup: { from: "products", localField: "_id.prodId", foreignField: "_id", as: "products" } }
], { allowDiskUse: true });

內容解密:

  1. 最佳化後的 $sort$limit 階段:在連線操作之前進行排序和限制輸出,大大減少了需要進行連線操作的檔案數量。
  2. 效能改進:透過將 $sort$limit 提前幾行,我們建立了一個更高效、更可擴充套件的解決方案。

自動管道最佳化

MongoDB 將對聚合管道進行一些最佳化以提高效能。具體的最佳化措施會根據版本的不同而有所變化,並且當透過驅動程式或 MongoDB shell 執行聚合操作時,沒有明顯的跡象表明最佳化已經發生。事實上,您唯一能確定的方法是使用 explain() 檢查查詢計劃。

讓我們執行一些聚合操作,並觀察 MongoDB 如何使用 explain() 來改進管道。

重點提示:注意聚合管道的順序,以盡早消除檔案。

在管道中盡早消除資料,可以減少後續管道的工作量。

示例:觀察 MongoDB 的最佳化效果

考慮以下糟糕構建的聚合管道:

var explain = db.listingsAndReviews.explain("executionStats").aggregate([
  { $match: { "property_type": "House" } },
  { $match: { "bedrooms": 3 } },
  { $limit: 100 },
  { $limit: 5 },
  { $skip: 3 },
  { $skip: 2 }
]);

內容解密:

  1. 多個 $match$limit$skip 階段:這些階段可以合併而不會改變結果。
  2. MongoDB 的最佳化效果:MongoDB 將多個階段合併為更少的操作,從而提高了效能。

透過檢查查詢計劃,我們可以看到 MongoDB 將六個步驟合併為只有三個操作。

MongoDB還可以執行其他一些聰明的合併操作,例如將 $unwind 合併到 $lookup 中。

MongoDB 聚合管道最佳化技術深入解析

聚合管道最佳化的核心原理

MongoDB 的聚合管道(Aggregation Pipeline)是資料處理的核心工具之一,其最佳化對於提升系統效能至關重要。MongoDB 內建的最佳化器能夠自動對聚合管道進行多項最佳化,包括階段合併、階段移動和投影最佳化等,以提升查詢效能。

階段合併的最佳化實踐

階段合併是 MongoDB 聚合管道最佳化的一個重要方面。當連續的階段可以合併為單一階段時,MongoDB 能夠減少中間結果的產生,從而提升效能。例如,$lookup$unwind 的合併能夠避免產生龐大的中間檔案。

db.collection.aggregate([
  {
    $lookup: {
      from: "comments",
      localField: "email",
      foreignField: "email",
      as: "comments"
    }
  },
  { $unwind: "$comments" }
]);

內容解密:

  1. $lookup 階段用於將 collectioncomments 集合進行關聯,依據 email 欄位進行匹配。
  2. $unwind 階段將 $lookup 結果中的陣列展開為單一檔案。
  3. 合併後的 $lookup$unwind 成為單一執行階段,避免了龐大中間檔案的產生。

同樣地,$sort$limit 的合併使得 $sort 只需維護有限數量的檔案,而不是整個輸入流。

db.users.aggregate([
  { $sort: { year: -1 } },
  { $limit: 1 }
]);

內容解密:

  1. $sort 階段根據 year 欄位進行降序排序。
  2. $limit 階段限制輸出結果為一筆資料。
  3. 合併後的單一階段最佳化了排序操作,使其只需維護一筆資料。

投影最佳化提升效能

MongoDB 的最佳化器能夠自動在聚合管道中加入投影(Projection),以移除未使用的欄位,減少資料傳輸量。

db.customers.aggregate([
  { $match: { Country: 'Japan' } },
  { $group: { _id: { City: '$City' } } }
]);

內容解密:

  1. $match 階段篩選出 Country 為 ‘Japan’ 的檔案。
  2. $group 階段根據 City 欄位進行分組。
  3. MongoDB 自動加入投影階段,移除未使用的欄位,減少資料處理量。

$match 階段的重排序最佳化

MongoDB 能夠將 $match 階段提前到投影階段之前,以減少後續階段需要處理的檔案數量。

db.customers.aggregate([
  {
    $group: {
      _id: '$Country',
      numCustomers: { $sum: 1 }
    }
  },
  {
    $match: {
      $or: [
        { _id: 'Netherlands' },
        { _id: 'Sudan' },
        { _id: 'Argentina' }
      ]
    }
  }
]);

內容解密:

  1. 原始的 $group$match 順序會導致先對所有檔案進行分組,再進行篩選。
  2. MongoDB 將 $match 提前,利用 Country 索引加速篩選過程。
  3. 這種最佳化大大減少了需要被分組的檔案數量,提升了查詢效能。

多集合聯接的最佳化實踐

在進行多集合聯接(Multi-collection Joins)時,特別是在使用 $lookup 時,確保相關欄位有索引是至關重要的,以避免效能問題。

db.customers.aggregate([
  {
    $lookup: {
      from: "orders",
      localField: "_id",
      foreignField: "customerId",
      as: "orders"
    }
  }
]);

內容解密:

  1. $lookup 用於將 customersorders 集合進行聯接,依據 _idcustomerId 欄位。
  2. 為了提升效能,必須在 orders 集合的 customerId 欄位建立索引。
  3. 使用索引能夠顯著提高 $lookup 的效能,避免全表掃描。