Golang中MongoDB多条件计数:替代多次调用coll.CountDocuments的聚合方案
问题:聚合查询替代多次CountDocuments返回空结果
我在Golang中通过多次调用coll.CountDocuments实现多条件文档计数,现在想把所有筛选条件整合进单次聚合查询来替代多次调用,但编写的聚合管道代码返回了空的游标(cur)和结果(result),求解决。
原实现代码(正常工作)
func NoOfDocumentsInfo(DB string, col string, filters ...bson.D) ([]int64, error) { if nil == dbInstance { if nil == GetDBInstance() { logger.Error("Not connecting to DB") err := errors.New("DB connection error") return nil, err } } logger.Debugf("%s %s", DB, col) coll := dbInstance.Database(DB).Collection(col) counts := make([]int64, len(filters)) for i, filter := range filters { count, err := coll.CountDocuments(context.TODO(), filter) if err != nil { logger.Fatal(err) return nil, err } counts[i] = count } return counts, nil }
尝试的聚合管道代码(返回空结果)
func NoOfDocumentsInfo(DB string, col string, filters ...bson.D) ([]int64, error) { if dbInstance == nil { if GetDBInstance() == nil { logger.Error("Not connecting to DB") err := errors.New("DB connection error") return nil, err } } logger.Debugf("%s %s", DB, col) coll := dbInstance.Database(DB).Collection(col) pipeline := make([]bson.M, 0, len(filters)+2) pipeline = append(pipeline, bson.M{"$match": bson.M{"$or": filters}}) pipeline = append(pipeline, bson.M{"$group": bson.M{"_id": nil, "count": bson.M{"$sum": 1}}}) pipeline = append(pipeline, bson.M{"$group": bson.M{"_id": nil, "count": bson.M{"$first": "$count"}}}) var result struct { Count int64 `bson:"count"` } cur, err := coll.Aggregate(context.TODO(), pipeline) if err != nil { logger.Fatal(err) return nil, err } logger.Debugf("cur: %+v", cur) err = cur.Decode(&result) logger.Debugf("result: %+v, err: %v", result, err) if err != nil { logger.Fatal(err) return nil, err } return []int64{result.Count}, nil }
问题分析与解决方案
原代码的核心问题
- 逻辑偏离需求:用
$or把所有筛选条件合并后,统计的是所有条件匹配的文档总数,但原需求是分别统计每个筛选条件的匹配数,完全不是一回事。 - 冗余聚合阶段:第二次
$group完全没必要,第一次$group已经能得到总数,多此一举反而可能引发异常。 - 游标处理错误:没有检查游标是否有数据就直接解码,如果聚合没有返回结果(比如没有文档匹配任何条件),Decode会直接报错。
正确实现:用$facet实现多条件并行统计
MongoDB的$facet阶段允许在单次聚合中同时运行多个子管道,正好满足"一次查询得到多个条件计数"的需求。以下是修正后的代码:
import "fmt" func NoOfDocumentsInfo(DB string, col string, filters ...bson.D) ([]int64, error) { if dbInstance == nil { if GetDBInstance() == nil { logger.Error("Not connecting to DB") return nil, errors.New("DB connection error") } } logger.Debugf("%s %s", DB, col) coll := dbInstance.Database(DB).Collection(col) // 构建$facet的每个子管道,对应一个筛选条件的统计 facet := bson.M{} for idx := range filters { // 为每个条件生成唯一的字段名 facetKey := fmt.Sprintf("filter_%d", idx) facet[facetKey] = []bson.M{ {"$match": filters[idx]}, // 应用当前筛选条件 {"$count": "total"}, // 统计匹配文档数 } } // 聚合管道只需要$facet阶段 pipeline := []bson.M{{"$facet": facet}} var result bson.M cur, err := coll.Aggregate(context.TODO(), pipeline) if err != nil { logger.Fatal(err) return nil, err } defer cur.Close(context.TODO()) // 务必关闭游标,避免资源泄漏 // 检查游标是否有结果 if !cur.Next(context.TODO()) { if cur.Err() != nil { logger.Fatal(cur.Err()) return nil, cur.Err() } // 无匹配文档时返回全0数组 return make([]int64, len(filters)), nil } // 解码$facet的结果 if err := cur.Decode(&result); err != nil { logger.Fatal(err) return nil, err } // 解析每个条件的统计结果 counts := make([]int64, len(filters)) for idx := range filters { facetKey := fmt.Sprintf("filter_%d", idx) // 取出对应子管道的结果数组 if resultArr, ok := result[facetKey].([]interface{}); ok && len(resultArr) > 0 { // 数组第一个元素就是统计结果对象 if countObj, ok := resultArr[0].(bson.M); ok { if total, ok := countObj["total"].(int64); ok { counts[idx] = total } } } // 若某个条件无匹配,保持默认值0 } return counts, nil }
代码说明
$facet的作用:每个子管道独立运行,分别对应当初的一个filters条件,实现多条件并行统计,避免多次数据库调用。- 兼容性:返回值和原函数完全一致(
[]int64),无需修改调用方代码。 - 错误处理:完善了游标关闭、空结果处理逻辑,避免资源泄漏和空指针错误。
内容的提问来源于stack exchange,提问作者Joytiprokash Roy
相关产品推荐
相关产品推荐

