2011-11-19 72 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

請參閱上面的答案。 –