【问题标题】:How to optimize MongoDB query with both $gt and $lte?如何使用 $gt 和 $lte 优化 MongoDB 查询?
【发布时间】:2012-10-14 23:24:42
【问题描述】:

我有以下查询,有点像反向范围查找:

db.ip_ranges.find({ $and: [{ start_ip_num: { $lte: 1204135028 } }, { end_ip_num: { $gt: 1204135028 } }] })

仅使用 $lte 标识符运行时,查询会立即返回。但是当我在同一个查询中同时使用 $gt 和 $lte 时,它​​非常慢(以秒为单位)。

start_ip_num 和 end_ip_num 字段均已编入索引。

我该如何优化这个查询?

编辑

当我在查询中使用 explain() 函数时,我得到以下信息:

{
    "cursor" : "BtreeCursor start_ip_num_1",
    "nscanned" : 452336,
    "nscannedObjects" : 452336,
    "n" : 1,
    "millis" : 2218,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {
        "start_ip_num" : [
            [
                -1.7976931348623157e+308,
                1204135028
            ]
        ]
    }
}

编辑 2

添加复合索引后,explain() 函数返回以下内容:

{
    "cursor" : "BtreeCursor start_ip_num_1_end_ip_num_1",
    "nscanned" : 431776,
    "nscannedObjects" : 1,
    "n" : 1,
    "millis" : 3433,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {
        "start_ip_num" : [
            [
                -1.7976931348623157e+308,
                1204135028
            ]
        ],
        "end_ip_num" : [
            [
                1204135028,
                1.7976931348623157e+308
            ]
        ]
    }
}

但是,性能仍然很差(以秒为单位)。

【问题讨论】:

  • .find({...}).explain() 是一个很好的起点。正如 Wes Freeman 所问,您在 {start_ip_nm: 1, end_ip_num:1} 上有索引吗?
  • 您应该解决的可能有帮助的一件事是使用单个查询选择器对象而不是使用$anddb.ip_ranges.find({start_ip_num: {$lte: 1204135028}, end_ip_num: {$gt: 1204135028}})
  • B-tree 必须扫描 >400k 条目才能找到匹配项。试试盒子查询,看看是否有帮助。我敢打赌你会在一秒钟之内得到它。

标签: mongodb mongodb-query


【解决方案1】:

因此,在 Mongo 中不建议使用双范围查询。我假设您有一个包含{start_ip_num: 1, end_ip_num: 1} 的索引。

如果这不能让你足够接近(如果你有足够的数据从第一个字段返回,通常它仍然很慢,因为它必须进行大量 B-tree 扫描),你可以做一个技巧使用 2D 框查询来解决这个问题(一次仅适用于两个范围)。

基本上,您将 2D 地理索引放在包含数组中两个点的字段上,例如 [start_ip, end_ip],并给它一个足够高的最小值/最大值,这样它就不会达到极限默认情况下只有 -180/180。

最后,使用边界查询,范围从 min 到 $lte 值在盒子的一个角上,而 gt 和 max 值在盒子的另一个角上。语法见http://www.mongodb.org/display/DOCS/Geospatial+Indexing#GeospatialIndexing-BoundsQueries

看起来像这样:

db.ip_ranges.find({ip_range:{$within:{$box:[[0, 1204135028], [1204135028, max]]}}});

其中 max 是您可以拥有的最大 ip。

我已经有一段时间没有看到这个了,所以这个框可能是错误的,但这个概念是合理的,它使双范围查询的性能比使用常规的两字段 B-tree 索引好一点。始终不到一秒钟(尽管通常是几百毫秒),而使用常规索引则需要几秒钟——我想我当时有数亿个文档,但已经有一段时间了,所以请用这些记住的基准盐。我敢肯定,结果会因您的数据和范围大小而有很大差异。

更新:您可能想尝试使用bits 设置,尝试一个小数字和一个大数字,看看它是否有区别。对我来说,它似乎并没有平均影响查询。语法见http://www.mongodb.org/display/DOCS/Geospatial+Indexing#GeospatialIndexing-CreatingtheIndex

【讨论】:

  • 是的,我已经在 start_ip_num 和 end_ip_num 字段上有两个单一索引......让我试试你的解决方案......谢谢!
  • 首先在两个字段上尝试使用复合索引。请记住,mongo 每次查询只能使用一个索引。在问题中发布您的 explain() 结果——这可能会说明问题。
  • 我试过 $box 查询。它可以工作并在 1 秒左右返回。这太奇怪了。我只有大约 100 万份文件。看起来像这样的范围操作应该是相当直接的,但 Mongo 并没有很好地处理它。
  • 这再奇怪不过了。当我删除所有索引时,查询返回更快(800 毫秒)。什么
  • 我发现一篇文章涵盖了我遇到的同样问题。最快的查询必须在没有索引的情况下完成。 :( 知道为什么吗???
【解决方案2】:

经过大量的实验和研究,我发现了这个:

https://groups.google.com/forum/?fromgroups=#!topic/mongodb-user/IUwOzWsc0Sg

我可以通过这个查询将查询降低大约 200-300 毫秒,并且删除所有索引你必须删除所有索引才能工作!!!):

db.ip_ranges.find({start_ip_num: {$lte: 1204135028}, end_ip_num: {$gt: 1204135028}}).limit(1)

别问我为什么。我无法解释。如果您有兴趣,我正在使用 MongoDB 从 MaxMind 构建 GeoIP 数据库。

【讨论】:

  • Mongo 不能很好地进行双范围查询。您的无索引结果不会在规模上保持快速,但如果这只是一个 GeoIP 列表,它可能不会增长太多。此外,您从未提到您只需要一个结果。你可以只做 findOne 而不是 .limit(1)。我很好奇为什么你需要搜索这么大的范围只是为了找到一个范围。如果你要从你的 IP 中减去一些合理的数量(为了到达网络的底部/顶部或其他东西),你可能会得到更快的结果,为范围查询指定一个最小值/最大值。
【解决方案3】:

诀窍是使用 $lte 和排序。我将查询缩短到几毫秒。

我遇到了完全相同的问题 - 查找与特定 IP 地址匹配的 CIDR 块。我还尝试使用 $gte 和 $lte,得到了 10 秒的响应时间。

我以不同的方式解决了这个问题。请注意,MaxMind 数据库中的 CIDR 块(IP 地址范围)不重叠。每个 IP 地址最多匹配一个结果。因此,您需要做的就是找到具有小于特定 IP 地址的最大 start_ip_num 的 CIDR 块。然后在应用程序代码中验证 end_ip_num 是否大于特定 IP 地址。

这是代码(使用节点 MongoDB 客户端):

// Convert IP address to base 10.
var ipToDecimal = function (ipAddress) {
  var split = ipAddress.split('.');
  return (split[0] * 16777216) + (split[1] * 65536) + (split[2] * 256) + (+split[3]);
};

var ipAddress = '1.2.3.4';
var ipDecimal = ipToDecimal(ipAddress);

db.ip_addresses.find({start_ip_num: {$lte: ipDecimal}}, {_id: 0, country_code: 1, end_ip_num: 1}, {limit: 1, sort: [['start_ip_num', -1]]}).toArray(function (error, ipAddresses) {
  if (ipAddresses[0] && ipAddresses[0]['end_ip_num'] >= ipDecimal) {
    console.log('IP address found: ', ipAddresses[0]['country_code']);
  } else {
    console.log('IP address not found.');
  }
});

确保在 start_ip_num 上创建索引。

【讨论】:

    【解决方案4】:

    根据Ip2location website,可以不用范围查询,用mongodb快速查询ip地址。 在 mongodb { ip_to: 1 } 上只创建一个索引,并使用以下命令查询 ip:

    db.collection_name.find({ ip_to: { $gte : ip_integer } }).sort({ ip_end: 1 }).limit(1)
    

    通过这个配置,我获得了 1 毫秒的查询时间和 600 万个文档集合。

    【讨论】:

    • 太棒了。这对我有用。对于大约 330 万个文档,查询平均需要大约 50 毫秒。顺便说一句,在我的代码中,我添加了一个检查以确保返回的文档的 start_ip_num 小于或等于查询的 IP 地址。这是为了确保确实有一个 ip 范围的文档可以满足等效范围查询对应项
    猜你喜欢
    • 2011-05-31
    • 2021-12-29
    • 2016-12-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-19
    • 2015-01-21
    • 2015-01-09
    相关资源
    最近更新 更多