Det här tillvägagångssättet har vissa skalbarhetsproblem (om du skulle välja att flytta till t.ex. stadsspecifik geoip-data), men för den givna storleken på data kommer det att ge avsevärd optimering.
Problemet du står inför är faktiskt att MySQL inte optimerar intervallbaserade frågor särskilt bra. Helst vill du göra en exakt ("=") uppslagning på ett index snarare än "större än", så vi måste bygga ett sådant index från den data du har tillgänglig. På så sätt kommer MySQL att ha mycket färre rader att utvärdera när man letar efter en matchning.
För att göra detta föreslår jag att du skapar en uppslagstabell som indexerar geolokaliseringstabellen baserat på den första oktetten (=1 från 1.2.3.4) av IP-adresserna. Tanken är att för varje uppslagning du måste göra kan du ignorera alla geolokaliserings-IP:er som inte börjar med samma oktett än den IP du letar efter.
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ärefter måste vi ta informationen som är tillgänglig i din geolokaliseringstabell och producera data som täcker alla (första) oktetter som geolokaliseringsraden täcker:Om du har en post med ip_start = '5.3.0.0'
och ip_end = '8.16.0.0'
, uppslagstabellen behöver rader för oktetter 5, 6, 7 och 8. Så...
ip_geolocation
|ip_start |ip_end |ip_numeric_start|ip_numeric_end|
|72.255.119.248 |74.3.127.255 |1224701944 |1241743359 |
Bör konvertera till:
ip_geolocation_lookup
|first_octet|ip_numeric_start|ip_numeric_end|
|72 |1224701944 |1241743359 |
|73 |1224701944 |1241743359 |
|74 |1224701944 |1241743359 |
Eftersom någon här efterfrågade en inbyggd MySQL-lösning, här är en lagrad procedur som kommer att generera denna data åt dig:
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;
Och sedan måste du fylla i tabellen genom att anropa den lagrade proceduren:
CALL recalculate_ip_geolocation_lookup();
Vid det här laget kan du ta bort proceduren du just skapade -- den behövs inte längre, om du inte vill räkna om uppslagstabellen.
När uppslagstabellen är på plats är allt du behöver göra att integrera den i dina frågor och se till att du frågar med den första oktetten. Din fråga till uppslagstabellen kommer att uppfylla två villkor:
- Hitta alla rader som matchar den första oktetten i din IP-adress
- Av den delmängden :Hitta raden som har intervallet som matchar din IP-adress
Eftersom steg två utförs på en delmängd av data, är det betydligt snabbare än att göra intervalltesterna på hela datan. Detta är nyckeln till denna optimeringsstrategi.
Det finns olika sätt att ta reda på vad den första oktetten i en IP-adress är; Jag använde ( r.ip_numeric & 0xFF000000 ) >> 24
eftersom mina käll-IP:er är i numerisk form:
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;
Nu blev jag visserligen lite lat till slut:du kan lätt bli av med ip_geolocation
tabellen helt och hållet om du gjorde ip_geolocation_lookup
Tabellen innehåller även landdata. Jag antar att en tabell från den här frågan skulle göra det lite snabbare.
Och slutligen, här är de två andra tabellerna jag använde i det här svaret som referens, eftersom de skiljer sig från dina tabeller. Jag är dock säker på att de är kompatibla.
# 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;