多键索引上的慢速范围查询

6

我有一个名为post的MongoDB集合,其中包含 3500万 个对象。该集合定义了两个二级索引,如下所示。

> db.post.getIndexKeys()
[
    {
        "_id" : 1
    },
    {
        "namespace" : 1,
        "domain" : 1,
        "post_id" : 1
    },
    {
        "namespace" : 1,
        "post_time" : 1,
        "tags" : 1  // this is an array field
    }
]

我希望以下查询仅通过namespacepost_time进行过滤,而不必扫描所有对象,能在合理的时间内运行。

>db.post.find({post_time: {"$gte" : ISODate("2013-04-09T00:00:00Z"), "$lt" : ISODate("2013-04-09T01:00:00Z")}, namespace: "my_namespace"}).count()
7408

然而,使用MongoDB检索结果至少需要十分钟,并且令人好奇的是,它可以扫描 7000万 个对象来完成此任务,这是通过 explain 函数得出的。

> db.post.find({post_time: {"$gte" : ISODate("2013-04-09T00:00:00Z"), "$lt" : ISODate("2013-04-09T01:00:00Z")}, namespace: "my_namespace"}).explain()
{
    "cursor" : "BtreeCursor namespace_1_post_time_1_tags_1",
    "isMultiKey" : true,
    "n" : 7408,
    "nscannedObjects" : 69999186,
    "nscanned" : 69999186,
    "nscannedObjectsAllPlans" : 69999186,
    "nscannedAllPlans" : 69999186,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 378967,
    "nChunkSkips" : 0,
    "millis" : 290048,
    "indexBounds" : {
        "namespace" : [
            [
                "my_namespace",
                "my_namespace"
            ]
        ],
        "post_time" : [
            [
                ISODate("2013-04-09T00:00:00Z"),
                ISODate("292278995-01--2147483647T07:12:56.808Z")
            ]
        ],
        "tags" : [
            [
                {
                    "$minElement" : 1
                },
                {
                    "$maxElement" : 1
                }
            ]
        ]
    },
    "server" : "localhost:27017"
}

对象数量和扫描数量之间的差异一定是由标签数组的长度引起的(它们都等于2)。但我不明白为什么post_time 过滤器没有使用索引。

你能告诉我可能错过了什么吗?

(我正在使用拥有24个核心和96 GB RAM的优秀机器。我使用的是MongoDB 2.2.3版本。)


命名空间的基数非常低吗? - Sammaye
目前只有一个独特的“命名空间”值,这就是我正在使用的值。 - Eser Aygün
是的,这就是为什么MongoDB必须首先限制第一个字段,以便它获取所有my_namespace,然后获取该日期之间的所有文档等等。尝试重新排序索引,使post_time排在第一位。 - Sammaye
索引树的“my_namespace”分支下不也包含“post_time”值吗?为什么要在缩小范围之前开始扫描? - Eser Aygün
2
尽管btree包含了两个字段,但在MongoDB中,复合索引的工作方式是扫描所有命名空间以查找该值,然后缩小范围到日期。嗯,我正在尝试寻找一篇能够很好地解释它的文档页面,但是从谷歌搜索结果来看,没有真正好的索引内部文档页面,不过这篇文章可能会有所帮助:http://emptysquare.net/blog/optimizing-mongodb-compound-indexes/。 - Sammaye
显示剩余3条评论
1个回答

3

在这个问题中找到了答案:MongoDB范围查询中$lt和$gt的顺序

我的索引是一个多键索引(在tags上),我正在运行一个范围查询(在post_time上)。显然,在这种情况下,MongoDB不能使用范围的两侧作为过滤器,因此它只选择第一个出现的$gte子句。因为我的下限恰好是最低的post_time值,所以MongoDB开始扫描所有对象。

不幸的是,这还不是全部。试图解决这个问题,我也创建了非多键索引,但MongoDB坚持使用错误的索引。这使我认为问题出在其他地方。最后,我不得不放弃多键索引,创建一个没有tags字段的索引。现在一切都很好。


我从未意识到 $gt$lt 以及多键索引的这些内容,真是个好发现! - Sammaye
1
使用cursor.hint也可以是解决方案,使mongodb使用其他索引 (http://docs.mongodb.org/manual/reference/method/cursor.hint/#cursor.hint) - rudi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接