MongoDB 2dsphere索引$geoWithin性能

15
我有一个包含地理JSON点坐标数据的集合,需要查询在某个区域内最新的10个条目。现在有100万条目,但将来会增加到约10倍。
当所需区域内有大量条目时,我的查询性能会急剧下降(情况3)。我目前使用的测试数据是随机的,但真实数据不会这样,因此不能仅基于区域维度选择另一个索引(如情况4)。
为了使其无论区域如何都能表现出可预测的性能,我该怎么做?
1.集合统计信息:
> db.randomcoordinates.stats()
{
    "ns" : "test.randomcoordinates",
    "count" : 1000000,
    "size" : 224000000,
    "avgObjSize" : 224,
    "storageSize" : 315006976,
    "numExtents" : 15,
    "nindexes" : 3,
    "lastExtentSize" : 84426752,
    "paddingFactor" : 1,
    "systemFlags" : 0,
    "userFlags" : 0,
    "totalIndexSize" : 120416128,
    "indexSizes" : {
        "_id_" : 32458720,
        "position_2dsphere_timestamp_-1" : 55629504,
        "timestamp_-1" : 32327904
    },
    "ok" : 1
}

2. 索引:

> db.randomcoordinates.getIndexes()
[
    {
        "v" : 1,
        "key" : {
            "_id" : 1
        },
        "ns" : "test.randomcoordinates",
        "name" : "_id_"
    },
    {
        "v" : 1,
        "key" : {
            "position" : "2dsphere",
            "timestamp" : -1
        },
        "ns" : "test.randomcoordinates",
        "name" : "position_2dsphere_timestamp_-1"
    },
    {
        "v" : 1,
        "key" : {
            "timestamp" : -1
        },
        "ns" : "test.randomcoordinates",
        "name" : "timestamp_-1"
    }
]

3. 使用2dsphere复合索引查找:

> db.randomcoordinates.find({position: {$geoWithin: {$geometry: {type: "Polygon", coordinates: [[[1, 1], [1, 90], [180, 90], [180, 1], [1, 1]]]}}}}).sort({timestamp: -1}).limit(10).hint("position_2dsphere_timestamp_-1").explain()
{
    "cursor" : "S2Cursor",
    "isMultiKey" : true,
    "n" : 10,
    "nscannedObjects" : 116775,
    "nscanned" : 283424,
    "nscannedObjectsAllPlans" : 116775,
    "nscannedAllPlans" : 283424,
    "scanAndOrder" : true,
    "indexOnly" : false,
    "nYields" : 4,
    "nChunkSkips" : 0,
    "millis" : 3876,
    "indexBounds" : {

    },
    "nscanned" : 283424,
    "matchTested" : NumberLong(166649),
    "geoTested" : NumberLong(166649),
    "cellsInCover" : NumberLong(14),
    "server" : "chan:27017"
}

4. 通过时间戳索引查找:

> db.randomcoordinates.find({position: {$geoWithin: {$geometry: {type: "Polygon", coordinates: [[[1, 1], [1, 90], [180, 90], [180, 1], [1, 1]]]}}}}).sort({timestamp: -1}).limit(10).hint("timestamp_-1").explain()
{
    "cursor" : "BtreeCursor timestamp_-1",
    "isMultiKey" : false,
    "n" : 10,
    "nscannedObjects" : 63,
    "nscanned" : 63,
    "nscannedObjectsAllPlans" : 63,
    "nscannedAllPlans" : 63,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 0,
    "indexBounds" : {
        "timestamp" : [
            [
                {
                    "$maxElement" : 1
                },
                {
                    "$minElement" : 1
                }
            ]
        ]
    },
    "server" : "chan:27017"
}

有人建议使用{timestamp: -1, position: "2dsphere"}索引,所以我也尝试了一下,但似乎表现还不够好。

5. 使用时间戳和2dsphere复合索引进行查找

> db.randomcoordinates.find({position: {$geoWithin: {$geometry: {type: "Polygon", coordinates: [[[1, 1], [1, 90], [180, 90], [180, 1], [1, 1]]]}}}}).sort({timestamp: -1}).limit(10).hint("timestamp_-1_position_2dsphere").explain()
{
    "cursor" : "S2Cursor",
    "isMultiKey" : true,
    "n" : 10,
    "nscannedObjects" : 116953,
    "nscanned" : 286513,
    "nscannedObjectsAllPlans" : 116953,
    "nscannedAllPlans" : 286513,
    "scanAndOrder" : true,
    "indexOnly" : false,
    "nYields" : 4,
    "nChunkSkips" : 0,
    "millis" : 4597,
    "indexBounds" : {

    },
    "nscanned" : 286513,
    "matchTested" : NumberLong(169560),
    "geoTested" : NumberLong(169560),
    "cellsInCover" : NumberLong(14),
    "server" : "chan:27017"
}

你能否澄清一下你说“因此,仅基于区域尺寸选择另一个索引(如情况4)是不可能的”时的意思?在我看来,无论区域大小如何,由于您只寻找最近的十个点,您始终可以通过时间戳索引获得更好的结果,其中scanAndOrder为false且nscanned最接近n。鉴于此,我建议首先使用时间戳,其次是位置创建复合索引,但是当前的mongo版本(2.4.6)将无法以所需的方式利用它:https://jira.mongodb.org/browse/SERVER-10801。 - jribnik
2
问题区域相当大,由于集合中的坐标是随机的,这意味着有很多坐标,因此时间戳索引更有效。当区域很小且条目很少时,使用时间戳索引时需要遍历所有条目,当该区域中少于10个条目时。在这种情况下,位置-时间戳复合索引显然是最快的,返回时间为2毫秒,而时间戳-位置索引将需要超过2000毫秒。我认为我需要(至少)针对不同类型的区域使用不同的索引。 - hleinone
2
我认为你的评论说得很到位。如果你要查询一个相当大的区域内最近的10个点,问题就从“查找区域内所有点并找出最近的10个”变成了“遍历最近的条目并检查它们是否在该区域内”。正如你所说,如果所有点的重要部分都在多边形内,第二种方法会快得多。 - 3rf
1
出于好奇,你能否运行这种类型的查询几次而不使用提示,然后运行explain来查看查询优化器正在使用哪个索引?MongoDB旨在测试和选择最佳索引以执行您的查询,因此您无需考虑这种事情(该功能可能会更好,但大多数情况下仍然有效)。 - 3rf
1
使用最初的索引,它选择了2dsphere-timestamp复合索引。我尝试在一个相当大的区域内运行了几次而没有使用提示,因此它并没有选择最优的时间戳索引。 - hleinone
显示剩余2条评论
3个回答

4
当我在寻找类似解决方法时,看到了这个问题。这是一个非常古老的问题,没有得到答复,如果其他人正在寻找类似情况的解决方法,我将尝试解释为什么提出的方法不适合应对当前任务,并说明如何优化这些查询。
在第一种情况下,扫描大量的项目是完全正常的。让我来解释一下为什么:
当Mongodb构建复合索引“position_2dsphere_timestamp_-1”时,实际上会创建一个B树来保存所有包含在位置键中的几何图形, 在本例中为Points,对于此B树中每个不同的值,都会创建另一个B树以按降序保存时间戳。这意味着,除非您的条目非常(我是说非常)接近彼此,否则辅助B树将只保持一个条目,查询性能几乎与仅在位置字段上具有索引相同。除了MongoDB可以使用二级B树上的时间戳值而不是将实际文档带到内存并检查时间戳。
在构建复合索引“timestamp_-1_position_2dsphere”的其他场景中也适用相同的情况。两个条目在毫秒精度下同时输入的可能性非常小。因此,在此情况下,是的,我们的数据按时间戳字段排序,但是我们还有许多其他的B树,每个不同的时间戳值都只持有一个条目。因此,应用geoWithin筛选器的性能表现不佳,因为它必须检查每个条目直到达到限制为止。
那么如何使这些查询执行良好?个人建议尽可能将字段放在地理空间字段的前面。但主要的技巧是保留另一个字段,比如“createdDay”,它会保存以天为精度的数字。如果需要更高的精度,您也可以使用小时级别精度,但代价是性能降低,这完全取决于项目的需求。索引应如下所示:{createdDay:-1,position:“2dsphere”}。现在,创建在同一天的每个文档都将存储并按同一2dsphere B树索引进行排序。因此,MongoDB将从当前日期开始,因为它应该是索引中最大的值,并在保持今天创建的文档位置的B树上进行索引扫描。如果它找到至少10个文档,就会停止并返回这些文档;如果没有,它就会移动到前一天,以此类推。这种方法应该极大地提高您的性能。
希望这能对你有所帮助。

基于这个前提:“除非你的条目非常接近彼此(我是说非常接近)”,如果我在用于查询大面积的次要点属性中近似物品的位置,会发生什么? - Paolo Sanchi
这个做法可以提高性能,但牺牲了精度。但是使用此方法,您要么会损失很多精度,要么无法获得足够的性能提升。相反,您可以尝试使用谷歌的S2算法,在MongoDb的B树之上构建自己的地理索引,这样您就可以对精度和性能有绝对的控制。 - Anıl Şimşek

1
你尝试过在数据集上使用聚合框架吗?
你需要的查询将类似于以下内容:
db.randomcoordinates.aggregate(
    { $match: {position: {$geoWithin: {$geometry: {type: "Polygon", coordinates: [[[1, 1], [1, 90], [180, 90], [180, 1], [1, 1]]]}}}}},
    { $sort: { timestamp: -1 } },
    { $limit: 10 }
);

很遗憾,聚合框架在生产版本中还没有explain功能,因此只有当它产生巨大的时间差异时,您才会知道它的作用。如果您可以构建源代码,似乎最近一个月已经加入了该功能:https://jira.mongodb.org/browse/SERVER-4504。同时,看起来它将在计划于下周二(2013年10月15日)发布的Dev build 2.5.3中出现。


1

无论区域如何,我该怎么做才能使它的表现更可预测?

$geoWithin 的效率并不是 Θ(1)。据我所知,平均情况下它的效率为 Θ(n)(考虑算法最多需要检查 n 个点,最少需要检查 10 个)。

然而,我肯定会对坐标集合进行一些预处理,以确保最近添加的坐标首先被处理,从而给您更好的机会获得 Θ(10) 的效率(听起来除了使用 position_2dsphere_timestamp_-1 还应该这样做)!

有人建议使用 {timestamp: -1, position: "2dsphere"} 索引,因此我也尝试了一下,但似乎效果不太好。

(请参见对初始问题的回答。)

此外,以下内容可能会有所帮助!

MongoDB 优化策略

希望能对您有所帮助!

TL;DR即使您玩弄索引,也无法从$geoWithin中获得更高的效率,除非您重写它。话虽如此,您仍然可以专注于优化索引性能并重写该函数!

hleinone,这有帮助吗? - Drew

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