【问题标题】:GeoIP table join with table of IP's in MySQLGeoIP 表与 MySQL 中的 IP 表连接
【发布时间】:2012-01-01 23:27:38
【问题描述】:

我在寻找一种快速加入表格的方法时遇到问题:

mysql> explain geo_ip;
+--------------+------------------+------+-----+---------+-------+
| Field        | Type             | Null | Key | Default | Extra |
+--------------+------------------+------+-----+---------+-------+
| ip_start     | varchar(32)      | NO   |     | ""      |       |
| ip_end       | varchar(32)      | NO   |     | ""      |       |
| ip_num_start | int(64) unsigned | NO   | PRI | 0       |       |
| ip_num_end   | int(64) unsigned | NO   |     | 0       |       |
| country_code | varchar(3)       | NO   |     | ""      |       |
| country_name | varchar(64)      | NO   |     | ""      |       |
| ip_poly      | geometry         | NO   | MUL | NULL    |       |
+--------------+------------------+------+-----+---------+-------+


mysql> explain entity_ip;
+------------+---------------------+------+-----+---------+-------+
| Field      | Type                | Null | Key | Default | Extra |
+------------+---------------------+------+-----+---------+-------+
| entity_id  | int(64) unsigned    | NO   | PRI | NULL    |       |
| ip_1       | tinyint(3) unsigned | NO   |     | NULL    |       |
| ip_2       | tinyint(3) unsigned | NO   |     | NULL    |       |
| ip_3       | tinyint(3) unsigned | NO   |     | NULL    |       |
| ip_4       | tinyint(3) unsigned | NO   |     | NULL    |       |
| ip_num     | int(64) unsigned    | NO   |     | 0       |       |
| ip_poly    | geometry            | NO   | MUL | NULL    |       |
+------------+---------------------+------+-----+---------+-------+

请注意,我对一次仅通过一个 IP 地址在 geo_ip 中查找所需行不感兴趣,我需要一个 entity_ip LEFT JOIN geo_ip(或类似/模拟方式)。

这就是我现在所拥有的(按照http://jcole.us/blog/archives/2007/11/24/on-efficiently-geo-referencing-ips-with-maxmind-geoip-and-mysql-gis/ 的建议使用多边形):

mysql> EXPLAIN SELECT li.*, gi.country_code FROM entity_ip AS li
-> LEFT JOIN geo_ip AS gi ON
-> MBRCONTAINS(gi.`ip_poly`, li.`ip_poly`);

+----+-------------+-------+------+---------------+------+---------+------+--------+-------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------+
|  1 | SIMPLE      | li    | ALL  | NULL          | NULL | NULL    | NULL |   2470 |       |
|  1 | SIMPLE      | gi    | ALL  | ip_poly_index | NULL | NULL    | NULL | 155183 |       |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------+

mysql> SELECT li.*, gi.country_code FROM entity AS li LEFT JOIN geo_ip AS gi ON MBRCONTAINS(gi.`ip_poly`, li.`ip_poly`) limit  0, 20;
20 rows in set (2.22 sec)

没有多边形

mysql> explain SELECT li.*, gi.country_code FROM entity_ip AS li LEFT JOIN geo_ip AS gi ON li.`ip_num` >= gi.`ip_num_start` AND li.`ip_num` <= gi.`ip_num_end` LIMIT 0,20;
+----+-------------+-------+------+---------------------------+------+---------+------+--------+-------+
| id | select_type | table | type | possible_keys             | key  | key_len | ref  | rows   | Extra |
+----+-------------+-------+------+---------------------------+------+---------+------+--------+-------+
|  1 | SIMPLE      | li    | ALL  | NULL                      | NULL | NULL    | NULL |   2470 |       |
|  1 | SIMPLE      | gi    | ALL  | PRIMARY,geo_ip,geo_ip_end | NULL | NULL    | NULL | 155183 |       |
+----+-------------+-------+------+---------------------------+------+---------+------+--------+-------+

mysql> SELECT li.*, gi.country_code FROM entity_ip AS li LEFT JOIN geo_ip AS gi ON li.ip_num BETWEEN gi.ip_num_start AND gi.ip_num_end limit  0, 20;
20 rows in set (2.00 sec)

(在搜索中的行数更高 - 没有区别)

目前我无法从这些查询中获得更快的性能,因为每个 IP 0.1 秒对我来说太慢了。

有什么方法可以让它更快?

【问题讨论】:

  • 在黑暗中射击:entity_ipip_num 上的索引是否有可能提高第二次查询的速度?
  • 必须在 MySQL 中进行吗?如果我们将 ip_num_start 和 ip_num_end 视为关联点,并将 entity_ip.ip_num 以排序方式读取为跨点的扫掠线的 x 坐标,则扫掠线算法的概念可能会给您比 n-by-m 左侧更快的运行加入 MySQL。
  • 不知道作者的情况,对我(和很多人)来说,看到只有mysql的解决方案会很有趣。
  • 第一个明显的修复是在entity_ip.ip_poly 中使用GEOMETRY 数据类型的POINT insead。 Next: mysql 可能没有在您的查询中使用索引,因为您从entity_ip 中选择了所有行并且查询优化器决定进行全扫描。
  • Oroboros102,POINT 不起作用,我是从 POINT 开始的,其实 :)

标签: mysql join geoip


【解决方案1】:

这种方法存在一些可扩展性问题(如果您选择迁移到特定城市的 geoip 数据),但对于给定的数据大小,它将提供相当大的优化。

您面临的问题实际上是 MySQL 不能很好地优化基于范围的查询。理想情况下,您希望对索引进行精确 ("=") 查找,而不是“大于”,因此我们需要根据您可用的数据构建类似的索引。这样,MySQL 在查找匹配项时要评估的行数就会少得多。

为此,我建议您创建一个查找表,根据 IP 地址的第一个八位字节(=1 来自 1.2.3.4)索引地理位置表。这个想法是,对于您必须执行的每次查找,您可以忽略所有不以与您正在查找的 IP 相同的八位字节开头的地理定位 IP。

CREATE TABLE `ip_geolocation_lookup` (
  `first_octet` int(10) unsigned NOT NULL DEFAULT '0',
  `ip_numeric_start` int(10) unsigned NOT NULL DEFAULT '0',
  `ip_numeric_end` int(10) unsigned NOT NULL DEFAULT '0',
  KEY `first_octet` (`first_octet`,`ip_numeric_start`,`ip_numeric_end`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

接下来,我们需要获取您的地理位置表中可用的数据,并生成涵盖所有(第一个)八位字节的地理位置行涵盖的数据:如果您有一个带有 ip_start = '5.3.0.0' 和 @987654324 的条目@,查找表将需要八位字节 5、6、7 和 8 的行。所以...

ip_geolocation
|ip_start       |ip_end          |ip_numeric_start|ip_numeric_end|
|72.255.119.248 |74.3.127.255    |1224701944      |1241743359    |

应该转换为:

ip_geolocation_lookup
|first_octet|ip_numeric_start|ip_numeric_end|
|72         |1224701944      |1241743359    |
|73         |1224701944      |1241743359    |
|74         |1224701944      |1241743359    |

由于这里有人要求使用本机 MySQL 解决方案,这里有一个存储过程将为您生成该数据:

DROP PROCEDURE IF EXISTS recalculate_ip_geolocation_lookup;

CREATE PROCEDURE recalculate_ip_geolocation_lookup()
BEGIN
    DECLARE i INT DEFAULT 0;

    DELETE FROM ip_geolocation_lookup;

    WHILE i < 256 DO
       INSERT INTO ip_geolocation_lookup (first_octet, ip_numeric_start, ip_numeric_end) 
                SELECT  i, ip_numeric_start, ip_numeric_end FROM ip_geolocation WHERE 
                ( ip_numeric_start & 0xFF000000 ) >> 24 <= i AND 
                ( ip_numeric_end & 0xFF000000 ) >> 24 >= i;

       SET i = i + 1;
    END WHILE;
END;

然后您需要通过调用该存储过程来填充表:

CALL recalculate_ip_geolocation_lookup();

此时您可以删除刚刚创建的过程——不再需要它,除非您想重新计算查找表。

查找表到位后,您所要做的就是将其集成到您的查询中,并确保您按第一个八位字节进行查询。您对查找表的查询将满足两个条件:

  1. 查找与您的 IP 地址的第一个八位字节匹配的所有行
  2. 该子集:查找范围与您的 IP 地址匹配的行

因为第二步是在数据子集上执行的,所以它比对整个数据进行范围测试要快得多。这是这个优化策略的关键。

有多种方法可以确定 IP 地址的第一个八位字节是什么;我使用了( r.ip_numeric &amp; 0xFF000000 ) &gt;&gt; 24,因为我的源 IP 是数字形式:

SELECT 
    r.*, 
    g.country_code
FROM 
    ip_geolocation g,
    ip_geolocation_lookup l,
    ip_random r
WHERE 
    l.first_octet = ( r.ip_numeric & 0xFF000000 ) >> 24 AND 
    l.ip_numeric_start <= r.ip_numeric AND      
    l.ip_numeric_end >= r.ip_numeric AND 
    g.ip_numeric_start = l.ip_numeric_start;

现在,诚然,我最终确实有点懒惰:如果您让 ip_geolocation_lookup 表也包含国家/地区数据,您可以轻松地完全摆脱 ip_geolocation 表。我猜想从此查询中删除一个表会使其更快一些。

最后,这是我在此回复中用作参考的另外两个表格,因为它们与您的表格不同。不过,我确信它们是兼容的。

# This table contains the original geolocation data

CREATE TABLE `ip_geolocation` (
  `ip_start` varchar(16) NOT NULL DEFAULT '',
  `ip_end` varchar(16) NOT NULL DEFAULT '',
  `ip_numeric_start` int(10) unsigned NOT NULL DEFAULT '0',
  `ip_numeric_end` int(10) unsigned NOT NULL DEFAULT '0',
  `country_code` varchar(3) NOT NULL DEFAULT '',
  `country_name` varchar(64) NOT NULL DEFAULT '',
  PRIMARY KEY (`ip_numeric_start`),
  KEY `country_code` (`country_code`),
  KEY `ip_start` (`ip_start`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


# This table simply holds random IP data that can be used for testing

CREATE TABLE `ip_random` (
  `ip` varchar(16) NOT NULL DEFAULT '',
  `ip_numeric` int(10) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

【讨论】:

  • 哇,非常详细的答案。请给我两天时间来测试这种方法。看起来,像工作解决方案。
  • 这个查询比全扫描快得多,但它仍然需要扫描很多行(ranges_qty / 255)。如果我们将使用每个城市范围表(30 000 000 行)的地理 ip,则此查询会很慢。我找到了一些使用几何的解决方案。如果我的问题会得到任何适当的答案(stackoverflow.com/questions/8244535/…),我会为这个问题提供更好的解决方案。如果不是 - 你的答案将是最好的。
  • 问题实际上是不同的。 INNER JOIN 工作正常,而 LEFT JOIN 在 2k entity_ip 表上至少需要 4 分钟。
  • 你可以建立一个更大的查找表。上面的答案实际上忽略了 IP 地址的 32 位中的最后 24 位。例如,使用前两个八位字节构建查找表将提高对大型数据集的查询速度,并且仍将查找表保持在合理的大小。
  • 谢谢你;第一个 octet 解决方案正是我优化查询数百万行所需的东西。
【解决方案2】:

只是想回馈社区:

这是一种基于 Aleksi 解决方案的更好和优化的方法:

DROP PROCEDURE IF EXISTS recalculate_ip_geolocation_lookup;

DELIMITER ;;
CREATE PROCEDURE recalculate_ip_geolocation_lookup()
BEGIN
  DECLARE i INT DEFAULT 0;
DROP TABLE `ip_geolocation_lookup`;

CREATE TABLE `ip_geolocation_lookup` (
  `first_octet` smallint(5) unsigned NOT NULL DEFAULT '0',
  `startIpNum` int(10) unsigned NOT NULL DEFAULT '0',
  `endIpNum` int(10) unsigned NOT NULL DEFAULT '0',
  `locId` int(11) NOT NULL,
  PRIMARY KEY (`first_octet`,`startIpNum`,`endIpNum`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT IGNORE INTO ip_geolocation_lookup
SELECT  startIpNum DIV 1048576 as first_octet, startIpNum, endIpNum, locId
FROM ip_geolocation;

INSERT IGNORE INTO ip_geolocation_lookup
SELECT  endIpNum DIV 1048576 as first_octet, startIpNum, endIpNum, locId
FROM ip_geolocation;

  WHILE i < 1048576 DO
    INSERT IGNORE INTO ip_geolocation_lookup
        SELECT i, startIpNum, endIpNum, locId 
        FROM ip_geolocation_lookup 
        WHERE first_octet = i-1
        AND endIpNum DIV 1048576 > i;
   SET i = i + 1;
  END WHILE;
END;;
DELIMITER ;

CALL recalculate_ip_geolocation_lookup();

它的构建速度比他的解决方案更快,并且更容易向下钻取,因为我们不仅采用前 8 位,而且采用前 20 位。加入性能:158 毫秒内 100000 行。您可能必须将表和字段名称重命名为您的版本。

使用查询

SELECT ip, kl.*
FROM random_ips ki
JOIN `ip_geolocation_lookup` kb ON (ki.`ip` DIV 1048576 = kb.`first_octet` AND ki.`ip` >= kb.`startIpNum` AND ki.`ip` <= kb.`endIpNum`)
JOIN ip_maxmind_locations kl ON kb.`locId` = kl.`locId`;

【讨论】:

    【解决方案3】:

    尚不能发表评论,但 user1281376 的回答是错误的,并且不起作用。您只使用第一个八位字节的原因是因为您不会匹配所有 IP 范围。有很多范围跨越多个第二个八位字节,user1281376s 更改的查询不会匹配。是的,如果您使用 Maxmind GeoIp 数据,这实际上会发生。

    通过 aleksis 的建议,您可以对第一个八位字节进行简单比较,从而减少匹配集。

    【讨论】:

    • 也许我应该检查一下,但当时我决定跳过它,因为它仍然有效(我记得我还假设作者已经完成了他的作业)。谢谢
    • 是的,它显然更快,但尤其是使用 maxmind 的 geoip 表时,例如您将无法匹配级别 3。我花了一段时间才弄清楚我第一次遇到这个问题。因此,您必须为 end_range 添加另一行,然后您仍然会遇到范围查询。更糟糕的是,当您没有匹配到 ip 时,它会扫描整个表。
    【解决方案4】:

    我找到了一个简单的方法。我注意到组中的所有第一个 ip % 256 = 0, 所以我们可以添加一个 ip_index 表

    CREATE TABLE `t_map_geo_range` (
      `_ip` int(10) unsigned NOT NULL,
      `_ipStart` int(10) unsigned NOT NULL,
      PRIMARY KEY (`_ip`)
    ) ENGINE=MyISAM
    

    如何填充索引表

    FOR_EACH(Every row of ip_geo)
    {
        FOR(Every ip FROM ipGroupStart/256 to ipGroupEnd/256)
        {
            INSERT INTO ip_geo_index(ip, ipGroupStart);
        }
    }
    

    使用方法:

    SELECT * FROM YOUR_TABLE AS A
    LEFT JOIN ip_geo_index AS B ON B._ip = A._ip DIV 256
    LEFT JOIN ip_geo AS C ON C.ipStart = B.ipStart;
    

    快 1000 倍以上。

    【讨论】:

    • 请看上面的答案。
    猜你喜欢
    • 2013-01-27
    • 2017-07-25
    • 1970-01-01
    • 2023-04-06
    • 2010-09-26
    • 1970-01-01
    • 2015-10-28
    • 1970-01-01
    相关资源
    最近更新 更多