我有一个具有以下结构的表:
CREATE TABLE `geo_ip` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`start_ip` int(10) unsigned NOT NULL,
`end_ip` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `start_ip` (`start_ip`),
KEY `end_ip` (`end_ip`),
KEY `start_end` (`start_ip`,`end_ip`),
KEY `end_start` (`end_ip`,`start_ip`)) ENGINE=InnoDB;
Run Code Online (Sandbox Code Playgroud)
MySQL的似乎是无法使用我的大多数查询的指标,作为where
第一个使用between
落在介于start_ip
和end_ip
:
select * from geo_ip where 2393196360 between start_ip and end_ip;
+----+-------------+--------+------+-------------------------------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+-------------------------------------+------+---------+------+---------+-------------+
| 1 | SIMPLE | geo_ip | ALL | start_ip,end_ip,start_end,end_start | NULL | NULL | NULL | 2291578 | Using where |
+----+-------------+--------+------+-------------------------------------+------+---------+------+---------+-------------+
Run Code Online (Sandbox Code Playgroud)
该表有几百万条记录.我试图扩大该表,通过移除start_ip
和end_ip
列,并为每一个可能值创建一行start_ip
,并end_ip
作为id
,然后查询id
.虽然这大大提高了查询性能,但它导致表大小从不到1千兆字节增长到数十千兆字节(表中显然还有其他列).
还有什么可以提高查询性能?我可以以某种方式更改查询,还是可以不同地索引列以导致命中?或者也许是我还没有想到的东西?
编辑:
奇怪的是,索引用于某些值.例如:
explain select * from geo_ip where 3673747503 between start_ip and end_ip;
+----+-------------+--------+-------+-------------------------------------+--------+---------+------+-------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+-------+-------------------------------------+--------+---------+------+-------+-------------+
| 1 | SIMPLE | geo_ip | range | start_ip,end_ip,start_end,end_start | end_ip | 4 | NULL | 19134 | Using where |
+----+-------------+--------+-------+-------------------------------------+--------+---------+------+-------+-------------+
Run Code Online (Sandbox Code Playgroud)
Jes*_*run 11
不知道为什么,但添加order by子句和限制查询似乎总是导致索引命中,并在几毫秒而不是几秒内执行.
explain select * from geo_ip where 2393196360 between start_ip and end_ip order by start_ip desc limit 1;
+----+-------------+--------+-------+-----------------+----------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+-------+-----------------+----------+---------+------+--------+-------------+
| 1 | SIMPLE | geo_ip | range | start_ip,end_ip | start_ip | 4 | NULL | 975222 | Using where |
+----+-------------+--------+-------+-----------------+----------+---------+------+--------+-------------+
Run Code Online (Sandbox Code Playgroud)
这对我来说已经足够了,虽然我很想知道为什么优化器决定不在其他情况下使用索引的原因.
我遇到了同样的问题。由于没有人回答“为什么”,并且我已经弄清楚了,因此在这里我将为所有未来的读者提供解释。
首先,让我们剖析查询。
where 2393196360 between start_ip and end_ip
Run Code Online (Sandbox Code Playgroud)
真正意思
where start_ip <= C and end_ip >= C
Run Code Online (Sandbox Code Playgroud)
因此引擎将首先使用on索引start_ip, end_ip
来获取start_ip小于C的所有行,然后进一步过滤掉end_ip也大于C的行。
当引擎寻找start_ip <= C
,并且C
其值足够大以至于大多数或所有start_ips都小于C时,此“第一遍”将导致很多行。每当C
IP范围较高的IP 都将发生这种情况。
现在,这是要实现的主要内容:我们的数据集的制作方式是,对于每个start_ip,只有一个end_ip值,并且保证该end_ip值小于下一条记录的start_ip值。我们正在划分范围,并且分区不重叠。但是,在一般情况下,当涉及到两个表字段时,就不必如此!
因此,在“首次通过”之后,引擎将不得不浏览所有匹配的记录,start_ip <= C
以确保end_ip >= C
即使有索引,它们也都匹配。end_ip
在我们的案例中,拥有作为复合指数的一部分并没有多大作用;仅当end_ip
每个值都有多个值start_ip
,但只有1个时,它才有帮助。举一个例子,假设列中填充了以下数据:
start_ip end_ip
1 10001
1 10002
1 10003
------------
2 10001
2 10002
2 10003
------------
...
------------
9999 10001
9999 10002
9999 10003
Run Code Online (Sandbox Code Playgroud)
如果您使用进行查询start_ip <= 10000 AND end_ip >= 10000
,请注意所有行均与表达式匹配。另一方面,在本例中start_ip <= C AND end_ip >= C
,由于ip数据的结构方式,对于ip-ranges数据集,我们可以保证只有ONE记录将与任何表达式匹配。特别是start_ip
在所有匹配的记录中,具有最大价值的记录start_ip <= C
。这就是为什么在这种情况下添加ORDER BY和LIMIT 1的原因,并且我认为这是最干净的解决方案。
编辑:我刚刚注意到,在某些情况下,添加ORDER BY start_ip DESC和LIMIT子句可能不够。如果您使用数据中任何范围都未覆盖的值(例如,使用127.0.0.1或192.168。*这样的私有IP)运行查询,则引擎仍会查看与该start_ip <= C
表达式匹配的所有记录,并且查询将慢一点 这是因为由于没有记录与表达式(end_ip >= C
)的第二部分匹配,所以LIMIT 1子句永远不会插入。
我发现的解决方案是使用联接构造查询,以强制引擎首先获取start_ip
where 值最大的记录start_ip <= C
,然后才检查end_ip是否也> =C。像这样:
SELECT *
FROM
( select id FROM geo_ip WHERE start_ip <= C ORDER BY start_ip DESC LIMIT 1 ) limit_ip
INNER JOIN geo_ip ON limit_ip.id = geo_ip.id
WHERE geo_ip.end_ip >= C
Run Code Online (Sandbox Code Playgroud)
无论C
表中的范围是否覆盖特定的ip,此查询都将执行一次查找,并且只需要打开一个索引start_ip
(以及id
主键)。