[Из песочницы] IP-Geo. Оптимизация SQL запроса

54e1e8e5774c45fcb937712dcb71ae51.pngЗдравствуй, Хабрахабр! Меня зовут Бабичев Максим и я быдлокодер. Это моя первая статья на Хабрахабр, прошу строго не судить.

Нашел на Хабре статью, в которой были базы в двух модификациях: Страны и города и только страны. Также в этих архивах есть небольшие примеры использования таблиц на php.

Сразу хочу сказать, что эта статья рассчитана на новичков, а не на продвинутых гуру.

На всякий случай приложу структуру таблиц --  — Структура таблицы `net_city` --

CREATE TABLE IF NOT EXISTS `net_city` ( `id` INT (11) NOT NULL AUTO_INCREMENT, `country_id` INT (11) DEFAULT NULL, `name_ru` VARCHAR (100) DEFAULT NULL, `name_en` VARCHAR (100) DEFAULT NULL, `region` VARCHAR (2) DEFAULT NULL, `postal_code` VARCHAR (10) DEFAULT NULL, `latitude` VARCHAR (10) DEFAULT NULL, `longitude` VARCHAR (10) DEFAULT NULL, PRIMARY KEY (`id`), KEY `country_id` (`country_id`), KEY `name_ru` (`name_ru`), KEY `name_en` (`name_en`) ) ENGINE = MyISAM DEFAULT CHARSET = utf8;

--  — Структура таблицы `net_city_ip` --

CREATE TABLE IF NOT EXISTS `net_city_ip` ( `city_id` INT (11) DEFAULT NULL, `begin_ip` BIGINT (11) DEFAULT NULL, `end_ip` BIGINT (11) DEFAULT NULL, KEY `city_id` (`city_id`), KEY `ip` (`begin_ip`) ) ENGINE = MyISAM DEFAULT CHARSET = utf8;

--  — Структура таблицы `net_country` --

CREATE TABLE IF NOT EXISTS `net_country` ( `id` INT (11) NOT NULL AUTO_INCREMENT, `name_ru` VARCHAR (100) DEFAULT NULL, `name_en` VARCHAR (100) DEFAULT NULL, `code` VARCHAR (2) DEFAULT NULL, PRIMARY KEY (`id`), KEY `code` (`code`), KEY `name_en` (`name_en`), KEY `name_ru` (`name_ru`) ) ENGINE = MyISAM DEFAULT CHARSET = utf8;

--  — Структура таблицы `net_country_ip` --

CREATE TABLE IF NOT EXISTS `net_country_ip` ( `country_id` INT (11) DEFAULT '0', `begin_ip` BIGINT (11) DEFAULT NULL, `end_ip` BIGINT (11) DEFAULT '0', KEY `country_id` (`country_id`), KEY `ip` (`begin_ip`) ) ENGINE = MyISAM DEFAULT CHARSET = utf8;

--  — Структура таблицы `net_euro` --

CREATE TABLE IF NOT EXISTS `net_euro` ( `country_id` INT (11) DEFAULT '0', `begin_ip` BIGINT (11) DEFAULT NULL, `end_ip` BIGINT (11) DEFAULT '0', KEY `country_id` (`country_id`), KEY `ip` (`begin_ip`) ) ENGINE = MyISAM DEFAULT CHARSET = utf8;

--  — Структура таблицы `net_ru` --

CREATE TABLE IF NOT EXISTS `net_ru` ( `city_id` INT (11) DEFAULT '0', `begin_ip` BIGINT (11) DEFAULT NULL, `end_ip` BIGINT (11) DEFAULT NULL, KEY `city_id` (`city_id`), KEY `ip` (`begin_ip`) ) ENGINE = MyISAM DEFAULT CHARSET = utf8; Меня больше интересуют запросы SQL.LONG_IP_ADDRESS, число полученное с помощью функции ip2long () в PHP.

 — Ищем по российским и украинским городам  — Запрос (1)

SELECT * FROM ( SELECT * FROM net_ru WHERE begin_ip <= LONG_IP_ADDRESS – IP пользователя, ip2long() ORDER BY begin_ip DESC LIMIT 1 ) AS t WHERE end_ip >= LONG_IP_ADDRESS — IP пользователя long После запроса (1), получают нужный город из таблицы net_city:  — Запрос (2)

SELECT * FROM net_city WHERE id = — (Результат из первого запроса).city_id Код из примера на PHP:

// IP-адрес, который нужно проверить $ip = »79.134.219.2»;

// Преобразуем IP в число $int = sprintf (»%u», ip2long ($ip));

$country_name = »; $country_id = 0;

$city_name = »; $city_id = 0;

// Ищем по российским и украинским городам $sql = «select * from (select * from net_ru where begin_ip<=$int order by begin_ip desc limit 1) as t where end_ip>=$int»; $result = mysql_query ($sql); if ($row = mysql_fetch_array ($result)) { $city_id = $row['city_id']; $sql = «select * from net_city where id='$city_id'»; $result = mysql_query ($sql); if ($row = mysql_fetch_array ($result)) { $city_name = $row['name_ru']; $country_id = $row['country_id']; } else { $city_id = 0; } } Избавимся от подзапроса в запросе (1), используя AND.  — Модифицированный запрос (3)

SELECT `city_id` FROM `net_ru` WHERE begin_ip <= LONG_IP_ADDRESS AND end_ip >= LONG_IP_ADDRESS И в этом случае мы избавимся от подзапроса. Но вспомним про BETWEEN и запишем запрос так:  — Модифицированный запрос (4)

SELECT `city_id` FROM `net_ru` WHERE LONG_IP_ADDRESS BETWEEN begin_ip AND end_ip Так SQL-код читабельнее и короче.

Остался отдельный запрос на город. Объединим модифицированный запрос (4) и (2).

 — Модифицированный запрос (5)

SELECT * FROM `net_city` `city` JOIN ( SELECT `city_id` FROM `net_ru` WHERE LONG_IP_ADDRESS BETWEEN begin_ip AND end_ip ) AS `res` ON `res`.`city_id` = `city`.`id` Так получилось, что IP-адреса 79.134.219.2 в базе net_ru — нет. Но он есть в базе net_city_ip.На многих ресурсах объединяют запросы так:  — Модифицированный запос (5.1)

SELECT * FROM `net_city` `city` JOIN ( SELECT `city_id` FROM `net_ru` WHERE LONG_IP_ADDRESS BETWEEN begin_ip AND end_ip ) AS `res` ON `res`.`city_id` = `city`.`id` UNION SELECT * FROM `net_city` `city` JOIN ( SELECT `city_id` FROM `net_city_ip` WHERE LONG_IP_ADDRESS BETWEEN begin_ip AND end_ip ) AS `res` ON `res`.`city_id` = `city`.`id` Видим, что запросы идентичны. Объединим внутри JOIN два запроса, получим:  — Модифицированный запрос (5.2)

SELECT * FROM `net_city` `city` JOIN ( SELECT `city_id` FROM `net_ru` WHERE LONG_IP_ADDRESS BETWEEN begin_ip AND end_ip UNION SELECT `city_id` FROM `net_city_ip` WHERE LONG_IP_ADDRESS BETWEEN begin_ip AND end_ip ) as `res` ON `res`.`city_id` = `city`.`id` Модифицированный запрос (5.2) хорош собой, но нам не нужны все поля.Вытащим: name_ru name_en region postal_code latitude longitude  — Модифицированный запрос (6)

SELECT DISTINCT `city`.`name_ru` `city_name_ru`, `city`.`name_en` `city_name_en`, `city`.`region`, `city`.`postal_code`, `city`.`latitude`, `city`.`longitude` FROM `net_city` `city` JOIN ( SELECT `city_id` FROM `net_ru` WHERE LONG_IP_ADDRESS BETWEEN begin_ip AND end_ip UNION SELECT `city_id` FROM `net_city_ip` WHERE LONG_IP_ADDRESS BETWEEN begin_ip AND end_ip ) AS `res` ON `res`.`city_id` = `city`.`id` Далее нам нужно выбрать страну, в которой находится пользователь. Добавим JOIN соединение в запрос.  — Модифицированный запрос (7)

SELECT DISTINCT `city`.`name_ru` `city_name_ru`, `city`.`name_en` `city_name_en`, `city`.`region`, `city`.`postal_code`, `city`.`latitude`, `city`.`longitude`, `country`.`name_ru` `country_name_ru`, `country`.`name_en` `country_name_en`, `country`.`code` FROM `net_city` `city` JOIN ( SELECT `city_id` FROM `net_ru` WHERE LONG_IP_ADDRESS BETWEEN begin_ip AND end_ip UNION SELECT `city_id` FROM `net_city_ip` WHERE LONG_IP_ADDRESS BETWEEN begin_ip AND end_ip ) AS `res` ON `res`.`city_id` = `city`.`id` JOIN `net_country` `country` ON `country`.`id` = `city`.`country_id` Протестируем запрос.Для этого получим с помощью PHP значение LANG_IP_ADDRESS:

SELECT DISTINCT `city`.`name_ru` `city_name_ru`, `city`.`name_en` `city_name_en`, `city`.`region`, `city`.`postal_code`, `city`.`latitude`, `city`.`longitude`, `country`.`name_ru` `country_name_ru`, `country`.`name_en` `country_name_en`, `country`.`code` FROM `net_city` `city` JOIN ( SELECT `city_id` FROM `net_ru` WHERE 1334237954 BETWEEN begin_ip AND end_ip UNION SELECT `city_id` FROM `net_city_ip` WHERE 1334237954 BETWEEN begin_ip AND end_ip ) AS `res` ON `res`.`city_id` = `city`.`id` JOIN `net_country` `country` ON `country`.`id` = `city`.`country_id` Результат работы запроса: 61877b0156a2437e932b56a24d2474d9.png

1 всего, запрос занял 0.3408 сек.

JOIN соединения сильно влияют на скорость выполнения запроса. Запишем JOIN с помощью WHERE

 — Модифицированный запрос (8)  — Записали JOIN с помощью WHERE

SELECT DISTINCT `city`.`name_ru` `city_name_ru`, `city`.`name_en` `city_name_en`, `city`.`region`, `city`.`postal_code`, `city`.`latitude`, `city`.`longitude`, `country`.`name_ru` `country_name_ru`, `country`.`name_en` `country_name_en`, `country`.`code` FROM `net_city` `city` JOIN `net_country` `country` ON `country`.`id` = `city`.`country_id` WHERE `city`.`id` = ( SELECT `city_id` FROM `net_city_ip` WHERE LONG_IP_ADDRESS BETWEEN `begin_ip` AND `end_ip` UNION SELECT `city_id` FROM `net_ru` WHERE LONG_IP_ADDRESS BETWEEN `begin_ip` AND `end_ip` ) Протестируем модифицированный запрос: Модифицированный запрос с помощью WHERE SELECT DISTINCT `city`.`name_ru` `city_name_ru`, `city`.`name_en` `city_name_en`, `city`.`region`, `city`.`postal_code`, `city`.`latitude`, `city`.`longitude`, `country`.`name_ru` `country_name_ru`, `country`.`name_en` `country_name_en`, `country`.`code` FROM `net_city` `city` JOIN `net_country` `country` ON `country`.`id` = `city`.`country_id` WHERE `city`.`id` = ( SELECT `city_id` FROM `net_city_ip` WHERE 1334237954 BETWEEN `begin_ip` AND `end_ip` UNION SELECT `city_id` FROM `net_ru` WHERE 1334237954 BETWEEN `begin_ip` AND `end_ip` ) Результат работы запроса: 8fad56dd1a14483bae9b21c09b1c4766.png

1 всего, запрос занял 0.1527 сек.

Модифицированный запрос был выполнен быстрее более чем в два раза.

Аналогичный запрос можно получить и для стран.

Данная Geo-IP база была выбрана для объяснения JOIN/UNION соединений и оптимизации запросов.Надеюсь, данная статья поможет начинающим понять, насколько важна оптимизация и как её можно добиться. Рекомендую к чтению статью пользователя tuta_larson.

Данная Geo-IP база очень старая и знает очень мало IP-адресов. Но вы можете составить свою IP-базу и с помощью пользователей пополнять её.

b9428091dee0429ca999ce28dbde1f1b.png

Дальше составить рейтинг IP и основываясь на собственный рейтинг «угадывать» город из которого пользователь.

База данных и информацию по GeoIP брал из статьи: «База GeoIP — страны и города, сентябрь 2013». Спасибо пользователю netload за увлекательную статью, написаную в 2013 году.

© Habrahabr.ru