我有一个庞大的数据库和许多复合索引。问题是,mongo 不会使用索引中的所有字段进行查询,它只使用 4 个字段中的 2 个,即使所有 4 个字段都在复合索引中。或者它使用 3 字段复合索引而不是 4 字段。
这是我的架构(数据和元中还有其他字段,但在此查询中未使用它们):
{
store_id: {type: String},
data: {
id: {type: Number},
},
meta: {
is_published: {type: Boolean},
lowercase_sku: {type: String}
}
}
我有这些索引:
db.products.createIndex({'store_id':1,'meta.is_published':1,'data.id':1})
db.products.createIndex({'store_id':1,'meta.is_published':1,'data.id':1,'meta.lowercase_sku':1})
这是我的查询:
db.products.find({
'store_id': 'my_domain.com',
'meta.lowercase_sku': 'go-a83-3034/8+4',
'meta.is_published': true,
'data.id': {'$ne': 7801040323}
}).explain('executionStats')
这是我从解释中得到的:
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "my_database.products",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"meta.is_published" : {
"$eq" : true
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
{
"store_id" : {
"$eq" : "my_domain.com"
}
},
{
"$not" : {
"data.id" : {
"$eq" : 7801040323
}
}
}
]
},
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"data.id" : 1
},
"indexName" : "store_id_1_meta.is_published_1_data.id_1",
"isMultiKey" : false,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"["my_domain.com", "my_domain.com"]"
],
"meta.is_published" : [
"[true, true]"
],
"data.id" : [
"[MinKey, 7801040323.0)",
"(7801040323.0, MaxKey]"
]
}
}
},
"rejectedPlans" : [
{
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"$not" : {
"data.id" : {
"$eq" : 7801040323
}
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
}
]
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"data.id" : 1,
"meta.lowercase_sku" : 1
},
"indexName" : "store_id_1_meta.is_published_1_data.id_1_meta.lowercase_sku_1",
"isMultiKey" : true,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"["my_domain.com", "my_domain.com"]"
],
"meta.is_published" : [
"[true, true]"
],
"data.id" : [
"[MinKey, 7801040323.0)",
"(7801040323.0, MaxKey]"
],
"meta.lowercase_sku" : [
"[MinKey, MaxKey]"
]
}
}
}
]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 4590,
"totalKeysExamined" : 28527,
"totalDocsExamined" : 28525,
"executionStages" : {
"stage" : "FETCH",
"filter" : {
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 130,
"works" : 28528,
"advanced" : 1,
"needTime" : 28525,
"needYield" : 0,
"saveState" : 5351,
"restoreState" : 5351,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 28525,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 28525,
"executionTimeMillisEstimate" : 70,
"works" : 28527,
"advanced" : 28525,
"needTime" : 1,
"needYield" : 0,
"saveState" : 5351,
"restoreState" : 5351,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"data.id" : 1
},
"indexName" : "store_id_1_meta.is_published_1_data.id_1",
"isMultiKey" : false,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"["my_domain.com", "my_domain.com"]"
],
"meta.is_published" : [
"[true, true]"
],
"data.id" : [
"[MinKey, 7801040323.0)",
"(7801040323.0, MaxKey]"
]
},
"keysExamined" : 28527,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
},
"serverInfo" : {
"version" : "3.2.18",
"gitVersion" : "4c1bae566c0c00f996a2feb16febf84936ecaf6f"
},
"ok" : 1
}
如您所见,由于某种原因,mongo 不使用 4 字段索引,即使我暗示索引,它也只使用前两个字段("store_id"和"meta.is_published")。它倾向于选择仅涵盖 3 个字段("store_id"、"meta.is_published"和"data.id")的 3 字段索引,前提是从 4 字段索引中仅使用 2 个字段,这有点有意义。但是为什么?
有人可以向我解释这一点,也许提示我如何让查询更快地工作吗?
数据库很大(大约 11 G),并且最近迁移(使用索引导入),如果有帮助的话。但是,有问题的索引是在导入后创建的。
UPD (2):我还创建了一个具有相同架构和索引的文档测试集合。同样的查询确实正确地使用了索引。所以我倾向于认为索引大小或值类型存在某种问题。但我仍然无法确定它。
对此的任何帮助或见解我都非常感激。
下面是带有 4 字段索引提示的查询和结果供您参考。
db.products.find({
'store_id': 'my_domain.com',
'meta.lowercase_sku': 'go-a83-3034/8+4',
'meta.is_published': true,
'data.id': {'$ne': 7801040323}
}).hint('store_id_1_meta.is_published_1_data.id_1_meta.lowercase_sku_1').explain('executionStats')
结果:
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "sharp_production.products",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"meta.is_published" : {
"$eq" : true
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
{
"store_id" : {
"$eq" : "my_domain.com"
}
},
{
"$not" : {
"data.id" : {
"$eq" : 7801040323
}
}
}
]
},
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"$not" : {
"data.id" : {
"$eq" : 7801040323
}
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
}
]
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"data.id" : 1,
"meta.lowercase_sku" : 1
},
"indexName" : "store_id_1_meta.is_published_1_data.id_1_meta.lowercase_sku_1",
"isMultiKey" : true,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"["my_domain.com", "my_domain.com"]"
],
"meta.is_published" : [
"[true, true]"
],
"data.id" : [
"[MinKey, 7801040323.0)",
"(7801040323.0, MaxKey]"
],
"meta.lowercase_sku" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 213,
"totalKeysExamined" : 28630,
"totalDocsExamined" : 28525,
"executionStages" : {
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"$not" : {
"data.id" : {
"$eq" : 7801040323
}
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
}
]
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 210,
"works" : 28630,
"advanced" : 1,
"needTime" : 28628,
"needYield" : 0,
"saveState" : 223,
"restoreState" : 223,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 28525,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 28525,
"executionTimeMillisEstimate" : 110,
"works" : 28630,
"advanced" : 28525,
"needTime" : 104,
"needYield" : 0,
"saveState" : 223,
"restoreState" : 223,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"data.id" : 1,
"meta.lowercase_sku" : 1
},
"indexName" : "store_id_1_meta.is_published_1_data.id_1_meta.lowercase_sku_1",
"isMultiKey" : true,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"["my_domain.com", "my_domain.com"]"
],
"meta.is_published" : [
"[true, true]"
],
"data.id" : [
"[MinKey, 7801040323.0)",
"(7801040323.0, MaxKey]"
],
"meta.lowercase_sku" : [
"[MinKey, MaxKey]"
]
},
"keysExamined" : 28630,
"dupsTested" : 28628,
"dupsDropped" : 103,
"seenInvalidated" : 0
}
}
},
"serverInfo" : {
"version" : "3.2.18",
"gitVersion" : "4c1bae566c0c00f996a2feb16febf84936ecaf6f"
},
"ok" : 1
}
更新 (1)
好吧,显然"$ne"运算符可能会破坏索引,就像@JohnnyHK所说的那样。所以我创建了另一个这样的:
db.products.createIndex({'store_id':1,'meta.is_published':1,'meta.lowercase_sku':1})
。并在删除"$ne"后尝试将其用于我的查询(它只是一个文档,并没有真正改变任何东西):
db.products.find({
'store_id': 'my_domain.com',
'meta.is_published': true,
'meta.lowercase_sku': 'go-a83-3034/8+4',
}).hint('store_id_1_meta.is_published_1_meta.lowercase_sku_1').explain('executionStats')
但这是我这次得到的:
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "sharp_production.products",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"meta.is_published" : {
"$eq" : true
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
{
"store_id" : {
"$eq" : "my_domain.com"
}
}
]
},
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"meta.lowercase_sku" : 1
},
"indexName" : "store_id_1_meta.is_published_1_meta.lowercase_sku_1",
"isMultiKey" : true,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"["my_domain.com", "my_domain.com"]"
],
"meta.is_published" : [
"[true, true]"
],
"meta.lowercase_sku" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 136,
"totalKeysExamined" : 28629,
"totalDocsExamined" : 28526,
"executionStages" : {
"stage" : "FETCH",
"filter" : {
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 120,
"works" : 28630,
"advanced" : 1,
"needTime" : 28628,
"needYield" : 0,
"saveState" : 223,
"restoreState" : 223,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 28526,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 28526,
"executionTimeMillisEstimate" : 30,
"works" : 28630,
"advanced" : 28526,
"needTime" : 103,
"needYield" : 0,
"saveState" : 223,
"restoreState" : 223,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"meta.lowercase_sku" : 1
},
"indexName" : "store_id_1_meta.is_published_1_meta.lowercase_sku_1",
"isMultiKey" : true,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"["my_domain.com", "my_domain.com"]"
],
"meta.is_published" : [
"[true, true]"
],
"meta.lowercase_sku" : [
"[MinKey, MaxKey]"
]
},
"keysExamined" : 28629,
"dupsTested" : 28629,
"dupsDropped" : 103,
"seenInvalidated" : 0
}
}
},
"serverInfo" : {
"version" : "3.2.18",
"gitVersion" : "4c1bae566c0c00f996a2feb16febf84936ecaf6f"
},
"ok" : 1
}
。仍然保留 FETCH 阶段的"meta.lowercase_sku"条件,即使它就在索引中。还有什么想法吗?
这是由于查询计划程序选择要使用的索引的方式。
问题的要点是有多个可以满足查询的索引。假设您有两个具有相同前缀的索引:
db.test.createIndex({a:1, b:1})
和
db.test.createIndex({a:1, b:1, c:1})
现在,如果您这样做:
db.test.find({a:1, b:1})
MongoDB如何知道哪个索引可以满足该查询?根据磁盘中的布局(在许多方面),一个索引可能比另一个索引更快。
因此,首先它将使用以下公式对计划进行排名:
score = baseScore + productivity + tieBreakers
目前在MongoDB 3.6.3中,baseScore = 1
,productivity
取决于计划返回尽可能多的结果需要多少工作,tieBreakers
取决于索引是否需要FETCH
,没有SORT
阶段,或者有索引交集。
如果两个计划给出相同的分数(如上面的两个计划),那么它会让他们更快地获得 101 个结果。获胜的计划将被缓存,计划将被执行。可以想象,这可能是不确定的,并且还会浪费服务器的时间。最好是服务器可以根据评分系统选择一个索引。
索引集合的最佳方法是删除不必要的索引,以确保MongoDB可以立即选择索引。
注意:如果您对细节感兴趣,可以:
- 使用
db.setLogLevel(5,'query')
记录查询计划方法,并查看mongod
日志中打印的过程。 db.collection.getPlanCache().clear()
清除集合的计划缓存。- 检查计划排名器源代码。
- 查看博客文章优化MongoDB复合索引。