MongoDB有很多创建索引的可能性。在本文中,我们将看到在整个子文档上创建索引的一种特殊情况。
创建测试集合
让我们创建一个包含一些随机文档的集合以运行我们的测试。您可以使用以下javascript代码生成示例测试集合。
for (var i = 1; i <= 10000; i++) {
db.test.insert(
{
name: "name_"+i,
subdoc: {
a: i,
b: i*2,
c: i*i
}
}
)
}
让我们看一下我们创建的文档:
> db.test.find().pretty()
{
"_id" : ObjectId("5f180f0fbdf0c5397723a6fe"),
"name" : "name_1",
"subdoc" : {
"a" : 1,
"b" : 2,
"c" : 1
}
}
{
"_id" : ObjectId("5f180f0fbdf0c5397723a6ff"),
"name" : "name_2",
"subdoc" : {
"a" : 2,
"b" : 4,
"c" : 4
}
}
{
"_id" : ObjectId("5f180f0fbdf0c5397723a700"),
"name" : "name_3",
"subdoc" : {
"a" : 3,
"b" : 6,
"c" : 9
}
}
...
...
现在创建的子文档的索引subdoc
> db.test.createIndex( { subdoc: 1 } )
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
查询子文档
现在我们在子文档上有了索引,我们可以尝试运行一些查询,看看是否使用了索引。
运行查询以查找子文档:
> db.test.find( { subdoc: { a:220, b:440, c: 48400 } } ).pretty()
{
"_id" : ObjectId("5f180f0fbdf0c5397723a7d9"),
"name" : "name_220",
"subdoc" : {
"a" : 220,
"b" : 440,
"c" : 48400
}
}
该查询有效,让我们使用explain()查看执行计划:
> db.test.find( { subdoc: { a:220, b:440, c: 48400 } } ).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "corra.test",
"indexFilterSet" : false,
"parsedQuery" : {
"subdoc" : {
"$eq" : {
"a" : 220,
"b" : 440,
"c" : 48400
}
}
},
"queryHash" : "07E4A30B",
"planCacheKey" : "759877DE",
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"subdoc" : 1
},
"indexName" : "subdoc_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"subdoc" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"subdoc" : [
"[{ a: 220.0, b: 440.0, c: 48400.0 }, { a: 220.0, b: 440.0, c: 48400.0 }]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "mdb1",
"port" : 27017,
"version" : "4.2.2-3",
"gitVersion" : "2cdb6e50913583f627acc5de35dc4e04dbfe196f"
},
"ok" : 1
}
很酷,我们在子文档上创建的索引已用于解决查询。您可以看到FETCH阶段在索引subdoc_1上使用了IXSCAN。
现在让我们在子文档的字段上尝试一些不同的查询。我们想执行一个查询来查找文档 b = 440和c = 48400。
> db.test.find( { subdoc: { b:440, c:48400 } } ).pretty()
>
没有结果。确实,这不是查询的正确语法。当仅查询子文档中的几个字段时,我们需要使用点符号。仅精确匹配过滤器支持查询子文档。
让我们尝试使用点符号查询:
> db.test.find( { "subdoc.b":440, "subdoc.c":48400 } ).pretty()
{
"_id" : ObjectId("5f180f0fbdf0c5397723a7d9"),
"name" : "name_220",
"subdoc" : {
"a" : 220,
"b" : 440,
"c" : 48400
}
}
好的,现在我们得到了结果。让我们看一下执行计划:
> db.test.find( { "subdoc.b":440, "subdoc.c":48400 } ).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "corra.test",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"subdoc.b" : {
"$eq" : 440
}
},
{
"subdoc.c" : {
"$eq" : 48400
}
}
]
},
"queryHash" : "D50037C0",
"planCacheKey" : "D50037C0",
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [
{
"subdoc.b" : {
"$eq" : 440
}
},
{
"subdoc.c" : {
"$eq" : 48400
}
}
]
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "mdb1",
"port" : 27017,
"version" : "4.2.2-3",
"gitVersion" : "2cdb6e50913583f627acc5de35dc4e04dbfe196f"
},
"ok" : 1
}
获奖计划是COLLSCAN。MongoDB无法使用索引。
可能是因为过滤条件不是子文档的左前缀。我们查询了{b:400,c:48400}。让我们尝试仅查询包含a和b的左前缀,并查看执行计划会发生什么。
> db.test.find( { "subdoc.a":220, "subdoc.b":440 } ).pretty()
{
"_id" : ObjectId("5f180f0fbdf0c5397723a7d9"),
"name" : "name_220",
"subdoc" : {
"a" : 220,
"b" : 440,
"c" : 48400
}
}
> db.test.find( { "subdoc.a":220, "subdoc.b":440 } ).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "corra.test",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"subdoc.a" : {
"$eq" : 220
}
},
{
"subdoc.b" : {
"$eq" : 440
}
}
]
},
"queryHash" : "10B20F88",
"planCacheKey" : "10B20F88",
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [
{
"subdoc.a" : {
"$eq" : 220
}
},
{
"subdoc.b" : {
"$eq" : 440
}
}
]
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "mdb1",
"port" : 27017,
"version" : "4.2.2-3",
"gitVersion" : "2cdb6e50913583f627acc5de35dc4e04dbfe196f"
},
"ok" : 1
}
哦,不,它仍然是COLLSCAN。
唯一可以使用子文档上的索引的查询是完全匹配。我们需要在子文档的所有字段上提供过滤器。对于任何其他不同条件,MongoDB将无法依赖索引。
此外,过滤器中字段的顺序必须与文档中的顺序完全相同。查看以下查询:
> db.test.find( { subdoc: { a:220, b:440, c:48400 } } )
{ "_id" : ObjectId("5f180f0fbdf0c5397723a7d9"), "name" : "name_220", "subdoc" : { "a" : 220, "b" : 440, "c" : 48400 } }
> db.test.find( { subdoc: { c: 48400, a:220, b:440 } } )
>
只有顺序正确的查询才能返回结果。第二个查询未返回任何内容,因为该订单与原始订单不匹配。注意这一点。
因此,如果您需要查询子文档的内部字段而不是整个子文档,则只需要提供点符号过滤器即可。
指数呢?
我们已经看到,我们需要运行查询来编写不同的过滤器,并且我们不能从已创建的索引中受益。因此,整个子文档上的索引不是很有用。确实,这不是出于我们查询的目的。
现在,如果我们要优化查询,我们正在使用点符号来运行,我们只需要在内部文件上创建其他索引。例如,如果大多数查询仅在a和b上使用过滤器,则应创建以下索引:
> db.test.createIndex( { "subdoc.a":1, "subdoc.b":1 } )
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}
现在让我们看看查询是否可以使用新索引。
> db.test.find( { "subdoc.a":220, "subdoc.b":440 } ).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "corra.test",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"subdoc.a" : {
"$eq" : 220
}
},
{
"subdoc.b" : {
"$eq" : 440
}
}
]
},
"queryHash" : "10B20F88",
"planCacheKey" : "B9C95AB7",
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"subdoc.a" : 1,
"subdoc.b" : 1
},
"indexName" : "subdoc.a_1_subdoc.b_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"subdoc.a" : [ ],
"subdoc.b" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"subdoc.a" : [
"[220.0, 220.0]"
],
"subdoc.b" : [
"[440.0, 440.0]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "mdb1",
"port" : 27017,
"version" : "4.2.2-3",
"gitVersion" : "2cdb6e50913583f627acc5de35dc4e04dbfe196f"
},
"ok" : 1
}
太好了,这是IXSCAN!使用新索引,查询将运行得更快。
结论
子文档上的索引只能在完全匹配过滤器的情况下使用。对于仅针对子文档中几个字段的任何其他类型的查询,我们必须提供其他简单或复合索引。
那么,子文档上的索引没有用吗?不,不是。这取决于您需要运行的查询。如果您始终有完全匹配过滤器,则索引很有用。此外,如果需要确保子文档的唯一性,则可以将索引与unique子句结合在一起创建。顺便说一下,在第二种情况下,即使在所有字段上都使用复合唯一索引,也可以实现相同的目的。
作者:Corrado Pandiani
文章原文:https://www.percona.com/blog/2020/07/24/mongodb-utilization-of-an-index-on-subdocuments/




