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

How to improve MongoDB find query performance?

我有一个集合 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 阶段花费的时间特别长,而不是索引扫描。这是为什么?此外,使用我正在使用的参数,查询意味着 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"
          }
        }
      ]
    }
  ]
}

那我们能做什么呢?

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

  2. 索引 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 之下。但是,它对其他两个不起作用,因此它仍然必须扫描数千个结果并按 field2field3.

进行过滤

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

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

查询在功能上完全相同,但这样它实际上使用了索引。 运行 查询现在需要 <1 秒。感谢大家的帮助和宝贵建议!