MongoDB-count()尽管使用了索引,但花费的时间太长

MongoDB-count()尽管使用了索引,但花费的时间太长,mongodb,indexing,query-optimization,Mongodb,Indexing,Query Optimization,我收集了62k份文件。同一个集合也有一组索引,其中大多数是简单的单字段索引。我所观察到的是,以下查询需要非常长的时间才能返回: db.jobs.count({"status":"complete","$or":[{"groups":{"$exists":false}},{"groups":{"$size":0}},{"groups":{"$in":["5e65ffc2a1e6ef0007bc5fa8"]}}]}) 上述查询的executionStats如下所示 { "queryPlanner"

我收集了62k份文件。同一个集合也有一组索引,其中大多数是简单的单字段索引。我所观察到的是,以下查询需要非常长的时间才能返回:

db.jobs.count({"status":"complete","$or":[{"groups":{"$exists":false}},{"groups":{"$size":0}},{"groups":{"$in":["5e65ffc2a1e6ef0007bc5fa8"]}}]})
上述查询的executionStats如下所示

{
"queryPlanner" : {
    "plannerVersion" : 1,
    "namespace" : "xxxxxx.jobs",
    "indexFilterSet" : false,
    "parsedQuery" : {
        "$and" : [
            {
                "$or" : [
                    {
                        "groups" : {
                            "$size" : 0
                        }
                    },
                    {
                        "groups" : {
                            "$eq" : "5e65ffc2a1e6ef0007bc5fa8"
                        }
                    },
                    {
                        "$nor" : [
                            {
                                "groups" : {
                                    "$exists" : true
                                }
                            }
                        ]
                    }
                ]
            },
            {
                "status" : {
                    "$eq" : "complete"
                }
            }
        ]
    },
    "winningPlan" : {
        "stage" : "FETCH",
        "filter" : {
            "$or" : [
                {
                    "groups" : {
                        "$size" : 0
                    }
                },
                {
                    "groups" : {
                        "$eq" : "5e65ffc2a1e6ef0007bc5fa8"
                    }
                },
                {
                    "$nor" : [
                        {
                            "groups" : {
                                "$exists" : true
                            }
                        }
                    ]
                }
            ]
        },
        "inputStage" : {
            "stage" : "IXSCAN",
            "keyPattern" : {
                "status" : 1,
                "groups" : 1
            },
            "indexName" : "status_1_groups_1",
            "isMultiKey" : true,
            "multiKeyPaths" : {
                "status" : [ ],
                "groups" : [
                    "groups"
                ]
            },
            "isUnique" : false,
            "isSparse" : false,
            "isPartial" : false,
            "indexVersion" : 2,
            "direction" : "forward",
            "indexBounds" : {
                "status" : [
                    "[\"complete\", \"complete\"]"
                ],
                "groups" : [
                    "[MinKey, MaxKey]"
                ]
            }
        }
    },
    "rejectedPlans" : [
        {
            "stage" : "FETCH",
            "filter" : {
                "$or" : [
                    {
                        "groups" : {
                            "$size" : 0
                        }
                    },
                    {
                        "groups" : {
                            "$eq" : "5e65ffc2a1e6ef0007bc5fa8"
                        }
                    },
                    {
                        "$nor" : [
                            {
                                "groups" : {
                                    "$exists" : true
                                }
                            }
                        ]
                    }
                ]
            },
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "status" : 1
                },
                "indexName" : "status_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "status" : [ ]
                },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "status" : [
                        "[\"complete\", \"complete\"]"
                    ]
                }
            }
        }
    ]
},
"executionStats" : {
    "executionSuccess" : true,
    "nReturned" : 62092,
    "executionTimeMillis" : 9992,
    "totalKeysExamined" : 62092,
    "totalDocsExamined" : 62092,
    "executionStages" : {
        "stage" : "FETCH",
        "filter" : {
            "$or" : [
                {
                    "groups" : {
                        "$size" : 0
                    }
                },
                {
                    "groups" : {
                        "$eq" : "5e65ffc2a1e6ef0007bc5fa8"
                    }
                },
                {
                    "$nor" : [
                        {
                            "groups" : {
                                "$exists" : true
                            }
                        }
                    ]
                }
            ]
        },
        "nReturned" : 62092,
        "executionTimeMillisEstimate" : 9929,
        "works" : 62093,
        "advanced" : 62092,
        "needTime" : 0,
        "needYield" : 0,
        "saveState" : 682,
        "restoreState" : 682,
        "isEOF" : 1,
        "invalidates" : 0,
        "docsExamined" : 62092,
        "alreadyHasObj" : 0,
        "inputStage" : {
            "stage" : "IXSCAN",
            "nReturned" : 62092,
            "executionTimeMillisEstimate" : 60,
            "works" : 62093,
            "advanced" : 62092,
            "needTime" : 0,
            "needYield" : 0,
            "saveState" : 682,
            "restoreState" : 682,
            "isEOF" : 1,
            "invalidates" : 0,
            "keyPattern" : {
                "status" : 1,
                "groups" : 1
            },
            "indexName" : "status_1_groups_1",
            "isMultiKey" : true,
            "multiKeyPaths" : {
                "status" : [ ],
                "groups" : [
                    "groups"
                ]
            },
            "isUnique" : false,
            "isSparse" : false,
            "isPartial" : false,
            "indexVersion" : 2,
            "direction" : "forward",
            "indexBounds" : {
                "status" : [
                    "[\"complete\", \"complete\"]"
                ],
                "groups" : [
                    "[MinKey, MaxKey]"
                ]
            },
            "keysExamined" : 62092,
            "seeks" : 1,
            "dupsTested" : 62092,
            "dupsDropped" : 0,
            "seenInvalidated" : 0
        }
    }
},
"serverInfo" : {
    "host" : "xxxxxxx",
    "port" : 27017,
    "version" : "3.6.15",
    "gitVersion" : "xxxxxx"
},
"ok" : 1}
我想理解的是,为什么当输入_阶段的索引扫描需要60毫秒时,FETCH阶段需要10秒钟。因为我最终要做一个count(),我并不需要mongoDB来返回文档,我只需要它对匹配的键的数量进行$求和,并给出总数


知道我做错了什么吗

查询解释说没有计数,它返回了很多文档:

"nReturned" : 62092,
db.jobs.update({"groups":{"$exists":false}},{"$set":{"groups":false}})
db.jobs.count({"status":"complete","groups":{"$size":0}})
db.jobs.count({"status":"complete","groups":{"$in":[false, "5e65ffc2a1e6ef0007bc5fa8"]}})
每个阶段的估计执行时间表明,索引扫描预计需要60毫秒,从磁盘获取文档需要额外的9.8秒

此计数要求获取文档有几个原因:

"nReturned" : 62092,
db.jobs.update({"groups":{"$exists":false}},{"$set":{"groups":false}})
db.jobs.count({"status":"complete","groups":{"$size":0}})
db.jobs.count({"status":"complete","groups":{"$in":[false, "5e65ffc2a1e6ef0007bc5fa8"]}})
  • 无法从索引中完全确定是否存在键
    {“$exists”:false}
    谓词也很麻烦。构建索引时,文档的值包含每个索引字段的值。“不存在”没有值,因此它使用
    null
    。由于包含值显式设置为
    null
    的字段的文档不应与
    {“$exists”:false}
    匹配,因此查询执行器必须从磁盘加载每个文档,以确定该字段是
    null
    还是不存在。这意味着不能使用COUNTSCAN阶段,这进一步意味着必须从磁盘加载所有要计数的文档
  • $或
    谓词不能确保排他性
    查询执行者不能提前知道
    $或
    中的子句是互斥的。它们在查询中,但在一般情况下,单个文档可能与
    $或
    中的多个子句相匹配,因此查询执行器必须加载文档以确保重复数据消除
那么,如何消除提取阶段呢? 如果只使用
$in
子句进行查询,或者只使用
$size
子句进行查询,您应该会发现计数是从索引扫描派生的,而无需加载任何文档

也就是说,如果要从客户端单独运行这些查询,并对结果求和,您应该会发现总体执行时间小于需要获取的查询:

db.jobs.count({"status":"complete","groups":{"$size":0}})
db.jobs.count({"status":"complete","groups":{"$in":["5e65ffc2a1e6ef0007bc5fa8"]}})
对于
{“groups”:{“$exists”:false}
谓词,您可以稍微修改数据,例如确保字段始终存在,但为其指定一个表示“未定义”的值,该值可以索引和查询

例如,如果要运行以下更新,则“组”字段将存在于所有文档中:

"nReturned" : 62092,
db.jobs.update({"groups":{"$exists":false}},{"$set":{"groups":false}})
db.jobs.count({"status":"complete","groups":{"$size":0}})
db.jobs.count({"status":"complete","groups":{"$in":[false, "5e65ffc2a1e6ef0007bc5fa8"]}})
通过运行这两个查询,您可以获得与上述计数相同的结果,这两个查询都应该包含在索引扫描中,并且应该比需要加载文档的查询一起运行得更快:

"nReturned" : 62092,
db.jobs.update({"groups":{"$exists":false}},{"$set":{"groups":false}})
db.jobs.count({"status":"complete","groups":{"$size":0}})
db.jobs.count({"status":"complete","groups":{"$in":[false, "5e65ffc2a1e6ef0007bc5fa8"]}})

如果您能够以某种方式避免空数组的情况,那么可以使用以下查询:
db.jobs.count({“status”:“complete”,“groups”:{$in:[null,“5e65ffc2a1e6ef0007bc5fa8”]})

null
相当于
$exists:false

另外:我建议使用ObjectId而不是string作为
字段的类型

更新

$size从未命中索引

您可以使用以下查询:

db.jobs.count({"status":"complete","$or":[
  {"groups":[],
  {"groups": {$in: [ null, "5e65ffc2a1e6ef0007bc5fa8" ]}
]})
`


请对您的文本进行更好的格式化,并解释您与其他解决方案的不同之处。谢谢。除了一些小的语法错误之外,这个管道还有一个问题——你不能以这种方式使用$count。根据mongoDB文档:$count具有以下原型形式:{$count:}是输出字段的名称,该字段的值为count。虽然此代码可能提供了一个问题的解决方案,但最好添加上下文以了解其工作的原因/方式。这可以帮助未来的用户学习,并将这些知识应用到他们自己的代码中。当代码被解释时,用户可能会以投票的形式给予你积极的反馈;显然不需要$in,因为$exists是一个更广泛的标准。。。简体中文:计数状态=包含字段组的所有记录/文档的完成。。。。是吗?不完全是,父条件是$or,因此它要么根本不是组字段,要么是空数组,要么是$INU中的一个。幸运的是,数据库中的大多数文档都有[]作为组值,因此我无法真正避免它。。关于ObjectId建议-这会带来什么影响?使用ObjectId而不是普通字符串更安全还是更快?ObjectId更小,索引性能更好。哦,伙计,我对你的答案寄予厚望!但不幸的是,我试图删除这些麻烦的谓词,但没有效果。运行
db.jobs.find({“status”:“complete”,“groups”:{“$size”:0}})。explain(“executionStats”)
给了我inputStage.executionTimeMillisEstimate:50,但父节点获取阶段仍然花费了>6秒的时间。你知道为什么它没有使用COUNTSCAN阶段吗?仔细看,
“groups”:{“$size”:0}
不能由索引正常提供服务,因为MongoDB没有将数组本身存储在索引值中,它为每个数组元素存储单独的索引值。我怀疑实现这一点的方法是在更新中结合
[]
missing
,比如
db.jobs.update({$or:[{“groups”:{“$exists”:false},{“groups”:[]},{“$set”:{“groups”:false})
,则第二个查询可以在不加载文档的情况下获得所需的计数。您知道mongoDB将如何处理在“groups”字段中实际包含哈希数组的文档吗?如果文档包含
组:[“foo”、“bar”、“abc”]
且查询筛选器为