我们知道,mongo 里的普通查询语句是没有分组查询功能的,如果要实现类似于关系型数据库 SQL 里的 group by 操作就要用 aggregate。遗憾的是副本集中,aggregate 操作是在主库执行(比如笔者的 3.0.3 就是这样的,据说 mongo 新版本会改善这一状况 - 将 aggregate 操作放到从库执行),这对于读写分离的副本集是不合理的,大并发时的大量慢查询操作很容易将主库给拖死。
要想提高主库性能,优化 aggregate 慢查询是一个不错的办法,但是如果能将 aggregate 换成普通查询语句进而去从库执行(实现读写分离)也不失为一个不错的优化方案。本文将以一个实战例子详解如何使用普通查询语句替代 aggregate 操作,本文示例代码均采用 go 语言实现。
database.C(col.name).Pipe([]bson.M{
{"$match": find},
{"$group": bson.M{"_id": "$merId"}},
{"$group": bson.M{"_id": "null", "total": bson.M{"$sum": 1}}},
}).One(&total)
代码解析:find 对象是一个用于存放查询条件的 BSON map(类似于 SQL 里的 where 从句),total 是一个自定义的用于存放返回结果的结构体(类似于 JDK JDBC API 里的 ResultSet)。上述查询使用了两层 group,第一层按 merId 进行分组,第二层对第一层分组后的 merId 个数进行统计。
理解其要做的事情之后,我们可以还是按 find 的条件进行查询,调用查询结果(是一个 v2.Query 对象)的 Distinct 函数对 merId 进行分组 - 这一步实现了上述语句的第一层 group;merId 分组后我们在对这个分组执行查看长度的操作,实现上述 aggregate 中的第二层分组。go 语言示例代码如下:
var itotal int = 0
var res []string
err := database.C(col.name).Find(find).Distinct("merId", &res)
if err != nil {
log.Errorf("fail: %s", err)
}
itotal = len(res)
total.Value = itotal
这段代码实现的功能和上述 aggregate 实现的功能是一样的,但它是在从库执行。美中不足的是要把所有符合条件的 merId 都从数据库拉到应用,之后由应用计算其长度,因为 Distinct 返回的结果是一个 error 接口,不能够计算 merId 集合的长度,但牺牲的这点应用内存和应用数据库带宽能换来 mongo 副本集主从库的读写分离,还是值得的。