【问题标题】:Mysql where between query optimizationMysql where 查询优化
【发布时间】:2017-07-19 17:25:35
【问题描述】:

下面是自治系统编号数据库的格式(从this site!下载解析)。

range_start  range_end  number  cc  provider
-----------  ---------  ------  --  -------------------------------------
   16778240   16778495   56203  AU  AS56203 - BIGRED-NET-AU Big Red Group
   16793600   16809983   18144      AS18144

745465 总行

普通查询如下所示:

select * from table where 3232235520 BETWEEN range_start AND range_end

工作正常,但我查询了大量的 IP 来检查他们的 AS 信息,这最终会占用太多的电话和时间。

分析器快照:

Blackfire profiler snapshot

我有两个索引:

  1. id 列
  2. range_start 和 range_end 列上的组合索引作为唯一行。

问题:

  1. 有没有办法在一次查询中查询大量 IP?
    • 多个where (IP between range_start and range_end) OR where (IP between range_start and range_end) OR ... 有效,但我无法获取 IP -> 行映射或为哪个 IP 检索哪些行。
  2. 有什么建议可以改变数据库结构以优化查询速度和减少时间?

任何帮助将不胜感激!谢谢!

【问题讨论】:

  • select * from table where 3232235520 BETWEEN range_start AND range_end 我不明白这是一个普通的查询。 3232235520 来自哪里?也就是说,如果 (range_start,range_end) 被索引,那么这些查询应该非常快。
  • @Strawberry:3232235520 是 Amar 想要搜索的 IP 地址的十进制表示。 IPv4 地址是 32 位整数。 0xC0A80000192.168.0.0。 OP 的问题是,即使单个查询的执行速度非常快,OP 仍在运行大量这些单独的查询。一次一个IP地址。而且我们知道处理 RBAR(逐行处理)很慢。运行一个惊人的快速查询是非常昂贵的。而且还是很慢。
  • @Strawberry 我在查询前将IP转换为十进制形式3232235520
  • 如果您的桌子很大,请参阅my solution

标签: php mysql database database-design


【解决方案1】:

可以查询多个 IP 地址。我们可以采取几种方法。假设range_startrange_end 被定义为整数类型。

对于合理数量的 IP 地址,我们可以使用内联视图:

 SELECT i.ip, a.*
   FROM (           SELECT 3232235520 AS ip
          UNION ALL SELECT 3232235521
          UNION ALL SELECT 3232235522
          UNION ALL SELECT 3232235523
          UNION ALL SELECT 3232235524
          UNION ALL SELECT 3232235525
        ) i
   LEFT 
   JOIN ip_to_asn a
     ON a.range_start <= i.ip
    AND a.range_end   >= i.ip
  ORDER BY i.ip

这种方法适用于合理数量的 IP 地址。可以使用更多 UNION ALL SELECT 扩展内联视图以添加额外的 IP 地址。但这并不一定适用于“巨大”的数字。

当我们变得“巨大”时,我们将在 MySQL 中遇到限制... SQL 语句的最大大小受max_allowed_packet 限制,可能会出现SELECT 的数量限制.

内联视图可以替换为先构建的临时表。

 DROP TEMPORARY TABLE IF EXISTS _ip_list_;
 CREATE TEMPORARY TABLE _ip_list_ (ip BIGINT NOT NULL PRIMARY KEY) ENGINE=InnoDB;
 INSERT INTO _ip_list_ (ip) VALUES (3232235520),(3232235521),(3232235522),...;
 ...
 INSERT INTO _ip_list_ (ip) VALUES (3232237989),(3232237990);

然后引用临时表代替内联视图:

 SELECT i.ip, a.*
   FROM _ip_list_ i
   LEFT
   JOIN ip_to_asn a
     ON a.range_start <= i.ip
    AND a.range_end   >= i.ip
  ORDER BY i.ip ;

然后删除临时表:

 DROP TEMPORARY TABLE IF EXISTS _ip_list_ ;

其他一些注意事项:

搅动数据库连接会降低性能。建立和断开连接会产生大量开销。如果应用程序重复连接和断开连接,如果它对每个发出的 SQL 语句都这样做,那么这种开销会很明显。

并且运行单个 SQL 语句也有开销...该语句必须发送到服务器,语句解析语法,从语义评估,选择执行计划,执行计划,准备结果集,返回结果集到客户端。这就是为什么处理 set 比处理 row 更有效的原因。与将语句发送到数据库并让它一举处理集合相比,处理 RBAR(逐行处理)可能非常慢。

但有一个权衡。有了巨大的个集合,事情可能又开始变慢了。

即使您可以在每条语句中处理两个 IP 地址,这减半 需要执行的语句数。如果您在每条语句中使用 20 个 IP 地址,则可以将语句数量减少到每次需要一行的数量的 5%。


并且已经在(range_start,range_end) 上定义的复合索引适用于此查询。


跟进

正如 Rick James 在评论中指出的那样,我之前所说的“适当”的索引并不理想。

我们可以稍微不同地编写查询,这样可以更有效地利用该索引。

如果 (range_start,range_end) 是 UNIQUE(或 PRIMARY)KEY,那么即使存在“重叠”范围,也会为每个 IP 地址返回一行。 (前面的查询将返回所有 range_start 和 range_end 与 IP 地址重叠的行。)

 SELECT t.ip, a.*
   FROM ( SELECT s.ip
               , s.range_start
               , MIN(e.range_end) AS range_end
            FROM ( SELECT i.ip
                        , MAX(r.range_start) AS range_start
                     FROM _ip_list_ i
                     LEFT
                     JOIN ip_to_asn r
                       ON r.range_start <= i.ip
                    GROUP BY i.ip
                 ) s
            LEFT
            JOIN ip_to_asn e
              ON e.range_start = s.range_start
             AND e.range_end  >= s.ip
           GROUP BY s.ip, s.range_start
        ) t
   LEFT
   JOIN ip_to_asn a
     ON a.range_start = t.range_start
    AND a.range_end   = t.range_end
  ORDER BY t.ip ;

通过这个查询,对于最内层的内联视图查询s,优化器或许能够有效地利用前导列range_start的索引,快速识别出range_start的“最高”值(即小于或等于 IP 地址)。但是有了外部连接,以及i.ip 上的 GROUP BY,我真的需要查看 EXPLAIN 输出;这只是猜测优化器可能会做什么;重要的是优化器实际上做了什么。)

然后,对于内联视图查询e,MySQL 可能能够更有效地利用(range_start,range_end) 上的复合索引,因为第一列上的相等谓词,以及 MIN 聚合上的不相等条件第二列。

对于最外层的查询,由于两列的相等谓词,MySQL 肯定能够有效地利用复合索引。

这种形式的查询可能会显示改进的性能,或者性能可能会在手篮中陷入困境。 EXPLAIN 的输出应该可以很好地指示正在发生的事情。我们希望在 Extra 列中看到“Using index for group-by”,而我们只想在最外层的查询中看到 ORDER BY 的“Using filesort”。 (如果我们删除 ORDER BY 子句,我们不希望在 Extra 列中看到“Using filesort”。)


另一种方法是利用 SELECT 列表中的相关子查询。当结果集包含大量行时,相关子查询的执行可能会变得很昂贵。但是这种方法可以为某些用例提供令人满意的性能。

此查询依赖ip_to_asn 表中没有重叠范围,当存在重叠范围时,此查询不会产生预期结果。

 SELECT t.ip, a.*
   FROM ( SELECT i.ip
               , ( SELECT MAX(s.range_start)
                     FROM ip_to_asn s
                    WHERE s.range_start <= i.ip
                 ) AS range_start
               , ( SELECT MIN(e.range_end)
                     FROM ip_to_asn e
                    WHERE e.range_end >= i.ip
                 ) AS range_end
            FROM _ip_list_ i
        ) r
   LEFT 
   JOIN ip_to_asn a
     ON a.range_start = r.range_start
    AND a.range_end   = r.range_end

为了说明为什么重叠范围会成为这个查询的问题,给出一个完全愚蠢的虚构示例

range_start  range_end 
-----------  ---------
       .101       .160
       .128       .244

给定 IP 地址 .140MAX(range_start) 子查询将找到 .128MIN(range_end) 子查询将找到 .160,然后外部查询将尝试找到匹配行 range_start=.128 AND range_end=.160。而那一行根本不存在。

【讨论】:

  • 是的,range_start 和 range_end 是整数类型。我已经测试了两种指定的方法,它们都可以正常工作。我正在实施临时表创建方法,因为它比内联视图花费更少的时间来获得更多的 IP。对于超过 100 个 IP:内联视图耗时 53.4641 秒 临时表耗时 16 秒
  • @AmarMyana:您报告的结果看起来很合理。对于临时表方法,我一定会使用多行插入。并确保在比较性能时包括额外语句创建临时表并填充它的成本(时间)。 (内联视图在实现派生表时执行类似的操作。)
  • 这样的复合索引并没有你想的那么有用。优化器不知道是否可能存在重叠范围。
  • @RickJames:MySQL 可以使用前导列 range_start 进行范围扫描。这并不理想,因为 IP 位于索引的中点,这只会消除一半的行。需要检查满足range_start 条件的所有行。我希望 MySQL 可以使用索引中的第二列来评估 range_end 上的条件,即使它是对未消除的索引条目的完整扫描。这将避免查找基础表中的页面。如果这是 InnoDB 表,如果集群键以 range_start 作为前导列,...
  • (续)二级索引并没有给我们带来太多收益。否则,我们所能期望的最好的(给定当前的表定义)将是一个覆盖索引。一些真正“低”范围开始的查询应该比“高”范围开始的查询执行得更好。在平均分布情况下,平均而言,查找 IP 需要检查一半的条目。不理想。但这比全面扫描每一行要好。我们可以提出一些优化建议,但我的回答实际上主要是关于避免处理 RBAR,一次处理一个令人痛苦的 IP。
【解决方案2】:

这是问题here 的重复,但我不会投票关闭它,因为该问题中接受的答案不是很有帮助; Quassnoi 的答案要好得多(但它只链接到解决方案)。

线性索引无助于解析范围数据库。解决方案是使用地理空间索引(在 MySQL 和其他 DBMS 中可用)。一个额外的复杂因素是 MySQL 地理空间索引仅适用于二维(当您拥有一维数据集时),因此您需要将其映射到二维。

因此:

CREATE TABLE IF NOT EXISTS `inetnum` (
  `from_ip` int(11) unsigned NOT NULL,
  `to_ip` int(11) unsigned NOT NULL,
  `netname` varchar(40) default NULL,
  `ip_txt` varchar(60) default NULL,
  `descr` varchar(60) default NULL,
  `country` varchar(2) default NULL,
  `rir` enum('APNIC','AFRINIC','ARIN','RIPE','LACNIC') NOT NULL default 'RIPE',
  `netrange` linestring NOT NULL,
  PRIMARY KEY  (`from_ip`,`to_ip`),
  SPATIAL KEY `rangelookup` (`netrange`)
) ENGINE=MyISAM DEFAULT CHARSET=ascii;

可能会填充......

INSERT INTO inetnum
(from_ip, to_ip
 , netname, ip_txt, descr, country
 , netrange)
VALUES
(INET_ATON('127.0.0.0'), INET_ATON('127.0.0.2') 
 , 'localhost','127.0.0.0-127.0.0.2', 'Local Machine', '.',
GEOMFROMWKB(POLYGON(LINESTRING(
   POINT(INET_ATON('127.0.0.0'), -1), 
   POINT(INET_ATON('127.0.0.2'), -1),
   POINT(INET_ATON('127.0.0.2'), 1), 
   POINT(INET_ATON('127.0.0.0'), 1),
   POINT(INET_ATON('127.0.0.0'), -1))))
);

那么你可能想创建一个函数来包装相当冗长的 SQL....

DROP FUNCTION `netname2`//
CREATE DEFINER=`root`@`localhost` FUNCTION `netname2`(p_ip VARCHAR(20) CHARACTER SET ascii) RETURNS varchar(80) CHARSET ascii
   READS SQL DATA
   DETERMINISTIC
BEGIN
  DECLARE l_netname varchar(80);

  SELECT CONCAT(country, '/',netname)
    INTO l_netname
  FROM inetnum
  WHERE MBRCONTAINS(netrange, GEOMFROMTEXT(CONCAT('POINT(', INET_ATON(p_ip), ' 0)')))
  ORDER BY (to_ip-from_ip)
  LIMIT 0,1;

  RETURN l_netname;
END

因此:

SELECT netname2('127.0.0.1');

./localhost

其中使用了索引:

id  select_type     table   type    possible_keys   key     key_len     ref     rows    Extra
1   SIMPLE  inetnum     range   rangelookup     rangelookup     34  NULL    1   Using where; Using filesort

(在我在这里使用的非常低规格的 VM 上,从组合的 APNIC、AFRINIC、ARIN、RIPE 和 LACNIC 数据集中查找记录大约需要 10 毫秒)

【讨论】:

    【解决方案3】:
    1. 您可以使用 MySQL 比较 IP 范围。此问题可能包含您正在寻找的答案:MySQL check if an IP-address is in range?
    SELECT * FROM TABLE_NAME WHERE (INET_ATON("193.235.19.255") BETWEEN INET_ATON(ipStart) AND INET_ATON(ipEnd));
    
    1. 您可能希望为您的数据库编制索引。这优化了搜索数据库所需的时间,类似于您在教科书背面找到的索引,但对于数据库:

      ALTER TABLE `table` ADD INDEX `name` (`column_id`)
      

    编辑:显然INET_ATON 不能用于索引数据库,因此您必须选择其中之一!

    【讨论】:

    • INET_ATON(column) 不能使用索引
    • 当你是对的!我编辑了我的帖子以包含它。
    • 这是一个很好的公司。在 MySQL 中,函数不能使用索引。
    • 解决方案:将INET_ATON()的输出存储在ipStart和ipEnd中,而不是字符串中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-13
    • 2016-06-02
    • 1970-01-01
    • 1970-01-01
    • 2011-01-22
    • 2011-07-07
    相关资源
    最近更新 更多