У этого подхода есть некоторые проблемы с масштабируемостью (если вы решите перейти, скажем, к данным геоипов, относящихся к конкретному городу), но для данного размера данных он обеспечит значительную оптимизацию.
Проблема, с которой вы сталкиваетесь, заключается в том, что 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();
На этом этапе вы можете удалить только что созданную процедуру - она больше не нужна, если вы не хотите пересчитать справочную таблицу.
После того, как справочная таблица создана, все, что вам нужно сделать, это включить ее в свои запросы и убедиться, что вы запрашиваете по первому октету. Ваш запрос к таблице соответствия будет удовлетворять двум условиям:
- Найти все строки, которые соответствуют первому октету вашего IP-адреса
- из этого подмножества : найдите строку, диапазон которой соответствует вашему 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;