MySQL BETWEEN 查询不使用索引

Posted

技术标签:

【中文标题】MySQL BETWEEN 查询不使用索引【英文标题】:MySQL BETWEEN query not using index 【发布时间】:2020-10-25 12:23:15 【问题描述】:

我的表中有 geoip 数据,network_start_ipnetwork_end_ipvarbinary(16) 列,结果为 INET6_ATON(ip_start/end) 作为值。其他 2 列是纬度和经度。

CREATE TABLE `ipblocks` (
 `network_start_ip` varbinary(16) NOT NULL,
 `network_last_ip` varbinary(16) NOT NULL,
 `latitude` double NOT NULL,
 `longitude` double NOT NULL,
 KEY `network_start_ip` (`network_start_ip`),
 KEY `network_last_ip` (`network_last_ip`),
 KEY `idx_range` (`network_start_ip`,`network_last_ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

如您所见,我创建了 3 个用于测试的索引。为什么我的(很简单的)查询

SELECT 
    latitude, longitude
FROM
    ipblocks b
WHERE
    INET6_ATON('82.207.219.33') BETWEEN b.network_start_ip AND b.network_last_ip

不使用任何这些索引?

查询需要大约 3 秒,这对于在生产环境中使用来说太长了。

【问题讨论】:

InnoDB 表确实需要PRIMARY KEY 【参考方案1】:

此查询是否为您提供正确的结果?在您搜索整数表示时,您的开始/结束 IP 似乎存储为二进制字符串。 我首先要确保 network_start_ip 和 network_last_ip 是无符号 INT 字段,具有 IP 地址的整数表示。这是假设您仅使用 IPv4:

CREATE TABLE ipblocks_int AS
SELECT
    INET_ATON(network_start_ip) as network_start_ip,
    INET_ATON(network_last_ip) as network_last_ip,
    latitude,
    longitude
FROM ipblocks

然后使用 (network_start_ip,network_last_ip) 作为主键。

【讨论】:

INET6_ATON() 返回一个二进制字符串。你误解了这个问题。 糟糕,很抱歉。我确实忽略了 INET6_ATON 中的“6”。仍然建议尝试使用该对作为主键。 @Alex - 编辑您的答案以包括 6PRIMARY KEY。 (如果您需要更多声望点,请学习教程;很容易获得 100 点。)【参考方案2】:

它不起作用,因为引用了两列——这真的很难优化。假设没有重叠的 IP 范围,您可以将查询重组为:

SELECT b.*
FROM (SELECT b.*
      FROM ipblocks b
      WHERE b.network_start_ip <= INET6_ATON('82.207.219.33')
      ORDER BY b.network_start_ip DESC
      LIMIT 1
     ) b
WHERE INET6_ATON('82.207.219.33') <= network_last_ip;

内部查询应使用ipblocks(network_start_ip) 上的索引。外层查询只比较一行,不需要任何索引。

或如:

SELECT b.*
FROM (SELECT b.*
      FROM ipblocks b
      WHERE b.network_last_ip >= INET6_ATON('82.207.219.33')
      ORDER BY b.network_end_ip ASC
      LIMIT 1
     ) b
WHERE network_last_ip <= INET6_ATON('82.207.219.33');

这将使用(network_last_ip) 上的索引。 mysql(我认为 MariaDB)在升序排序方面比降序排序做得更好。

【讨论】:

这不是解决方案,但它有助于加快速度。 LIMIT 1 是不可能的,因为network_start_ip 必须小于INET6_ATON('82.207.219.33') 并且有许多更小。如果不使用 LIMIT 1 并与外部查询一起使用,结果是正确的,查询速度最高可达 0.8 秒。仍然不是很好,但这是一个开始:) 谢谢。【参考方案3】:

这是一个棘手的问题。没有简单的解决方案。

之所以难,是因为它是有效的

   start <= 123  AND
   last  >= 123

无论有哪些索引可用,优化器都将使用其中一个或另一个。使用INDEX(start, ...),它将选择start &lt;= 123,它将扫描索引的第一部分。另一个子句也是如此。其中一个扫描超过一半的索引,另一个扫描较少 - 但不足以少到值得使用索引。在某些情况下,将其移至 PRIMARY KEY 会有所帮助,但这并不值得。

归根结底,无论您以INDEXPRIMARY KEY 的方式做什么,大多数 IP 常量都会导致查询时间超过 1.5 秒。

您的开始/最后一个 IP 范围是否重叠?如果是这样,那就增加了复杂性。特别是,重叠可能会使 Gordon 的LIMIT 1 无效。

我的解决方案需要非重叠区域。 IP 中的任何间隙都需要“无主”范围的 IP。这是因为只有一个start_ip; last_ip 暗示小于表中下一项的开始。请参阅http://mysql.rjweb.org/doc.php/ipranges(它包括 IPv4 和 IPv6 的代码。)

同时,DOUBLE 用于 lat/lng 是多余的:http://mysql.rjweb.org/doc.php/latlng#representation_choices

【讨论】:

【参考方案4】:

感谢Gordon Linoff 我为我的问题找到了最佳查询。

SELECT b.* FROM 
  (SELECT b.* FROM ipblocks b WHERE b.network_start_ip <= INET6_ATON('82.207.219.33') 
                              ORDER BY b.network_start_ip DESC LIMIT 1 ) 
b WHERE INET6_ATON('82.207.219.33') <= network_last_ip

现在我们在内部查询中选择 smallerINET6_ATON(82.207.219.33) 的块,但我们对它们进行排序降序这使我们能够再次使用LIMIT 1

查询响应时间现在为 0.002 到 0.004 秒。太好了!

【讨论】:

假设有记录且不重叠,我认为对last_ip的外块测试是没有必要的。 SELECT b.* FROM ipblocks b WHERE b.network_start_ip &lt;= INET6_ATON('82.207.219.33') ORDER BY b.network_start_ip DESC LIMIT 1

以上是关于MySQL BETWEEN 查询不使用索引的主要内容,如果未能解决你的问题,请参考以下文章

MySQL的WHERE语句中BETWEEN与IN的使用教程

mysql join不使用'between'运算符的索引

MySQL 中的索引,用于按 DESC、BETWEEN 和几个可能的字段集进行查询

MYSQL 存储过程如何取得一个表的查询结果?

MySQL:索引具有多个 BETWEEN 表达式的 WHERE 子句

为啥 MySql 不自动优化 BETWEEN 查询?