如何提高 MongoDB 查找查询性能?
How to improve MongoDB find query performance?
我有一个集合 collection1
,其中包含如下文档:
{
_id: 123,
field1: "test",
array1: [
{
array2: [
{
field2: 1,
object1: {
field3: "test"
}
}
]
}
]
}
我正在尝试从按字段 field1
、field2
和 field3
过滤的集合中获取所有文档。我的查询看起来像:
db.collection1.find(
{
field1: "test",
array1: {
$elemMatch: {
array2: {
$elemMatch: {
field2: {
$gte: 1
},
"object1.field3": "test"
}
}
}
}
})
此集合包含约 125,000 个文档。鉴于查询必须浏览两个嵌套数组以进行过滤,人们会认为此查询很慢。它是,大约需要 30-40 秒。
因此,为了提高其性能,我为所有 3 个字段创建了一个索引,看起来像 db.collection1.createIndex({"array1.array2.object1.field3": 1, "array1.array2.field2": 1, "field1": 1});
使用索引,查询速度提高了一倍,大约需要 15 秒。然而,这还是太慢了。我想让查询 <5 秒。关于如何提高速度的任何想法?如果有帮助,我可以为两个查询添加查询计划程序(使用和不使用索引)。
编辑:我尝试使用索引中字段的不同排序的所有 6 种可能组合,它们都有相同的结果。因此,我更加关注查询的查询计划器和执行统计信息,我注意到了一些事情:
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "db.collection1",
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"indexName" : "fields_index"
}
}
},
"executionStats" : {
"executionSuccess" : true,
"executionTimeMillis" : "15602.784",
"planningTimeMillis" : "0.248",
"executionStages" : {
"stage" : "FETCH",
"nReturned" : "0",
"executionTimeMillisEstimate" : "15602.130",
"inputStages" : [
{
"stage" : "IXSCAN",
"nReturned" : "300220",
"executionTimeMillisEstimate" : "87.616",
"indexName" : "fields_index"
},
{
"nReturned" : "0",
"executionTimeMillisEstimate" : "0.018"
}
]
}
},
"serverInfo" : {
"host" : "mongo-instance",
"port" : 27017,
"version" : "3.6.0"
},
"ok" : 1
似乎 FETCH
阶段花费的时间特别长,而不是索引扫描。这是为什么?此外,使用我正在使用的参数,查询意味着 return 没有结果。 FETCH
阶段执行 return 0 个结果,但索引扫描 returns 300220 个文档。为什么?
这里的顺序不是问题,问题是你没有完全理解 Mongo 索引数组的方式。
Mongo 的方法是展平数组并分别索引每个元素,这意味着看起来像这样的元素(如下)仍将与索引匹配,因此进入 FETCH
阶段比需要的大得多。
{
_id: 123,
field1: "test",
array1: [
{
array2: [
{
field2: 1,
object1: {
field3: "no-test"
}
},
{
field2: 2,
object1: {
field3: "test"
}
}
]
}
]
}
那我们能做什么呢?
首先让我们以更自然的方式对索引进行排序,将 test
作为复合索引中的字段字段。
索引 array2
中的完整元素,正如我提到的,现在每个键都被展平,这使得索引在您查询整个元素时具有冗余。所以不是这个:
"array1.array2.object1.field3": 1, "array1.array2.field2": 1
你应该做的:
"array1.array2": 1
这显然会创建一个更大的索引树,这可能会影响更新的性能。如果嵌套对象太大,第 2 步可能不适合您,但它可以提高您的查询速度。
这是主题的变体。
我创建了一个包含 200,000 个文档的集合。 100,000 人的 field1
设置为 NOTtest
,因此他们甚至连第一次晋级都没有。在其他 100,000 个中,每个元素都有一个长度为 2 的 array1
,并且在每个元素中有一个长度为 3 的 array2
。这些叶元素中的 20,000 个中有一个被设置为 object1.field3:"test"
和 field2:4
使其 >1 并满足两个条件查询(OP 具有 gte
1,我将其设为 gt
1 以使其更清楚)。因此,200,000 个文档中只有 5 个可以满足所需的查询。在 MacBookPro 上,以下查询 在 2.4 秒内生成 5 个文档,没有索引 。诀窍是使用 $map
“潜入”数组以到达所需的目标数组,然后使用 $filter
生成填充数组或空数组。空数组表示不匹配,将在下一阶段过滤掉。
这种方法的额外优势是 仅 具有匹配字段的子文档。 $elemMatch
的挑战在于 returns 数组中的子文档与 整个 数组的匹配可能包含不匹配的子文档。这些必须在管道中进一步过滤或在客户端代码中进行 post 处理。
db.foo.aggregate([
{$match: {field1: "test"}},
{$project: {
XX:{$map: {input: "$array1", as:"z1", in:
{QQ: {$filter: {input: "$$z1.array2",
as: "z2",
cond: {$and:[
{$eq:["$$z2.object1.field3", "test"]},
{$gt:["$$z2.field2",1]}
]}
}}
}
}}
}}
,{$match: {$expr: {
// total of length of QQ array(s) must be > 0
$gt:[ {$reduce: {input: "$XX",
initialValue: 0,
in: {$add:["$$value",{$size: "$$this.QQ"}]}
}}, 0]
}
}}
]);
随着 material 大幅减少,您现在可以 $unwind
和 $project
以及根据您的需要定制输出。
$map
可以“链接”到任意深度:
var r = [
{array1: [
{array2: [
{array3: [
{array4: [
{f: "X"},
{f: "A"},
{f: "A"}
]}
]
}
]}
]}
,
{array1: [
{array2: [
{array3: [
{array4: [
{f: "X"},
{f: "X"}
]}
]
}
]}
]}
]
db.foo2.drop();
db.foo2.insert(r);
c = db.foo2.aggregate([
{$project: {XX:
{$map: {input: "$array1", as:"z1", in:
{$map: {input: "$$z1.array2", as: "z2", in:
{$map: {input: "$$z2.array3", as: "z3", in:
{QQ: {$filter: {input: "$$z3.array4",
as: "z4",
cond: {$eq:["$$z4.f","A"]}
}}
}
}}
}}
}}
}}
]);
无可否认,输出的数组有点重,但这种方法避免了深度倍数 $unwind
,后者可能会按数量级爆炸数据集。
我发现了问题。我在最初的问题中没有提到的是我正在使用 AWS 的 DocumentDB 服务,它具有 MongoDB 兼容性。根据 this,在“$ne、$nin、$nor、$not、$exists 和 $elemMatch Indexing”部分下,它表示 DocumentDB 不支持在 $elemMatch
中使用索引。我的查询使用索引的原因是它用于 field1
,不在 $elemMatch
之下。但是,它对其他两个不起作用,因此它仍然必须扫描数千个结果并按 field2
和 field3
.
进行过滤
我修复它的方法是重写我的查询。根据 MongoDB documentation,我的查询不需要使用 $elemMatch
。所以我的查询现在看起来像:
db.collection1.find(
{
field1: "test",
"array1.array2.field2": {
$gte: 1
},
"array1.array2.object1.field3": "test"
})
查询在功能上完全相同,但这样它实际上使用了索引。 运行 查询现在需要 <1 秒。感谢大家的帮助和宝贵建议!
我有一个集合 collection1
,其中包含如下文档:
{
_id: 123,
field1: "test",
array1: [
{
array2: [
{
field2: 1,
object1: {
field3: "test"
}
}
]
}
]
}
我正在尝试从按字段 field1
、field2
和 field3
过滤的集合中获取所有文档。我的查询看起来像:
db.collection1.find(
{
field1: "test",
array1: {
$elemMatch: {
array2: {
$elemMatch: {
field2: {
$gte: 1
},
"object1.field3": "test"
}
}
}
}
})
此集合包含约 125,000 个文档。鉴于查询必须浏览两个嵌套数组以进行过滤,人们会认为此查询很慢。它是,大约需要 30-40 秒。
因此,为了提高其性能,我为所有 3 个字段创建了一个索引,看起来像 db.collection1.createIndex({"array1.array2.object1.field3": 1, "array1.array2.field2": 1, "field1": 1});
使用索引,查询速度提高了一倍,大约需要 15 秒。然而,这还是太慢了。我想让查询 <5 秒。关于如何提高速度的任何想法?如果有帮助,我可以为两个查询添加查询计划程序(使用和不使用索引)。
编辑:我尝试使用索引中字段的不同排序的所有 6 种可能组合,它们都有相同的结果。因此,我更加关注查询的查询计划器和执行统计信息,我注意到了一些事情:
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "db.collection1",
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"indexName" : "fields_index"
}
}
},
"executionStats" : {
"executionSuccess" : true,
"executionTimeMillis" : "15602.784",
"planningTimeMillis" : "0.248",
"executionStages" : {
"stage" : "FETCH",
"nReturned" : "0",
"executionTimeMillisEstimate" : "15602.130",
"inputStages" : [
{
"stage" : "IXSCAN",
"nReturned" : "300220",
"executionTimeMillisEstimate" : "87.616",
"indexName" : "fields_index"
},
{
"nReturned" : "0",
"executionTimeMillisEstimate" : "0.018"
}
]
}
},
"serverInfo" : {
"host" : "mongo-instance",
"port" : 27017,
"version" : "3.6.0"
},
"ok" : 1
似乎 FETCH
阶段花费的时间特别长,而不是索引扫描。这是为什么?此外,使用我正在使用的参数,查询意味着 return 没有结果。 FETCH
阶段执行 return 0 个结果,但索引扫描 returns 300220 个文档。为什么?
这里的顺序不是问题,问题是你没有完全理解 Mongo 索引数组的方式。
Mongo 的方法是展平数组并分别索引每个元素,这意味着看起来像这样的元素(如下)仍将与索引匹配,因此进入 FETCH
阶段比需要的大得多。
{
_id: 123,
field1: "test",
array1: [
{
array2: [
{
field2: 1,
object1: {
field3: "no-test"
}
},
{
field2: 2,
object1: {
field3: "test"
}
}
]
}
]
}
那我们能做什么呢?
首先让我们以更自然的方式对索引进行排序,将
test
作为复合索引中的字段字段。索引
array2
中的完整元素,正如我提到的,现在每个键都被展平,这使得索引在您查询整个元素时具有冗余。所以不是这个:
"array1.array2.object1.field3": 1, "array1.array2.field2": 1
你应该做的:
"array1.array2": 1
这显然会创建一个更大的索引树,这可能会影响更新的性能。如果嵌套对象太大,第 2 步可能不适合您,但它可以提高您的查询速度。
这是主题的变体。
我创建了一个包含 200,000 个文档的集合。 100,000 人的 field1
设置为 NOTtest
,因此他们甚至连第一次晋级都没有。在其他 100,000 个中,每个元素都有一个长度为 2 的 array1
,并且在每个元素中有一个长度为 3 的 array2
。这些叶元素中的 20,000 个中有一个被设置为 object1.field3:"test"
和 field2:4
使其 >1 并满足两个条件查询(OP 具有 gte
1,我将其设为 gt
1 以使其更清楚)。因此,200,000 个文档中只有 5 个可以满足所需的查询。在 MacBookPro 上,以下查询 在 2.4 秒内生成 5 个文档,没有索引 。诀窍是使用 $map
“潜入”数组以到达所需的目标数组,然后使用 $filter
生成填充数组或空数组。空数组表示不匹配,将在下一阶段过滤掉。
这种方法的额外优势是 仅 具有匹配字段的子文档。 $elemMatch
的挑战在于 returns 数组中的子文档与 整个 数组的匹配可能包含不匹配的子文档。这些必须在管道中进一步过滤或在客户端代码中进行 post 处理。
db.foo.aggregate([
{$match: {field1: "test"}},
{$project: {
XX:{$map: {input: "$array1", as:"z1", in:
{QQ: {$filter: {input: "$$z1.array2",
as: "z2",
cond: {$and:[
{$eq:["$$z2.object1.field3", "test"]},
{$gt:["$$z2.field2",1]}
]}
}}
}
}}
}}
,{$match: {$expr: {
// total of length of QQ array(s) must be > 0
$gt:[ {$reduce: {input: "$XX",
initialValue: 0,
in: {$add:["$$value",{$size: "$$this.QQ"}]}
}}, 0]
}
}}
]);
随着 material 大幅减少,您现在可以 $unwind
和 $project
以及根据您的需要定制输出。
$map
可以“链接”到任意深度:
var r = [
{array1: [
{array2: [
{array3: [
{array4: [
{f: "X"},
{f: "A"},
{f: "A"}
]}
]
}
]}
]}
,
{array1: [
{array2: [
{array3: [
{array4: [
{f: "X"},
{f: "X"}
]}
]
}
]}
]}
]
db.foo2.drop();
db.foo2.insert(r);
c = db.foo2.aggregate([
{$project: {XX:
{$map: {input: "$array1", as:"z1", in:
{$map: {input: "$$z1.array2", as: "z2", in:
{$map: {input: "$$z2.array3", as: "z3", in:
{QQ: {$filter: {input: "$$z3.array4",
as: "z4",
cond: {$eq:["$$z4.f","A"]}
}}
}
}}
}}
}}
}}
]);
无可否认,输出的数组有点重,但这种方法避免了深度倍数 $unwind
,后者可能会按数量级爆炸数据集。
我发现了问题。我在最初的问题中没有提到的是我正在使用 AWS 的 DocumentDB 服务,它具有 MongoDB 兼容性。根据 this,在“$ne、$nin、$nor、$not、$exists 和 $elemMatch Indexing”部分下,它表示 DocumentDB 不支持在 $elemMatch
中使用索引。我的查询使用索引的原因是它用于 field1
,不在 $elemMatch
之下。但是,它对其他两个不起作用,因此它仍然必须扫描数千个结果并按 field2
和 field3
.
我修复它的方法是重写我的查询。根据 MongoDB documentation,我的查询不需要使用 $elemMatch
。所以我的查询现在看起来像:
db.collection1.find(
{
field1: "test",
"array1.array2.field2": {
$gte: 1
},
"array1.array2.object1.field3": "test"
})
查询在功能上完全相同,但这样它实际上使用了索引。 运行 查询现在需要 <1 秒。感谢大家的帮助和宝贵建议!