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表中的資料,在記憶體中組裝成想要的結果,但是如果該類欄位重複可能較多,如姓名,則不能用次辦法。
以上資料為測試資料,因系統中只有一個此類查詢,系統各個表的欄位不固定,所以只能優化此查詢而非換庫,如果系統中存在大量的業務操作,建議使用關係型資料庫。