MongoDB - 使用 $elemMatch 在数组中搜索比没有索引慢
MongoDB - searching in array using $elemMatch slower with index than without
我有一个包含 50 万个文档的集合,其结构如下:
{
"_id" : ObjectId("5f2d30b0c7cc16c0da84a57d"),
"RecipientId" : "6a28d20f-4741-4c14-a055-2eb2593dcf13",
...
"Actions" : [
{
"CampaignId" : "7fa216da-db22-44a9-9ea3-c987c4152ba1",
"ActionDatetime" : ISODate("1998-01-13T00:00:00.000Z"),
"ActionDescription" : "OPEN"
},
...
]
}
我需要计算“Actions”数组中的子文档满足特定条件的顶级文档,为此我创建了以下多键索引(仅以“ActionDatetime”字段为例):
db.getCollection("recipients").createIndex( { "Actions.ActionDatetime": 1 } )
问题是当我使用 $elemMatch 编写查询时,操作比我根本不使用多键索引时慢得多:
db.getCollection("recipients").count({
"Actions":
{ $elemMatch:{ ActionDatetime: {$gt: new Date("1950-08-04")} }}}
)
此查询的统计信息:
{
"executionSuccess" : true,
"nReturned" : 0,
"executionTimeMillis" : 13093,
"totalKeysExamined" : 8706602,
"totalDocsExamined" : 500000,
"executionStages" : {
"stage" : "COUNT",
"nReturned" : 0,
"executionTimeMillisEstimate" : 1050,
"works" : 8706603,
"advanced" : 0,
"needTime" : 8706602,
"needYield" : 0,
"saveState" : 68020,
"restoreState" : 68020,
"isEOF" : 1,
"nCounted" : 500000,
"nSkipped" : 0,
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"Actions" : {
"$elemMatch" : {
"ActionDatetime" : {
"$gt" : ISODate("1950-08-04T00:00:00.000Z")
}
}
}
},
"nReturned" : 500000,
"executionTimeMillisEstimate" : 1040,
"works" : 8706603,
"advanced" : 500000,
"needTime" : 8206602,
"needYield" : 0,
"saveState" : 68020,
"restoreState" : 68020,
"isEOF" : 1,
"docsExamined" : 500000,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 500000,
"executionTimeMillisEstimate" : 266,
"works" : 8706603,
"advanced" : 500000,
"needTime" : 8206602,
"needYield" : 0,
"saveState" : 68020,
"restoreState" : 68020,
"isEOF" : 1,
"keyPattern" : {
"Actions.ActionDatetime" : 1.0
},
"indexName" : "Actions.ActionDatetime_1",
"isMultiKey" : true,
"multiKeyPaths" : {
"Actions.ActionDatetime" : [
"Actions"
]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"Actions.ActionDatetime" : [
"(new Date(-612576000000), new Date(9223372036854775807)]"
]
},
"keysExamined" : 8706602,
"seeks" : 1,
"dupsTested" : 8706602,
"dupsDropped" : 8206602
}
}
}
}
执行此查询需要 14 秒,而如果我删除索引,COLLSCAN 需要 1 秒。
我知道不使用 $elemMatch 并直接按“Actions.ActionDatetime”过滤会获得更好的性能,但实际上我需要按数组中的多个字段进行过滤,因此 $elemMatch 成为强制性的。
我怀疑是 FETCH 阶段破坏了性能,但我注意到当我直接使用“Actions.ActionDatetime”时,MongoDB 能够使用 COUNT_SCAN 而不是fetch,但是性能还是比COLLSCAN差(4s)。
我想知道是否有更好的索引策略来索引数组中具有高基数的子文档,或者我目前的方法是否遗漏了一些东西。
随着数量的增长,索引这些信息将是必要的,我不想依赖 COLLSCAN。
这里的问题是双重的:
每个文档都符合您的查询
将索引类比为图书馆的目录。如果你想找到一本书,在目录中查找它可以让你直接走到拿着它的书架,这比从第一个书架开始搜索书籍要快得多(当然除非它确实在那个书架上)第一个架子)。但是,如果您想获得图书馆中的 所有 本书,直接将它们从书架上取下来比查看目录然后再去获取要快得多它。
虽然这个类比远非完美,但它确实表明,当考虑大部分文档时,集合扫描有望比索引查找更有效。
多键索引对每个文档有多个条目
当 mongod 在数组上构建索引时,它会在索引中为每个离散元素创建一个单独的条目。当您匹配数组元素中的值时,索引可以让您快速找到匹配的文档,但由于单个文档预计在索引中有多个条目,因此之后需要进行重复数据删除。
$elemMatch
进一步加剧了这些情况。由于索引包含单独索引字段的值,因此无法确定不同字段的值是否出现在索引的同一数组元素中,因此它必须加载每个文档以进行检查。
本质上,当将 elemMatch 与索引和匹配每个文档的查询一起使用时,mongod 节点将检查索引以识别匹配值,删除该列表的重复项,然后加载每个文档(可能按照索引中遇到的顺序) 查看单个数组值是否满足 elemMatch。
与 non-indexed 集合扫描执行相比,其中 mongod 必须按照在磁盘上遇到的顺序加载每个文档,并检查单个数组元素匹配是否满足 elemMatch,很明显索引如果大部分文档与查询匹配,查询将执行得更差。
TLDR:这是多键索引结合 $elemMatch
.
的预期行为
为什么会这样?
所以是 FETCH
阶段破坏了您的查询性能,不幸的是,这是预期的行为。
来自多键索引文档的 covered query 部分:
Multikey indexes cannot cover queries over array field(s).
意味着关于 sub-document 的所有信息都不在多键索引中 - 用一个字段计数是一个例外,它可以做得更好。但是这种情况下$elemMatch
还是在逼一个FETCH
阶段?为什么它只使用一个字段。
想象一下这个场景:
//doc1
{
"Actions" : [
{
"CampaignId" : "7fa216da-db22-44a9-9ea3-c987c4152ba1",
"ActionDatetime" : ISODate("1998-01-13T00:00:00.000Z"),
"ActionDescription" : "OPEN"
},
...
]
}
//doc2
{
"Actions" : {
"CampaignId" : "7fa216da-db22-44a9-9ea3-c987c4152ba1",
"ActionDatetime" : ISODate("1998-01-13T00:00:00.000Z"),
"ActionDescription" : "OPEN"
}
}
因为Mongo“扁平化”它索引的数组,一旦建立索引Mongo就无法区分这两个文档,但是$elemMatch
需要一个数组object要匹配它必须获取这些文件以确定哪一个符合条件。
这正是您面临的问题。
你能做什么?
好吧,没什么可悲的。我不确定您的查询有多动态,但解决此问题的唯一方法是预处理文档以包含对您的查询的“答案”。
我仍然很难相信 COLSCAN
比索引查询做得更好。我假设您正在匹配 collection 的很大一部分,同时 Actions
数组非常大。
我的建议是,性能将一直是一个问题,特别是如果您的查询将继续匹配 collection 的大部分内容,则重组
你的数据。只需将每个 Actions
条目保存为自己的文档即可。
{
"Actions" : {
"CampaignId" : "7fa216da-db22-44a9-9ea3-c987c4152ba1",
"ActionDatetime" : ISODate("1998-01-13T00:00:00.000Z"),
"ActionDescription" : "OPEN"
}
}
然后您的查询将被允许使用索引,您必须使用与计数不同的查询。 RecipientId
上的 distinct 听起来像是一个有效的选项。
我有一个包含 50 万个文档的集合,其结构如下:
{
"_id" : ObjectId("5f2d30b0c7cc16c0da84a57d"),
"RecipientId" : "6a28d20f-4741-4c14-a055-2eb2593dcf13",
...
"Actions" : [
{
"CampaignId" : "7fa216da-db22-44a9-9ea3-c987c4152ba1",
"ActionDatetime" : ISODate("1998-01-13T00:00:00.000Z"),
"ActionDescription" : "OPEN"
},
...
]
}
我需要计算“Actions”数组中的子文档满足特定条件的顶级文档,为此我创建了以下多键索引(仅以“ActionDatetime”字段为例):
db.getCollection("recipients").createIndex( { "Actions.ActionDatetime": 1 } )
问题是当我使用 $elemMatch 编写查询时,操作比我根本不使用多键索引时慢得多:
db.getCollection("recipients").count({
"Actions":
{ $elemMatch:{ ActionDatetime: {$gt: new Date("1950-08-04")} }}}
)
此查询的统计信息:
{
"executionSuccess" : true,
"nReturned" : 0,
"executionTimeMillis" : 13093,
"totalKeysExamined" : 8706602,
"totalDocsExamined" : 500000,
"executionStages" : {
"stage" : "COUNT",
"nReturned" : 0,
"executionTimeMillisEstimate" : 1050,
"works" : 8706603,
"advanced" : 0,
"needTime" : 8706602,
"needYield" : 0,
"saveState" : 68020,
"restoreState" : 68020,
"isEOF" : 1,
"nCounted" : 500000,
"nSkipped" : 0,
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"Actions" : {
"$elemMatch" : {
"ActionDatetime" : {
"$gt" : ISODate("1950-08-04T00:00:00.000Z")
}
}
}
},
"nReturned" : 500000,
"executionTimeMillisEstimate" : 1040,
"works" : 8706603,
"advanced" : 500000,
"needTime" : 8206602,
"needYield" : 0,
"saveState" : 68020,
"restoreState" : 68020,
"isEOF" : 1,
"docsExamined" : 500000,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 500000,
"executionTimeMillisEstimate" : 266,
"works" : 8706603,
"advanced" : 500000,
"needTime" : 8206602,
"needYield" : 0,
"saveState" : 68020,
"restoreState" : 68020,
"isEOF" : 1,
"keyPattern" : {
"Actions.ActionDatetime" : 1.0
},
"indexName" : "Actions.ActionDatetime_1",
"isMultiKey" : true,
"multiKeyPaths" : {
"Actions.ActionDatetime" : [
"Actions"
]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"Actions.ActionDatetime" : [
"(new Date(-612576000000), new Date(9223372036854775807)]"
]
},
"keysExamined" : 8706602,
"seeks" : 1,
"dupsTested" : 8706602,
"dupsDropped" : 8206602
}
}
}
}
执行此查询需要 14 秒,而如果我删除索引,COLLSCAN 需要 1 秒。
我知道不使用 $elemMatch 并直接按“Actions.ActionDatetime”过滤会获得更好的性能,但实际上我需要按数组中的多个字段进行过滤,因此 $elemMatch 成为强制性的。
我怀疑是 FETCH 阶段破坏了性能,但我注意到当我直接使用“Actions.ActionDatetime”时,MongoDB 能够使用 COUNT_SCAN 而不是fetch,但是性能还是比COLLSCAN差(4s)。
我想知道是否有更好的索引策略来索引数组中具有高基数的子文档,或者我目前的方法是否遗漏了一些东西。 随着数量的增长,索引这些信息将是必要的,我不想依赖 COLLSCAN。
这里的问题是双重的:
每个文档都符合您的查询
将索引类比为图书馆的目录。如果你想找到一本书,在目录中查找它可以让你直接走到拿着它的书架,这比从第一个书架开始搜索书籍要快得多(当然除非它确实在那个书架上)第一个架子)。但是,如果您想获得图书馆中的 所有 本书,直接将它们从书架上取下来比查看目录然后再去获取要快得多它。
虽然这个类比远非完美,但它确实表明,当考虑大部分文档时,集合扫描有望比索引查找更有效。多键索引对每个文档有多个条目
当 mongod 在数组上构建索引时,它会在索引中为每个离散元素创建一个单独的条目。当您匹配数组元素中的值时,索引可以让您快速找到匹配的文档,但由于单个文档预计在索引中有多个条目,因此之后需要进行重复数据删除。
$elemMatch
进一步加剧了这些情况。由于索引包含单独索引字段的值,因此无法确定不同字段的值是否出现在索引的同一数组元素中,因此它必须加载每个文档以进行检查。
本质上,当将 elemMatch 与索引和匹配每个文档的查询一起使用时,mongod 节点将检查索引以识别匹配值,删除该列表的重复项,然后加载每个文档(可能按照索引中遇到的顺序) 查看单个数组值是否满足 elemMatch。
与 non-indexed 集合扫描执行相比,其中 mongod 必须按照在磁盘上遇到的顺序加载每个文档,并检查单个数组元素匹配是否满足 elemMatch,很明显索引如果大部分文档与查询匹配,查询将执行得更差。
TLDR:这是多键索引结合 $elemMatch
.
为什么会这样?
所以是 FETCH
阶段破坏了您的查询性能,不幸的是,这是预期的行为。
来自多键索引文档的 covered query 部分:
Multikey indexes cannot cover queries over array field(s).
意味着关于 sub-document 的所有信息都不在多键索引中 - 用一个字段计数是一个例外,它可以做得更好。但是这种情况下$elemMatch
还是在逼一个FETCH
阶段?为什么它只使用一个字段。
想象一下这个场景:
//doc1
{
"Actions" : [
{
"CampaignId" : "7fa216da-db22-44a9-9ea3-c987c4152ba1",
"ActionDatetime" : ISODate("1998-01-13T00:00:00.000Z"),
"ActionDescription" : "OPEN"
},
...
]
}
//doc2
{
"Actions" : {
"CampaignId" : "7fa216da-db22-44a9-9ea3-c987c4152ba1",
"ActionDatetime" : ISODate("1998-01-13T00:00:00.000Z"),
"ActionDescription" : "OPEN"
}
}
因为Mongo“扁平化”它索引的数组,一旦建立索引Mongo就无法区分这两个文档,但是$elemMatch
需要一个数组object要匹配它必须获取这些文件以确定哪一个符合条件。
这正是您面临的问题。
你能做什么?
好吧,没什么可悲的。我不确定您的查询有多动态,但解决此问题的唯一方法是预处理文档以包含对您的查询的“答案”。
我仍然很难相信 COLSCAN
比索引查询做得更好。我假设您正在匹配 collection 的很大一部分,同时 Actions
数组非常大。
我的建议是,性能将一直是一个问题,特别是如果您的查询将继续匹配 collection 的大部分内容,则重组
你的数据。只需将每个 Actions
条目保存为自己的文档即可。
{
"Actions" : {
"CampaignId" : "7fa216da-db22-44a9-9ea3-c987c4152ba1",
"ActionDatetime" : ISODate("1998-01-13T00:00:00.000Z"),
"ActionDescription" : "OPEN"
}
}
然后您的查询将被允许使用索引,您必须使用与计数不同的查询。 RecipientId
上的 distinct 听起来像是一个有效的选项。