mongodb聚合查询优化_Mongodb聚合慢查询优化

最近leader让我针对线上一个查询进行优化。举一个相似的例子,以如下的书籍信息文档为例,需要注意的是一本书可能会有多个作者。

业务上的需求是获取指定的50个作者最新出版的的书籍。在优化前,原先的代码逻辑是每次访问数据库获取一个作者参与的图书,按照时间倒序排序,分页取第一条信息。那么完成这个需求需要访问50次mongodb,生产环境这个业务总共耗时5s。leader让我优化到1S内。

这个查询的瓶颈主要是在IO次数上,是否可以访问一次数据库就能得到结果呢。笔者最先想到的是group by操作。以作者id进行group by,但是难点在于每个分组内部都要进行排序,然后取每个分组的第一条数据。Mongodb的分组聚合查询不支持组内排序的操作。

经过思考笔者采用如下的设计,在分组前先对时间进行倒序排序,随后再进行分组,这样获取的每个分组都是按照时间倒序排序的

db.book.aggregate([

{

"$match": {

"authors": {

"$in": ["author1", "author2"]

},

"time": {

"$gte": "20190101",

"$lte": "20191201"

}

}

},

{

"$unwind": "$authors"

},

{

"$match": {

"authors": {

"$in": ["author1", "author2"]

}

}

},

{

"$sort": {

"time": - 1

}

},

{

"$group": {

"_id": "$authors",

"title": {

"$first": "$title"

},

"time": {

"$first": "$time"

},

"book_id": {

"$first": "$_id"

}

}

}

],{"allowDiskUse":true})

Mongodb规定了管道聚合的返回数据不能超过16M,超过16M就会报异常错误,解决方案是允许使用磁盘帮助缓存聚合的中间结果。上述这个查询总共需要耗时7.5s,这样的聚合优化比优化前耗时时间还要长。

聚合慢查询优化

使用explain工具分析上述的查询

发现mongodb没有使用索引,mongodb进行了全表扫描。为了能让mongodb使用authors字段进行分组,同时使用time字段进行筛选和排序,笔者添加了authors和time的复合索引。为了让mongodb利用复合索引排序,同时考虑到author字段是否排序对业务结果没有影响,笔者改写了mongodb查询排序的逻辑,笔者将排序字段修改如下

{

"$sort": {

"authors":-1,"time": - 1

}

}

笔者认为改写后的查询语句的执行过程如下,mongodb首先会使用复合索引的authors字段过滤数据,然后使用time字段过滤并且排序数据。笔者再次执行查询,发现查询速度变得更慢了,需要十几秒才能输出。笔者使用explain分析慢查询,发现mongodb仍然没有使用authors和time的复合索引。

带着疑惑笔者查询了mongodb官方文档,官方文档提到了两个优化策略

前置match

mongodb官方文档提到,要将match放在聚合查询第一个阶段

db.book.aggregate([

{

"$match": {

"authors": {

"$in": ["author1", "author2"]

},

"time": {

"$gte": "20190101",

"$lte": "20191201"

}

}

}

···

但是这样的查询结果输出和笔者想要的查询结果输出不同,原先笔者的查询意图是只输出作者为author1,author2的出版的最新一本书的查询结果分别是book1,book2,但是如果book1和book2有合著作者author3,author4,那么author3和author4也会一并输出到结果集中,虽然我们也可以在应用Service层过滤出我们想要的结果集,但是传输数据量增多,查询次数多的时候,对网络也会有负载,笔者再次改写查询为如下。

db.book.aggregate([

{

"$match": {

"authors": {

"$in": ["author1", "author2"]

},

"time": {

"$gte": "20190101",

"$lte": "20191201"

}

}

},

{

"$unwind": "$authors"

},

{

"$match": {

"authors": {

"$in": ["author1", "author2"]

}

}

},

{

"$group": {

"_id": "$authors",

"title": {

"$first": "$title"

},

"time": {

"$first": "$time"

},

"book_id": {

"$first": "$_id"

}

}

}

],{"allowDiskUse":true})

合并match

当有两个前后紧密连接的match的时候,应该要将其合并为一个match

笔者改写后使用改写查询语句如下

{

"$match": {

"authors": {

"$in": ["author1", "author2"]

},

"time": {

"$gte": "20190101",

"$lte": "20191201"

}

}

}

这也是笔者最后优化有的查询语句,查询最终耗时246毫秒,explain分析如下,发现mongodb已经命中了我们需要的索引

优化原理

之前笔者对aggregate索引使用的误解在于笔者没有理解mongodb的聚合。Mongodb的聚合操作是一个管道,这个管道可以有很多阶段,mongodb提供了match,unwind,sort,group等管道操作,每一个阶段输出的结果是下一个阶段输入的结果,如果我们想要优化mongodb的聚合操作,我们应该尽量减少进入管道中的数据量。Mongodb在执行聚合查询的时候,在第一个阶段选择数据进入管道,只有第一个阶段mongodb聚合才会使用索引,为了尽量的减少进入管道中的数据量。所以上文提到的第一个前置match的优化措施就是为了减少进入管道的数据量,因为聚合查询只有在第一个阶段才能使用索引,笔者又将时间查询合并到第一个match中,以达到进一步减少进入管道数据量的目的。

你可能感兴趣的:(mongodb聚合查询优化)