【问题标题】:MySQL: Optimal index for between queriesMySQL:查询之间的最佳索引
【发布时间】:2014-03-02 08:33:13
【问题描述】:

我有一个结构如下的表:

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 子句使用了介于start_ipend_ip 之间的between

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_ipend_ip 列来扩展表,并为start_ipend_ip 的每个可能值创建一行作为id,然后查询id。虽然这极大地提高了查询性能,但它导致表大小从不到 1 GB 增长到数十 GB(该表显然还有其他列)。

还可以做些什么来提高查询性能?我可以以某种方式更改查询,还是可以对列进行不同的索引以导致命中?或者可能是我还没有想到的?

编辑:

奇怪的是,索引用于某些值。例如:

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 |
+----+-------------+--------+-------+-------------------------------------+--------+---------+------+-------+-------------+

【问题讨论】:

  • 哪个 MySQL 版本? This fiddle(诚然表中没有数据)似乎试图使用索引start_end
  • mysql Ver 14.14 Distrib 5.5.35, for debian-linux-gnu (x86_64) using readline 6.2
  • 如果你暂时不需要写信给表格,可以试试ANALYZE TABLE
  • 有趣。我在那个小提琴中添加了一些数据,它似乎仍然使用 start_end 索引。不知道为什么我的结果不同。我会试试ANALYZE TABLE,谢谢。
  • 我认为这取决于这些值的基数,如果某个值的命中次数过多,优化器会选择另一个索引。是的,我迟到了。

标签: mysql indexing


【解决方案1】:

添加索引会有所帮助。

注意:如果你的查询是这样的

where x between a and b AND y between c and d

INDEX(x, y)不会提高性能,但 xy 的两个单独索引会。

【讨论】:

  • 对此进行扩展:虽然 MySQL 通常每个“连接”(EXPLAIN 输出中的行)只使用 1 个索引,但 index merge optimization 有一个例外
【解决方案2】:

我刚刚遇到了同样的问题。由于没有人回答“为什么”,而且我想通了,我会在这里写一个解释给所有未来的读者。

首先,让我们剖析查询。

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 &lt;= C,而C 是一个足够大的值以致大多数或所有start_ips 都小于C 时,这个“第一遍”将导致很多行。每次C 是 IP 范围较高端的 IP 时都会发生这种情况。

现在,要实现的主要内容是:我们的数据集的制作方式是,对于每个 start_ip,只有一个 end_ip 值,并且这个 end_ip 值保证低于下一条记录的 start_ip 值.我们正在对范围进行分区,并且分区不重叠。但是,在一般情况下,当涉及到两个表字段时,不必如此!

因此,在“第一次通过”之后,引擎将不得不查看与 start_ip &lt;= C 匹配的所有记录,以确保它们也与 end_ip &gt;= C 匹配,尽管有索引。在我们的例子中,将end_ip 作为复合索引的一部分并没有多大作用;只有当我们为每个值start_ip 有多个值end_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 &lt;= 10000 AND end_ip &gt;= 10000 运行查询,请注意所有行都与表达式匹配。 另一方面,在我们的例子中,由于我们的 ip-ranges 数据集,我们保证只有一个记录将匹配任何start_ip &lt;= C AND end_ip &gt;= C 表达式,这要归功于 ip 数据的结构方式。特别是在所有匹配start_ip &lt;= C 的记录中,start_ip 的值最大的记录。这就是为什么在这种情况下添加 ORDER BY 和 LIMIT 1 的原因,在我看来,这是最干净的解决方案。


编辑:我刚刚注意到在某些情况下添加 ORDER BY start_ip DESC 和 LIMIT 子句可能还不够。如果您使用数据中的任何范围均未涵盖的值运行查询,例如使用 127.0.0.1 或 192.168.* 等私有 IP,引擎仍会查看所有匹配的记录start_ip &lt;= C 表达式,查询会很慢。这是因为没有记录与表达式的第二部分 (end_ip &gt;= C) 匹配,因此 LIMIT 1 子句永远不会生效。

我找到的解决方案是使用join构造查询,以强制引擎首先获取start_ip其中start_ip &lt;= 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

此查询将执行单次查找,无论特定 ip C 是否被表中的范围覆盖,并且它只需要 start_ip 上的单个索引(以及 id 作为主键)。

【讨论】:

    【解决方案3】:

    不知道为什么,但是在查询中添加 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 |
    +----+-------------+--------+-------+-----------------+----------+---------+------+--------+-------------+
    

    现在这对我来说已经足够好了,尽管我很想知道优化器决定在另一种情况下不使用索引的原因。

    【讨论】:

    • 我有一个类似的查询,并且 order by with limit 也使它快了 10 倍。很想知道是否有人可以对此有所了解。使用 EXPLAIN 的查询计划显示几乎相同(使用索引,使用 where),但使用 order/limit 显示“范围”类型,而其他类型为“全部”类型
    【解决方案4】:

    BETWEEN 查询的最佳索引是 B-TREE 索引。有关该主题,请参阅 MySQL docs

    ALTER TABLE myTable ADD INDEX myIdx USING BTREE (myCol)
    

    【讨论】:

      【解决方案5】:

      如果您为 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 使用部分索引而不是报告全索引扫描,这对我来说更令人欣慰。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-04-22
        • 2019-02-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-13
        相关资源
        最近更新 更多