Здравствуй, Хабрахабр! Меня зовут Бабичев Максим. Это моя первая статья на Хабрахабр, прошу строго не судить.
Нашел на Хабре статью, в которой были базы в двух модификациях: Страны и города и только страны. Также в этих архивах есть небольшие примеры использования таблиц на 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:
<?php
// Подключаемся к базе данных
$db_host = "localhost";
$db_user = "";
$db_password = "";
$db_database = "geo";
$link = mysql_connect ($db_host, $db_user, $db_password);
if ($link && mysql_select_db ($db_database)) {
mysql_query ("set names utf8");
} else {
die ("db error");
}
// 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:
<?php
echo ip2lang(‘79.134.219.2’); # Результат: 1334237954
Подставим его в наш запрос и выполним в phpMyAdmin.
Модифицированный запрос (7) с подставленным значением
-- Модифицированный запрос (7) с подставленным значением
-- ip2lang(‘79.134.219.2’) вместо LONG_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`
Результат работы запроса:
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`
)
Результат работы запроса:
1 всего, запрос занял 0.1527 сек.
Модифицированный запрос был выполнен быстрее более чем в два раза.
Аналогичный запрос можно получить и для стран.
Данная Geo-IP база была выбрана для объяснения JOIN/UNION соединений и оптимизации запросов.
Надеюсь, данная статья поможет начинающим понять, насколько важна оптимизация и как её можно добиться. Рекомендую к чтению статью пользователя tuta_larson.
Данная Geo-IP база очень старая и знает очень мало IP-адресов. Но вы можете составить свою IP-базу и с помощью пользователей пополнять её.
Дальше составить рейтинг IP и основываясь на собственный рейтинг “угадывать” город из которого пользователь.
База данных и информацию по GeoIP брал из статьи: «База GeoIP – страны и города, сентябрь 2013». Спасибо пользователю netload за увлекательную статью, написаную в 2013 году.