1. 程式人生 > >Mongo 聚合框架優化-Aggregate(四)

Mongo 聚合框架優化-Aggregate(四)

四 管道優化

1、管道序列優化

 1)$match操作符應該儘量出現在管道的前面
  $match操作符出現在管道的前面時,可以提早過濾文件,加快聚合速度。而且其只有出現在管道的最前面,才可以使用索引來加快查詢。
 2)管道序列
  應該儘量在管道的開始階段(執行”$project”、”$group”或者”$unwind”操作之前)就將盡可能多的文件和欄位過濾掉
 3)$sort +$match
  當$sort後面跟著$match操作符時,執行聚合時優化器會將$match管道操作符放在$sort之前,以減少排序的物件數量。

{ $sort: { age : -1 } },{ $match
: { status: 'A'
} }

  優化後

{ $match: { status: 'A' } },{ $sort: { age : -1 } }

4)$skip + $limit
當$skip後面跟著$limit操作符時,$limit操作符會移動到$skip之前,並且會把$skip的值加在$limit的值上。
{ $skip: 10 },{ $limit: 5 }優化後{ $limit: 15 },{ $skip: 10 }
5)$redact + $match
當$redact後面跟著$match操作符時,聚合操作有時候會把部分$match的匹配條件加到$redact之前,這樣可以利用索引查詢文件並縮小進入管道文件的數量。

{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
        { $match: { year: 2014, category: { $ne: "Z" } } }

  優化後

{ $match: { year: 2014 } },
        { $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
        { $match
: { year: 2014, category: { $ne: "Z" } } }

6)$project + $skip or $project + $limit
當$project後面跟著$skip或者$limit操作符時,會將其移到$project之前

{ $sort: { age : -1 } },
{ $project: { status: 1, name: 1 } },
{ $limit: 5 }

  優化後

{ $sort: { age : -1 } },
{ $limit: 5 }
{ $project: { status: 1, name: 1 } },

2、管道合併優化

 1)$sort + $limit
  該優化只有在allowDiskUse選項為true且要排序的文件數超過了聚合的記憶體限制
  當$sort操作符緊隨其後的是$limit時,優化器會把$limit合併入$sort操作符中。
 2)$limit + $limit
  當$limit操作符緊隨其後的是$limit時,這兩個階段可以捨棄值比較大的從而合併為一個$limit操作。
  { $limit: 100 },{ $limit: 10 } 優化後 { $limit: 10 }
 3)$skip + $skip
  當$skip操作符緊隨其後的是$skip操作符時,兩個階段可以合併一個$skip操作,合併後的值為二者之和。
  { $skip: 5 },{ $skip: 2 } 優化後 { $skip: 7 }
 4)$match + $match
  兩個$match操作連一起時,也可以用$and合併為一個操作

{ $match: { year: 2014 } },{ $match: { status: "A" } }

優化後

{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }

5)$lookup + $unwind
當$unwind緊隨在$lookup之後且作用的欄位為$lookup所關聯後的欄位時,優化器會將$unwind合併入$lookup操作中。

{
  $lookup: {
    from: "otherCollection",
    as: "resultingArray",
    localField: "x",
    foreignField: "y"
  }
},
{ $unwind: "$resultingArray"}

優化後

{
  $lookup: {
    from: "otherCollection",
    as: "resultingArray",
    localField: "x",
    foreignField: "y",
    unwinding: { preserveNullAndEmptyArrays: false }
  }
}

3、示例

 1)$sort + $skip + $limit序列

{ $sort: { age : -1 } },{ $skip: 10 },{ $limit: 5 }

  優化為

{ $sort: { age : -1 } },{ $limit: 15 }{ $skip: 10 }

2)$limit + $skip + $limit + $skip

{ $limit: 100 },{ $skip: 5 },{ $limit: 10 },{ $skip: 2 }

  首先優化為

{ $limit: 100 },{ $limit: 15},{ $skip: 5 },{ $skip: 2 }

  最終優化為

{ $limit: 15 },{ $skip: 7 }

五 優化案例

1、查詢使用者訂單資訊

 1)資料
  訂單集合

{ "_id" : ObjectId("59956e30b71978132313a6fc"), "orderId" : "1", "createTime" : ISODate("2017-08-17T10:21:36.677Z"), "status" : 1, "uid" : "39054854" }
{ "_id" : ObjectId("59956e30b71978132313a6fd"), "orderId" : "2", "createTime" : ISODate("2017-08-17T10:21:36.711Z"), "status" : 2, "uid" : "39054855" }
{ "_id" : ObjectId("59956e30b71978132313a6fe"), "orderId" : "3", "createTime" : ISODate("2017-08-17T10:21:36.712Z"), "status" : 3, "uid" : "39054856" }
{ "_id" : ObjectId("59956e30b71978132313a6ff"), "orderId" : "4", "createTime" : ISODate("2017-08-17T10:21:36.726Z"), "status" : 2, "uid" : "39054857" }
{ "_id" : ObjectId("59956e32b71978132313a700"), "orderId" : "5", "createTime" : ISODate("2017-08-17T10:21:38.068Z"), "status" : 1, "uid" : "39054858" }

  使用者集合

{ "_id" : ObjectId("59956e3cb71978132313a701"), "uid" : "39054857", "name" : "Jack", "sex" : 1, "mobile" : "18745968745" }
{ "_id" : ObjectId("59956e3cb71978132313a702"), "uid" : "39054854", "name" : "Tony", "sex" : 0, "mobile" : "18745968746" }
{ "_id" : ObjectId("59956e3cb71978132313a703"), "uid" : "39054856", "name" : "Keven", "sex" : 0, "mobile" : "18745968747" }
{ "_id" : ObjectId("59956e3cb71978132313a704"), "uid" : "39054858", "name" : "Jake", "sex" : 0, "mobile" : "18745968748" }
{ "_id" : ObjectId("59956e3cb71978132313a705"), "uid" : "39054857", "name" : "Seven", "sex" : 1, "mobile" : "18745968749" }
{ "_id" : ObjectId("59956e3db71978132313a706"), "uid" : "39054854", "name" : "Lily", "sex" : 1, "mobile" : "18745968742" }

 2)分頁查詢17年8月18號之後性別為1的訂單及其使用者資訊

db.order.aggregate([
    {"$lookup":{"from":"user","localField":"uid","foreignField":"uid","as":"u"}},
    {"$match":{"createTime":{"$gt":ISODate("2017-08-18T00:00:00.677Z")},"u.sex":1}},
    {"$skip":0},
    {"$limit":5}])

  首先因為訂單資訊和使用者資訊是分別存在兩個集合中,所以需要表聯合,mongo中的$lookup即mysql的join。
  其次,經過$lookpup管道之後,使用者的資訊會以陣列的形式存在u欄位下:”u” : [ { “_id” : ObjectId(“59956e3db71978132313a706”), “uid” : “39054855”, “name” : “Lily”, “sex” : 1, “mobile” : “18745968742” } ] ,因為使用者資訊是唯一的,所以陣列中始終只會有一個值,用$unwind將其分割嵌入,作為u欄位的子文件。
  然後根據用$match將結果根據查詢條件進行篩選,使用者資訊因為是以子文件的形式存在,所以需要以“u.sex”來查詢。
  最後根據$limit和$skip進行分頁。
 3)優化
  執行一段時間發現該查詢比較耗時,但是性別和時間都已建過索引。檢視執行計劃,該查詢沒用使用到索引,索引只會在訪問原始document時有效,聚合查詢時如果要使用索引,$match操作必須放在管道首位。因為性別需要聯合表後才可以查詢,所以將$match操作分割,做如下優化:

db.order.aggregate([
    {"$match":{"createTime":{"$gt":ISODate("2017-08-18T00:00:00.677Z")}}},
    {"$lookup":{"from":"user","localField":"uid","foreignField":"uid","as":"u"}},
    {"$unwind":"$u"},
    {"$match":{"u.sex":1}},
    {"$limit":5},
    {"$skip":0}])

  order集合中的欄位作為查詢條件時,都可以放在管道首位,可以使用索引來提高效能,而關聯的表中的欄位因為需要$lookup和$unwind操作,所以無法置首而使用索引。如果關聯的表中欄位如果是唯一或者很少重複,如身份證號之類的查詢條件,可以考慮先用該類欄位過濾,然後根據過濾後的資料去查詢order表中的資料,在記憶體中組裝成想要的結果,但是如果該類欄位重複可能較多,如姓名,則不能用次辦法。
  以上資料為測試資料,因系統中只有一個此類查詢,系統各個表的欄位不固定,所以只能優化此查詢而非換庫,如果系統中存在大量的業務操作,建議使用關係型資料庫。