Таблица GeoIP соединяется с таблицей IP в MySQL

у меня проблема с поиском быстрого способа присоединения к таблицам, выглядящим так:

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

обратите внимание, что я не заинтересован в поиске нужных строк в geo_ip только по одному 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)

(на большее количество строк в поиске - разницы нет)

В настоящее время я не могу получить более высокую производительность от этих запросов, поскольку 0,1 секунды на IP слишком медленные для меня.

есть ли способ сделать это быстрее?

4 ответов


этот подход имеет некоторые проблемы масштабируемости (если вы решите перейти, скажем, к городским данным geoip), но для данного размера данных он обеспечит значительную оптимизацию.

проблема, с которой вы столкнулись, заключается в том, что MySQL не очень хорошо оптимизирует запросы на основе диапазона. В идеале вы хотите сделать точный ( " = ") поиск по индексу, а не" больше, чем", поэтому нам нужно будет построить такой индекс из имеющихся у вас данных. Таким образом MySQL будет иметь гораздо меньше строк для оценки при поиске соответствия.

для этого я предлагаю вам создать таблицу поиска, которая индексирует таблицу геолокации на основе первого октета (=1 из 1.2.3.4) IP-адресов. Идея заключается в том, что для каждого поиска вы должны сделать, вы можете игнорировать все 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 так как мои исходные IPs числовые форма:

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 таблица в целом, если вы сделали ip_geolocation_lookup таблица также содержит данные по странам. Я предполагаю, что удаление одной таблицы из этого запроса сделает его немного быстрее.

и, наконец, вот две другие таблицы, которые я использовал в этом ответе Для справки, поскольку они отличаются от ваших таблиц. Хотя я уверен, что они совместимы.

# 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;

пока не могу комментировать, но ответы user1281376 неверны и не работают. причина, по которой вы используете только первый октет, заключается в том, что в противном случае вы не будете соответствовать всем диапазонам ip. существует множество диапазонов, которые охватывают несколько вторых октетов, которые измененный запрос user1281376s не будет соответствовать. И да, это действительно происходит, если вы используете данные Maxmind GeoIp.

с Алексис предложение вы можете сделать простое сравнение на первой октет, тем самым уменьшая набор.


просто хотел вернуть сообществу:

вот еще лучший и оптимизированный Способ построения решения Алекси:

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 бит. Соедините представление: 100000 строк в 158ms. Возможно, вам придется переименовать имена таблиц и полей в свою версию.

запрос с помощью

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

Я нашел простой способ. Я заметил, что все первые 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 раз быстрее.