四 管道优化

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