概述
MongoDB 是一款功能强大的 NoSQL 数据库,可以灵活扩展以处理大量数据。然而,在处理涉及多个字段和复杂条件的查询时,我们可能会遇到一个警告消息,显示 "查询目标已超过1000个扫描对象/返回的文档数"。在本文中,我们将探讨一个特定的情景,了解此警告的产生原因,并讨论通过优化索引来消除该警告的方法。
问题描述
给定一个MongoDB 集合:
{
"_id" : ObjectId("abc"),
"key" : "mykey",
"val" : "myval",
"created_at" : ISODate("2023-09-25T07:38:04.985Z"),
"a_has_sent" : false,
"b_has_sent" : false,
"updated_at" : ISODate("2023-09-25T07:38:04.985Z")
}
该集合有两个索引定义如下:
- 索引1:
{"key": {"updated_at": 1}}
名称:"updated_at_1" - 索引2:
{"key": {"updated_at": 1, "a_has_sent": 1, "b_has_sent": 1}}
名称:"updated_at_1_a_has_sent_1_b_has_sent_1"
"查询目标已超过1000个扫描对象/返回的文档数" (Query Targeting: Scanned Objects / Returned has gone above 1000) 警告是由以下查询触发的:
db.collectionname.find({
"updated_at": {"$gte": ISODate("2023-09-24")},
"$or": [
{"a_has_sent": false},
{"b_has_sent": false}
],
"key": "key_1"
})
问题研究
我先分析下这个错误 Query Targeting: Scanned Objects / Returned has gone above 1000
产生的原因。依据MongoDB文档 这个警告信息表示你的查询在执行时扫描的文档数量远远超过了实际返回的文档数量,意味着查询的执行效率不高。通常,我们可以通过创建适当的索引来解决这个问题。
针对我们的查询,先查看下 explain
的结果
> db.collectionname.find({ "updated_at": { "$gte": ISODate("2023-09-24")}, "$or": [{ "a_has_sent": false }, {"b_has_sent": false}], "key": "key1"}).explain()
...
winningPlan: {
stage: 'FETCH',
filter: {
'$and': [
{
'$or': [
{ a_has_sent: { '$eq': false } },
{ b_has_sent: { '$eq': false } }
]
},
{ key: { '$eq': 'key1' } }
]
},
inputStage: {
stage: 'IXSCAN',
keyPattern: { updated_at: 1 },
indexName: 'updated_at_1',
isMultiKey: false,
multiKeyPaths: { updated_at: [] }
}
},
rejectedPlans: [
{
stage: 'FETCH',
filter: {
'$and': [
{
'$or': [
{ a_has_sent: { '$eq': false } },
{ b_has_sent: { '$eq': false } }
]
},
{ key: { '$eq': 'key1' } }
]
},
inputStage: {
stage: 'IXSCAN',
keyPattern: { updated_at: 1, a_has_sent: 1, b_has_sent: 1 },
indexName: 'updated_at_1_a_has_sent_1_b_has_sent_1',
...
我们从 winningPlan中观察到使用的索引是 "updated_at_1",而索引 "updated_at_1_a_has_sent_1_b_has_sent_1"是在rejectedPlans,并没有被MongoDB采用,所以导致查询在执行时扫描的文档数量远远超过了实际返回的文档数量。
方案1
我们尝试添加了另一个新索引 {“updated_at”: 1, “key”: 1}
,希望此查询可以使用新索引来减少扫描的文档数量。不幸的是,我们失败了,这个查询仍然使用了名为"updated_at_1"的索引。
方案2
我们还尝试将find
替换为aggregate
> aggregate([{"$match": { "updated_at": { "$gte": ISODate("2023-09-24") }, "$or": [{ "a_has_sent": false }, { "b_has_sent": false}], "key": "key_1"}}])
再次失败,这个查询依然使用了名为"updated_at_1"的索引。
方案3
使用MongoDB The ESR (Equality, Sort, Range) Rule 来优化我们的查询语句
什么是ESR(Equality, Sort, Range)规则
MongoDB的ESR(Equality, Sort, Range)规则是一种用于优化复合索引的方法。复合索引是引用多个字段的索引,可以显著提高查询响应时间。索引键对应于文档字段。在大多数情况下,按照ESR规则来排列索引键有助于创建更高效的复合索引1。
让我们详细了解一下ESR规则的三个方面:
- Equality(等值):这指的是对单个值的精确匹配。在索引中,首先放置需要精确匹配的字段。索引可以具有多个键,用于处理精确匹配的查询。这些索引键的顺序不影响MongoDB的搜索算法,但为了满足索引的等值匹配,所有精确匹配的索引键必须出现在其他索引字段之前。确保等值测试能够消除至少90%的可能文档匹配,以减少扫描的索引键数量。
- Sort(排序):排序决定了结果的顺序。排序紧随等值匹配,因为等值匹配减少了需要排序的文档数量。在索引中,只有当查询字段是索引键的子集时,索引才能支持排序操作。如果查询包括了所有前缀键的等值条件,那么对索引键的子集进行排序操作是支持的。例如,如果我们查询汽车制造商为“GM”的文档,并按照型号排序,我们可以创建一个索引:
db.cars.createIndex({ manufacturer: 1, model: 1 })
。制造商是第一个键,因为它是一个等值匹配。型号按照相同的顺序(1)被索引,以满足查询的需求。 - Range(范围):范围过滤器用于扫描字段,不需要精确匹配。范围过滤器与索引键的绑定较松。为了提高查询效率,尽量使范围边界尽可能紧凑,并使用等值匹配来限制必须扫描的文档数量。例如,我们可以使用以下查询来查找价格大于等于15000的汽车:`db.cars.find({ price: { $gte: 15000 } })``
遵循ESR规则,我们重新创建了索引{"key": 1, "a_has_sent": 1, "b_has_sent": 1, "updated_at": 1}
,现在可以使用此索引来执行查询。这个警告"查询目标已超过1000个扫描对象/返回的文档数" (Query Targeting: Scanned Objects / Returned has gone above 1000) 也消除了。
winningPlan: {
stage: 'FETCH',
filter: {
'$or': [
{ a_has_sent: { '$eq': false } },
{ b_has_sent: { '$eq': false } }
]
},
inputStage: {
stage: 'IXSCAN',
keyPattern: {
key: 1,
a_has_sent: 1,
b_has_sent: 1,
updated_at: 1
},
indexName: 'key_1_a_has_sent_1_b_has_sent_1_updated_at_1',
isMultiKey: false,
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
总结
- 通过应用ESR规则,提高了查询效率,并消除了"查询目标已超过1000个扫描对象/返回的文档数"警告。
- 对于查询语句,需要检查explain结果,查看是否使用了最佳的索引,使查询效率最高。