暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MongoDB:利用子文档索引

原创 小小亮 2020-07-28
2612

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/

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论