如何提高MongoDB查找查询性能?



>我有一个集合collection1,其中包含这样的文档:

{
_id: 123,
field1: "test",
array1: [
{
array2: [
{
field2: 1,
object1: {
field3: "test"
}
}
]
}
]
}

我正在尝试按字段field1field2field3过滤集合中的所有文档。我的查询如下所示:

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阶段是花费极长时间的阶段,而不是索引扫描。为什么?此外,使用我正在使用的参数,查询旨在不返回任何结果。FETCH阶段返回 0 个结果,但索引扫描返回 300220 个文档。为什么?

我发现了这个问题。我在最初的问题中没有提到的是,我正在使用AWS的DocumentDB服务,该服务具有MongoDB兼容性。据此,在"$ne,$nin,$nor,$not,$exists和$elemMatch索引"部分下,它说DocumentDB不支持使用带有$elemMatch的索引。我的查询使用索引的原因是因为它将其用于field1,这不在$elemMatch之下。但是,它不适用于其他两个,因此它仍然必须对数千个结果进行扫描并按field2field3进行过滤。

我修复它的方法是重写我的查询。根据MongoDB文档,我不需要在查询中使用$elemMatch。所以我的查询现在看起来像:

db.collection1.find(
{
field1: "test",
"array1.array2.field2": {
$gte: 1
}, 
"array1.array2.object1.field3": "test"
})

查询在功能上完全相同,但这种方式实际上使用索引。现在,运行查询需要 <1 秒。感谢大家的所有帮助和好建议!

顺序不是这里的问题,问题是你没有完全理解 Mongo 如何索引数组。

Mongo这样做的方式是扁平化数组并单独索引每个元素,这意味着看起来像这样(下图)的元素仍将与索引匹配,从而使FETCH阶段比它需要的要大得多。

{
_id: 123,
field1: "test",
array1: [
{
array2: [
{
field2: 1,
object1: {
field3: "no-test"
}
},
{
field2: 2,
object1: {
field3: "test"
}
}
]
}
]
}

那么我们能做什么呢?

  1. 首先,让我们以更自然的方式对索引进行排序,方法是将test作为复合索引中的字段字段。

  2. 索引
  3. array2中的完整元素,现在正如我提到的,每个键都是扁平的,这使得索引在您查询整个元素时具有冗余。所以取而代之的是:

"array1.array2.object1.field3": 1, "array1.array2.field2": 1

你应该做:

"array1.array2": 1

这显然会创建一个更大的索引树,这可能会影响更新的性能。如果该嵌套对象太大,第 2 步可能不适合您,但它会提高您的查询速度。

这是主题的变体。 我创建了一个包含 200,000 个文档的集合。 100,000field1设置为NOTtest,因此他们甚至没有进行第一次削减。 在其他 100,000 个中,每个array1的长度为 2,每个array2的长度为 3。 这些叶元素的 20,000 个中有一个设置为object1.field3:"test"field2:4使其>1 并满足两个条件查询(OPgte1,我将其设为 1gt以使其更清晰)。 因此,200,000 个文档中只有 5 个可以满足所需的查询。 在 MacBookPro 上,以下查询在 2.4 秒内生成 5 个文档,没有索引。 诀窍是使用$map"潜入"数组以到达所需的目标数组,然后使用$filter生成填充数组或空数组。 空数组意味着不匹配,并在下一阶段被过滤掉。

此方法的另一个优点是返回具有匹配字段的子文档。$elemMatch的挑战在于数组中子文档的匹配会返回整个数组,其中可能包括不匹配的子文档。 这些必须在管道中进一步筛选或在客户端代码中进行后处理。

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]
}
}}
]);

随着材料的大幅减少,您现在可以$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,这可能会使数据集爆炸几个数量级。

最新更新