2011-11-19 15 views
6

Estoy teniendo un problema para encontrar una forma rápida de unir las tablas con ese aspecto:mesa GeoIP unirse con mesa de IP en MySQL

mysql> explain geo_ip; 
+--------------+------------------+------+-----+---------+-------+ 
| Field  | Type    | Null | Key | Default | Extra | 
+--------------+------------------+------+-----+---------+-------+ 
| ip_start  | varchar(32)  | NO |  | ""  |  | 
| ip_end  | varchar(32)  | NO |  | ""  |  | 
| ip_num_start | int(64) unsigned | NO | PRI | 0  |  | 
| ip_num_end | int(64) unsigned | NO |  | 0  |  | 
| country_code | varchar(3)  | NO |  | ""  |  | 
| country_name | varchar(64)  | NO |  | ""  |  | 
| ip_poly  | geometry   | NO | MUL | NULL |  | 
+--------------+------------------+------+-----+---------+-------+ 


mysql> explain entity_ip; 
+------------+---------------------+------+-----+---------+-------+ 
| Field  | Type    | Null | Key | Default | Extra | 
+------------+---------------------+------+-----+---------+-------+ 
| entity_id | int(64) unsigned | NO | PRI | NULL |  | 
| ip_1  | tinyint(3) unsigned | NO |  | NULL |  | 
| ip_2  | tinyint(3) unsigned | NO |  | NULL |  | 
| ip_3  | tinyint(3) unsigned | NO |  | NULL |  | 
| ip_4  | tinyint(3) unsigned | NO |  | NULL |  | 
| ip_num  | int(64) unsigned | NO |  | 0  |  | 
| ip_poly | geometry   | NO | MUL | NULL |  | 
+------------+---------------------+------+-----+---------+-------+ 

Tenga en cuenta que no estoy interesado en encontrar las filas necesarias en geo_ip por solo UNA dirección IP a la vez, necesito un entity_ip LEFT JOIN geo_ip (o una forma similar/analógica).

Esto es lo que tengo por ahora (usando polígonos tal como se aconseja en http://jcole.us/blog/archives/2007/11/24/on-efficiently-geo-referencing-ips-with-maxmind-geoip-and-mysql-gis/):

mysql> EXPLAIN SELECT li.*, gi.country_code FROM entity_ip AS li 
-> LEFT JOIN geo_ip AS gi ON 
-> MBRCONTAINS(gi.`ip_poly`, li.`ip_poly`); 

+----+-------------+-------+------+---------------+------+---------+------+--------+-------+ 
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 
+----+-------------+-------+------+---------------+------+---------+------+--------+-------+ 
| 1 | SIMPLE  | li | ALL | NULL   | NULL | NULL | NULL | 2470 |  | 
| 1 | SIMPLE  | gi | ALL | ip_poly_index | NULL | NULL | NULL | 155183 |  | 
+----+-------------+-------+------+---------------+------+---------+------+--------+-------+ 

mysql> SELECT li.*, gi.country_code FROM entity AS li LEFT JOIN geo_ip AS gi ON MBRCONTAINS(gi.`ip_poly`, li.`ip_poly`) limit 0, 20; 
20 rows in set (2.22 sec) 

No hay polígonos

mysql> explain SELECT li.*, gi.country_code FROM entity_ip AS li LEFT JOIN geo_ip AS gi ON li.`ip_num` >= gi.`ip_num_start` AND li.`ip_num` <= gi.`ip_num_end` LIMIT 0,20; 
+----+-------------+-------+------+---------------------------+------+---------+------+--------+-------+ 
| id | select_type | table | type | possible_keys    | key | key_len | ref | rows | Extra | 
+----+-------------+-------+------+---------------------------+------+---------+------+--------+-------+ 
| 1 | SIMPLE  | li | ALL | NULL      | NULL | NULL | NULL | 2470 |  | 
| 1 | SIMPLE  | gi | ALL | PRIMARY,geo_ip,geo_ip_end | NULL | NULL | NULL | 155183 |  | 
+----+-------------+-------+------+---------------------------+------+---------+------+--------+-------+ 

mysql> SELECT li.*, gi.country_code FROM entity_ip AS li LEFT JOIN geo_ip AS gi ON li.ip_num BETWEEN gi.ip_num_start AND gi.ip_num_end limit 0, 20; 
20 rows in set (2.00 sec) 

(El mayor número de filas de la búsqueda - no hay diferencia)

Actualmente no puedo obtener un rendimiento más rápido de estas consultas, ya que 0.1 segundos por IP es demasiado lento para mí.

¿Hay alguna manera de hacerlo más rápido?

+1

Disparo en la oscuridad: ¿hay alguna posibilidad de que un índice en 'ip_num' de' entity_ip' mejore la velocidad de la segunda consulta? –

+0

¿Hay que hacerlo dentro de MySQL?Si tratamos ip_num_start y ip_num_end como puntos asociados, y leyendo entity_ip.ip_num de manera ordenada como x-coord de una línea de barrido a través de los puntos, el concepto de algoritmo de línea de barrido puede proporcionarle una ejecución más rápida que la n-by-m left únete dentro de MySQL. –

+0

No sé sobre el caso del autor, para mí (y muchas personas) sería muy interesante ver la única solución de mysql. – Oroboros102

Respuesta

6

Este enfoque tiene algunos problemas de escalabilidad (si elige pasar a, por ejemplo, datos geográficos específicos de la ciudad), pero para el tamaño de datos dado, proporcionará una optimización considerable.

El problema al que se enfrenta es que MySQL no optimiza muy bien las consultas basadas en rangos. Idealmente, debe hacer una búsqueda exacta ("=") en un índice en lugar de "mayor que", por lo que necesitaremos crear un índice como ese a partir de los datos que tiene disponibles. De esta forma, MySQL tendrá menos filas para evaluar mientras busca una coincidencia.

Para hacer esto, le sugiero que cree una tabla de búsqueda que indexe la tabla de geolocalización basada en el primer octeto (= 1 de 1.2.3.4) de las direcciones IP. La idea es que para cada búsqueda que tengas que hacer, puedes ignorar todas las IP de geolocalización que no comiencen con el mismo octeto que el IP que estás buscando.

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; 

continuación, tenemos que tomar los datos disponibles en la tabla de geolocalización y producir datos que cubre todo (primera) octetos la fila de geolocalización cubre: Si usted tiene una entrada con ip_start = '5.3.0.0' y ip_end = '8.16.0.0', la tabla de búsqueda necesitará filas de octetos 5, 6, 7 y 8. Así que ...

ip_geolocation 
|ip_start  |ip_end   |ip_numeric_start|ip_numeric_end| 
|72.255.119.248 |74.3.127.255 |1224701944  |1241743359 | 

Debe convertir a:

ip_geolocation_lookup 
|first_octet|ip_numeric_start|ip_numeric_end| 
|72   |1224701944  |1241743359 | 
|73   |1224701944  |1241743359 | 
|74   |1224701944  |1241743359 | 

Desde aquí alguien pidió una solución nativa de MySQL, aquí es un procedimiento almacenado que va a generar que los datos por usted:

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; 

Y entonces necesitará para completar la tabla llamando a ese procedimiento almacenado:

CALL recalculate_ip_geolocation_lookup(); 

En este punto, puede eliminar el procedimiento que acaba de crear; ya no es necesario, a menos que desee volver a calcular la tabla de búsqueda.

Después de que la tabla de búsqueda esté en su lugar, todo lo que tiene que hacer es integrarlo en sus consultas y asegurarse de que está consultando por el primer octeto.Su consulta a la tabla de consulta va a satisfacer dos condiciones:

  1. encuentra todos los registros que coinciden con el primer octeto de la dirección IP
  2. De ese subconjunto: buscar la fila que tiene el rango que coincide su dirección IP

Dado que el paso dos se lleva a cabo en un subconjunto de datos, es considerablemente más rápido que hacer las pruebas de rango en la información completa. Esta es la clave de esta estrategia de optimización.

Existen varias maneras de averiguar cuál es el primer octeto de una dirección IP; Solía ​​(r.ip_numeric & 0xFF000000) >> 24 desde mis direcciones IP de origen están en forma numérica:

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; 

Ahora, la verdad es que hizo un poco flojo en el final: Se puede fácilmente deshacerse de ip_geolocation mesa por completo si ha realizado la tabla ip_geolocation_lookup también contienen la datos del país. Supongo que dejar caer una tabla de esta consulta lo haría un poco más rápido.

Y, finalmente, estas son las otras dos tablas que utilicé en esta respuesta para referencia, ya que difieren de sus tablas. Estoy seguro de que son compatibles, sin embargo.

# 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; 
+0

Guau, respuesta extremadamente detallada. Por favor, dame un par de días para probar este enfoque. Parece, como solución de trabajo. – Oroboros102

+0

Esta consulta es mucho más rápida que fullscan, pero aún necesita escanear muchas filas (ranges_qty/255). Si usaremos geo ip por tabla de rango de ciudades (30 000 000 filas), esta consulta será lenta. Encontré alguna solución, que usa geometría. Si una de mis preguntas obtiene una respuesta adecuada (http://stackoverflow.com/questions/8244535/joins-on-spatial-mysql-indexes), tendré una mejor solución para esta pregunta. Si no, tu respuesta será la mejor. – Oroboros102

+0

La pregunta era realmente diferente. INNER JOIN funciona bien, mientras que LEFT JOIN tardará al menos 4 minutos en la tabla 2k entity_ip. –

0

sólo quería dar a la comunidad:

Aquí hay un edificio de manera aún mejor y optimizado en la solución de Aleksi:

DROP PROCEDURE IF EXISTS recalculate_ip_geolocation_lookup; 

DELIMITER ;; 
CREATE PROCEDURE recalculate_ip_geolocation_lookup() 
BEGIN 
    DECLARE i INT DEFAULT 0; 
DROP TABLE `ip_geolocation_lookup`; 

CREATE TABLE `ip_geolocation_lookup` (
    `first_octet` smallint(5) unsigned NOT NULL DEFAULT '0', 
    `startIpNum` int(10) unsigned NOT NULL DEFAULT '0', 
    `endIpNum` int(10) unsigned NOT NULL DEFAULT '0', 
    `locId` int(11) NOT NULL, 
    PRIMARY KEY (`first_octet`,`startIpNum`,`endIpNum`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

INSERT IGNORE INTO ip_geolocation_lookup 
SELECT startIpNum DIV 1048576 as first_octet, startIpNum, endIpNum, locId 
FROM ip_geolocation; 

INSERT IGNORE INTO ip_geolocation_lookup 
SELECT endIpNum DIV 1048576 as first_octet, startIpNum, endIpNum, locId 
FROM ip_geolocation; 

    WHILE i < 1048576 DO 
    INSERT IGNORE INTO ip_geolocation_lookup 
     SELECT i, startIpNum, endIpNum, locId 
     FROM ip_geolocation_lookup 
     WHERE first_octet = i-1 
     AND endIpNum DIV 1048576 > i; 
    SET i = i + 1; 
    END WHILE; 
END;; 
DELIMITER ; 

CALL recalculate_ip_geolocation_lookup(); 

Se basa mucho más rápido que su solución y profundiza más fácilmente porque no solo estamos tomando los primeros 8, sino los primeros 20 bits. Unir rendimiento: 100000 filas en 158ms. Es posible que deba cambiar el nombre de la tabla y los nombres de campo a su versión.

consulta utilizando

SELECT ip, kl.* 
FROM random_ips ki 
JOIN `ip_geolocation_lookup` kb ON (ki.`ip` DIV 1048576 = kb.`first_octet` AND ki.`ip` >= kb.`startIpNum` AND ki.`ip` <= kb.`endIpNum`) 
JOIN ip_maxmind_locations kl ON kb.`locId` = kl.`locId`; 
1

no puedo opinar todavía, pero las respuestas de user1281376 está mal y no funciona. la razón por la que solo usas el primer octeto es porque de lo contrario no coincidirás con todos los intervalos de IP. hay muchos rangos que abarcan múltiples segundos octetos que la consulta modificada del usuario1281376s no coincidirá. Y sí, esto realmente sucede si usas los datos de Maxmind GeoIp.

con la sugerencia de aleksis que puede hacer una comparación simple en el primer octeto, reduciendo así el conjunto coincidente.

+0

Probablemente debería haberlo comprobado, pero en ese momento decidí omitirlo ya que funcionaba de todos modos (recuerdo que también asumí que el autor ha hecho su tarea). Gracias –

+0

a la derecha, obviamente es más rápido, aunque especialmente con la tabla geoip de maxmind no coincidirá con el nivel 3, por ejemplo. Me tomó un tiempo darme cuenta de la primera vez que me encontré con esto. Por lo tanto, tendrá que agregar otra fila para end_range y luego seguirá atrapado con una consulta de rango. Y lo que es peor cuando no tiene una coincidencia para la ip, escaneará toda la tabla. – knrdk

0

Encontré una manera fácil. Me di cuenta de que todo el primer IP en el grupo% 256 = 0, por lo que puede agregar una tabla ip_index

CREATE TABLE `t_map_geo_range` (
    `_ip` int(10) unsigned NOT NULL, 
    `_ipStart` int(10) unsigned NOT NULL, 
    PRIMARY KEY (`_ip`) 
) ENGINE=MyISAM 

Cómo llenar la tabla de índice

FOR_EACH(Every row of ip_geo) 
{ 
    FOR(Every ip FROM ipGroupStart/256 to ipGroupEnd/256) 
    { 
     INSERT INTO ip_geo_index(ip, ipGroupStart); 
    } 
} 

Modo de empleo:

SELECT * FROM YOUR_TABLE AS A 
LEFT JOIN ip_geo_index AS B ON B._ip = A._ip DIV 256 
LEFT JOIN ip_geo AS C ON C.ipStart = B.ipStart; 

Más de 1000 veces más rápido.

+0

Consulte la respuesta anterior. –