2011-11-19 75 views
6

我有一个问题,找到连接表看上去就像是一个快速的方式与IP公司的表连接只需一个IP地址,我需要一个entity_ip LEFT JOIN geo_ip(或类似/模拟的方式)。GeoIP的表在MySQL

这是我现在(使用多边形上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秒对我来说太慢了。

有什么办法让它更快?

+1

在黑暗中拍摄:对entity_ip的'ip_num'上的索引会提高第二个查询的速度的任何机会? –

+0

必须在MySQL内部做到这一点?如果我们将ip_num_start和ip_num_end作为关联点,并以排序的方式将entity_ip.ip_num作为横扫点上的扫描线的x坐标来读取,则扫描线算法的概念可能会让您的运行速度快于n-m左边加入MySQL内部。 –

+0

不知道作者的案例,对于我(和很多人)来说,只看到mysql的解决方案会非常有趣。 – Oroboros102

回答

6

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

您正面临的问题实际上是MySQL并未很好地优化基于范围的查询。理想情况下,您希望对索引执行精确(“=”)查找,而不是“大于”,因此我们需要根据您可用的数据构建索引。通过这种方式,MySQL在查找匹配时将有更少的行进行评估。

为此,我建议您创建一个查找表,根据IP地址的第一个字节(来自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'ip_end = '8.16.0.0',查找表中的条目将需要为八位字节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 & 0xFF000000) >> 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; 
+0

哇,极其详细的答案。请给我一两天来测试这种方法。看起来像工作解决方案。 – Oroboros102

+0

该查询比fullscan快得多,但仍需要扫描很多行(ranges_qty/255)。如果我们使用每个城市范围表(300 000 000行)的地理位置IP,此查询将会很慢。我发现了一些使用几何的soluton。如果我的问题会得到任何不恰当的答案(http://stackoverflow.com/questions/8244535/joins-on-spatial-mysql-indexes),我会有更好的解决方案这个问题。如果没有 - 你的答案将是最好的。 – Oroboros102

+0

这个问题实际上是不同的。 INNER JOIN正常工作,而LEFT JOIN在2k entity_ip表中至少需要4分钟。 –

0

只是想回馈社会:

这里有一个更美好,最优化的方式建设上阿列克西的解决方案:

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`; 
1

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

与aleksis的建议,你可以做一个简单的比较第一八位字节,从而减少匹配集。

+0

也许我应该检查一下,但在那个时候我决定跳过它,因为它工作正常(我记得我也假定作者已经完成他的功课)。非常感谢 –

+0

,它显然更快,但特别是对于maxmind的geoip表,您将不会匹配3级。我花了一段时间才弄清楚我第一次遇到这个问题。所以你必须为end_range添加另一行,然后你仍然坚持使用范围查询。更糟的是,当你没有匹配ip时,它会扫描整个表格。 – knrdk

0

我找到了一个简单的方法。我注意到,在该组%所有第一个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多倍。

+0

请参阅上面的答案。 –