【问题标题】:MongoDB - searching in array using $elemMatch slower with index than withoutMongoDB - 使用 $elemMatch 在数组中搜索比没有索引慢
【发布时间】:2020-11-28 00:23:59
【问题描述】:

我有一个包含 500k 文档的集合,其结构如下:

{
    "_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 编写查询时,操作比我根本不使用 Multikey 索引时慢得多:

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。

【问题讨论】:

  • $elemMatch 如果您只测试数组中的一个字段,则它是多余的。没有 elemMatch 的索引性能如何比较?
  • @Joe 是的,我在问题中提到了这一点,但实际上我需要查询多个字段,所以无论如何我都需要 $elemMatch,我只是使用一个字段作为例子。没有 $elemMatch 的性能更好(4 秒),因为规划器使用 COUNT_SCAN 并且不需要 FETCH 任何元素。但是,COLLSCAN 性能仍然更好(1s)。我不知道问题出在 $elemMatch 的使用上,还是多键索引的问题,或者两者兼而有之。

标签: arrays mongodb performance indexing mongodb-indexes


【解决方案1】:

这里的问题是双重的:

  • 每个文档都与您的查询匹配
    考虑将索引类比为图书馆中的目录。如果你想找一本书,在目录中查找它可以让你直接走到拿着它的书架上,这比从第一个书架开始搜索书籍要快得多(当然除非它确实在那第一个架子)。但是,如果您想获得图书馆中所有的书籍,直接将它们从书架上拿下来要比检查每本书的目录然后去拿要快得多。 虽然这个类比远非完美,但它确实表明,在考虑大量文档时,可以预期集合扫描比索引查找更有效。

  • 多键索引对每个文档都有多个条目
    当 mongod 在数组上构建索引时,它会在索引中为每个离散元素创建一个单独的条目。当您从数组元素中匹配一个值时,索引可以让您快速找到匹配的文档,但由于单个文档预计在索引中有多个条目,因此之后需要进行重复数据删除。

$elemMatch 进一步加剧了这些问题。由于索引包含单独索引字段的值,因此无法确定不同字段的值是否出现在索引的同一数组元素中,因此它必须加载每个文档以进行检查。

本质上,当将 elemMatch 与索引和匹配每个文档的查询一起使用时,mongod 节点将检查索引以识别匹配值,删除该列表的重复数据,然后加载每个文档(可能按索引中遇到的顺序)以查看单个数组值是否满足 elemMatch。

与 mongod 必须按照在磁盘上遇到的顺序加载每个文档并检查单个数组元素匹配是否满足 elemMatch 的非索引集合扫描执行相比,索引查询显然会执行得更差如果大部分文档与查询匹配。

【讨论】:

  • 感谢@Joe 的回复。对于如何提高此类查询的性能,您有什么建议吗?
【解决方案2】:

TLDR:这是与$elemMatch 结合的多键索引的预期行为。

为什么会这样?

所以是FETCH 阶段破坏了您的查询性能,不幸的是,这是预期的行为。

来自多键索引文档的covered query 部分:

多键索引不能覆盖对数组字段的查询。

意味着有关子文档的所有信息都不在多键索引中 - 一个字段的计数是一个例外,它可以做得更好。但在这种情况下,$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 需要一个数组对象来匹配它必须获取这些文档以确定哪一个符合条件。 这正是您面临的问题。

你能做什么?

没有太多可悲的。我不确定您的查询有多动态,但解决此问题的唯一方法是预处理文档以包含查询的“答案”。

我仍然很难相信COLSCAN 比索引查询做得更好。我假设您正在匹配您收藏的很大一部分以及 Actions 数组非常大的事实。

我的建议是性能将一直是一个问题,特别是如果您的查询将继续匹配大部分集合是重组 你的数据。只需将每个 Actions 条目保存为它自己的文档即可。

{
   "Actions" : {
            "CampaignId" : "7fa216da-db22-44a9-9ea3-c987c4152ba1",
            "ActionDatetime" : ISODate("1998-01-13T00:00:00.000Z"),
            "ActionDescription" : "OPEN"
   }
}

然后您的查询将被允许使用索引,您必须使用与计数不同的查询。 RecipientId 上的 distinct 听起来像是一个有效的选项。

【讨论】:

  • 感谢您的解释。只是为了确保我理解,您建议将 Actions 文档保存在单独的集合中?这种方法的问题在于,除了按 Actions 对象中的字段进行过滤外,我还需要按父文档中的其他字段进行过滤。因此,如果我将它们保存在单独的集合中,我将不得不加入不同的集合,这也是我希望避免的事情。 (另请注意,您错过了 doc2 Actions 字段中的 [ ])
  • 我没有错过 doc2 中的 []。我故意使用这种结构差异来解释为什么 FETCH 是强制的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-01-13
  • 2012-04-29
  • 2013-06-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多