Tento přístup má určité problémy se škálovatelností (pokud byste se rozhodli přesunout řekněme na data geoip specifická pro město), ale pro danou velikost dat poskytne značnou optimalizaci.
Problém, kterému čelíte, je fakt, že MySQL příliš dobře neoptimalizuje dotazy založené na rozsahu. V ideálním případě chcete provést přesné vyhledávání ("=") indexu spíše než "větší než", takže budeme muset takový index vytvořit z dat, která máte k dispozici. Tímto způsobem bude mít MySQL mnohem méně řádků k vyhodnocení při hledání shody.
Chcete-li to provést, navrhuji, abyste vytvořili vyhledávací tabulku, která indexuje tabulku geolokace na základě prvního oktetu (=1 z 1.2.3.4) IP adres. Myšlenka je taková, že pro každé vyhledávání, které musíte provést, můžete ignorovat všechny geolokační IP adresy, které nezačínají stejným oktetem než IP, kterou hledáte.
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;
Dále musíme vzít data dostupná ve vaší geolokační tabulce a vytvořit data, která pokrývají vše (první) oktety pokrývá řádek geolokace:Pokud máte položku s ip_start = '5.3.0.0'
a ip_end = '8.16.0.0'
, bude vyhledávací tabulka potřebovat řádky pro oktety 5, 6, 7 a 8. Takže...
ip_geolocation
|ip_start |ip_end |ip_numeric_start|ip_numeric_end|
|72.255.119.248 |74.3.127.255 |1224701944 |1241743359 |
Mělo by se převést na:
ip_geolocation_lookup
|first_octet|ip_numeric_start|ip_numeric_end|
|72 |1224701944 |1241743359 |
|73 |1224701944 |1241743359 |
|74 |1224701944 |1241743359 |
Protože zde někdo požadoval nativní řešení MySQL, zde je uložená procedura, která vám tato data vygeneruje:
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;
A pak budete muset naplnit tabulku voláním této uložené procedury:
CALL recalculate_ip_geolocation_lookup();
V tomto okamžiku můžete smazat proceduru, kterou jste právě vytvořili – již není potřeba, pokud nechcete přepočítat vyhledávací tabulku.
Poté, co je vyhledávací tabulka na svém místě, stačí ji začlenit do vašich dotazů a ujistit se, že dotazujete podle prvního oktetu. Váš dotaz do vyhledávací tabulky bude splňovat dvě podmínky:
- Najděte všechny řádky, které odpovídají prvnímu oktetu vaší IP adresy
- Z této podmnožiny :Najděte řádek s rozsahem, který odpovídá vaší IP adrese
Protože se druhý krok provádí na podmnožině dat, je podstatně rychlejší než provádění testů rozsahu na celých datech. To je klíč k této optimalizační strategii.
Existují různé způsoby, jak zjistit, jaký je první oktet IP adresy; Použil jsem ( r.ip_numeric & 0xFF000000 ) >> 24
protože moje zdrojové IP jsou v číselné podobě:
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;
Připouštím, že jsem nakonec trochu zlenivil:Můžete se snadno zbavit ip_geolocation
tabulku úplně, pokud jste provedli ip_geolocation_lookup
tabulka také obsahuje údaje o zemi. Hádám, že vypuštěním jedné tabulky z tohoto dotazu by to bylo o něco rychlejší.
A konečně, zde jsou dvě další tabulky, které jsem použil v této odpovědi pro referenci, protože se liší od vašich tabulek. Jsem si však jistý, že jsou kompatibilní.
# 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;