四 管道优化
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表中的数据,在内存中组装成想要的结果,但是如果该类字段重复可能较多,如姓名,则不能用次办法。
以上数据为测试数据,因系统中只有一个此类查询,系统各个表的字段不固定,所以只能优化此查询而非换库,如果系统中存在大量的业务操作,建议使用关系型数据库。
参考链接:https://docs.mongodb.com/manual/reference/operator/aggregation/
http://shift-alt-ctrl.iteye.com/blog/2259216