Таблица GeoIP соединяется с таблицей IP в MySQL - PullRequest
6 голосов
/ 19 ноября 2011

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

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 ]

6 голосов
/ 22 ноября 2011

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

Проблема, с которой вы сталкиваетесь, заключается в том, что 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, так как мои исходные 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, если бы таблица 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;
1 голос
/ 10 марта 2014

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

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

0 голосов
/ 08 мая 2017

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

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

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

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

Запрос с использованием

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