MySQL BETWEEN 查询不使用索引
Posted
技术标签:
【中文标题】MySQL BETWEEN 查询不使用索引【英文标题】:MySQL BETWEEN query not using index 【发布时间】:2020-10-25 12:23:15 【问题描述】:我的表中有 geoip 数据,network_start_ip
和 network_end_ip
是 varbinary(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 - 编辑您的答案以包括 6
和 PRIMARY 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 <= 123
,它将扫描索引的第一部分。另一个子句也是如此。其中一个扫描超过一半的索引,另一个扫描较少 - 但不足以少到值得使用索引。在某些情况下,将其移至 PRIMARY KEY
会有所帮助,但这并不值得。
归根结底,无论您以INDEX
或PRIMARY 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
现在我们在内部查询中选择 smaller 比 INET6_ATON(82.207.219.33)
的块,但我们对它们进行排序降序这使我们能够再次使用LIMIT 1
。
查询响应时间现在为 0.002 到 0.004 秒。太好了!
【讨论】:
假设有记录且不重叠,我认为对last_ip的外块测试是没有必要的。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
以上是关于MySQL BETWEEN 查询不使用索引的主要内容,如果未能解决你的问题,请参考以下文章
MySQL 中的索引,用于按 DESC、BETWEEN 和几个可能的字段集进行查询