我有一个具有以下结构的表:
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;
MySQL似乎无法在我的大多数查询中使用索引,因为where
子句使用的between
介于start_ip
和end_ip
之间:< / p>
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 |
+----+-------------+--------+------+-------------------------------------+------+---------+------+---------+-------------+
该表有几百万条记录。我尝试通过移除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 |
+----+-------------+--------+-------+-------------------------------------+--------+---------+------+-------+-------------+
答案 0 :(得分: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 |
+----+-------------+--------+-------+-----------------+----------+---------+------+--------+-------------+
这对我来说已经足够了,虽然我很想知道优化器为什么决定不在另一种情况下使用索引的原因。
答案 1 :(得分:5)
我遇到了同样的问题。既然没有人回答“为什么”,我想出来了,我会在这里给所有未来的读者写一个解释。
首先,让我们剖析一下这个问题。
where 2393196360 between start_ip and end_ip
真的意味着
where start_ip <= C and end_ip >= C
因此引擎将首先使用start_ip, end_ip
上的索引来获取start_ip小于C的所有行,然后进一步过滤掉end_ip也大于C的行。
当引擎查找start_ip <= C
时,C
是一个足够大的值,使得大多数或所有start_ip都小于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
如果您使用start_ip <= 10000 AND end_ip >= 10000
运行查询,请注意所有行都与表达式匹配。
另一方面,在我们的情况下,通过我们的ip-ranges数据集,我们可以保证只有一条记录可以匹配任何start_ip <= C AND end_ip >= C
表达式,这要归功于ip数据的结构方式。特别是start_ip
中具有最大值start_ip <= C
的记录,与start_ip <= C
匹配的记录。这就是为什么在这种情况下添加ORDER BY和LIMIT 1的原因,并且在我看来是最干净的解决方案。
编辑:我刚刚注意到在某些情况下添加ORDER BY start_ip DESC和LIMIT子句可能还不够。如果您运行的查询的值未被数据中的任何范围覆盖,例如使用私有IP(如127.0.0.1或192.168。*),引擎仍会查看所有匹配的记录end_ip >= C
表达式,查询速度很慢。那是因为没有记录与表达式的第二部分(start_ip
)匹配,LIMIT 1子句永远不会开始。
我找到的解决方案是使用连接构造查询,以强制引擎首先获取start_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
的最大值的记录,然后检查是否end_ip也是&gt; = C.像这样:
C
此查询将执行单个查找,无论表中的范围是否涵盖特定的ip start_ip
,它只需要id
上的单个索引(以及{{ 1}}作为主键)。
答案 2 :(得分:3)
BETWEEN查询的最佳索引是B-TREE索引。请参阅该主题的MySQL docs。
ALTER TABLE myTable ADD INDEX myIdx UNSING BTREE (myCol)
答案 3 :(得分:1)
如果你为start_ip创建一个索引,为end_ip创建一个索引,我发现我可以得到与Jeshurun的结果不同的结果,而不使用同一个表的内连接:
select a.* from geo_ip a inner join geo_ip b on a.id=b.id where 2393196360 >= a.start_ip and 2393196360 <= b.end_ip limit 1;
你也会发现MySQL使用部分索引而不是报告全索引扫描,这对我来说更令人感到欣慰。
答案 4 :(得分:0)
添加索引会有所帮助。
注意:如果您的查询类似于
INDEX(x, y)
,x
不会改善效果,但y
和Rectangle
会有两个单独的索引。