基于IP查找相应国家、地区

URL: http://code.openark.org/blog/mysql/sql-finding-a-users-countryregion-based-on-ip


我先后两次遇到过相同的问题,所以认为有必要讨论一下。


在web应用中,通过侦测HTTP请求,基于IP找到用户所在的国家或地区是一项基本的功能。而且依据获得的信息,website可以进行相应的I10N的工作。


要开始这项工作,需要有一个对照表列出IP段对应的国家或地区。这里假设我们仅处理IPv4的情况:

CREATE TABLE regions_ip_range (
  regions_ip_range_id INT UNSIGNED AUTO_INCREMENT,
  country VARCHAR(64) CHARSET utf8,
  region VARCHAR(64) CHARSET utf8,
  start_ip INT UNSIGNED,
  end_ip INT UNSIGNED,
  …
  PRIMARY KEY(regions_ip_range_id),
  ...
);


这个表被填充并用来做对照表。现在的问题是:我们如何查询这张表,并如何创建index。


错误的方式


我遇到的方式是:
    KEY ip_range_idx (start_ip, end_ip)

然后查询如下:
    SELECT * FROM `regions_ip_range` WHERE `my_ip` BETWEEN `start_ip` AND `end_ip`


这种对索引的理解是错误的。我并不是说结果是错误的,只是这个查询的效率会很差。重写这段查询就会明白。下面这段查询和之前的是等价的:
    SELECT * FROM `regions_ip_range` WHERE `my_ip` >= `start_ip` AND `my_ip` <= `end_ip`

发现问题了么?


在第一个索引列上有一个范围条件(range condition),自动取消了第二列上的索引使用。反转查询中的顺序也一样。


实际上,如果我们这和我们仅仅在start_ip上定义索引的效果是一样的。
    KEY ip_range_idx (start_ip)

可是,这是不允许的。很容易想到(事实上也确实是这样),针对大量的IP 地址,MySQL会执行一次全表查询而非使用索引。


另外一个错误的方式是:

    KEY start_ip_idx (start_ip)
    KEY end_ip_idx (end_ip)


即为每个地址创建了一个单独的索引。但我们并不会这么做,因为即使我们觉得MySQL一定会针对我们的查询使用两个索引,然后做一个索引合并(index_merge),也得不到一个高效的查询。试想:对于一个给定的IP,不可能选择两个不同的索引。要么这个IP趋近与区域的下界('my_ip >= start_ip'部分不会被选用),要么趋近与上界('my_ip <= end_ip'不会被选用),或者在中间,那样都不选用了。


事实上,很难想象MySQL会选择执行索引合并,所以最多使用一个索引,不然就全表查询。


解决方案


解决这个问题中重要的一步是认识到IP区域是相互排斥的。没有一个IP能存在于两个区域中,只能是一个(至少,这里是这样的。如果你面对一个分层的区域,就要自己做决定了...)。这就意味着我不需要对两个列都做索引。一个就足够了。

    KEY start_ip_idx (start_ip)


然后我们改写SQL 如下:
    SELECT * FROM `regions_ip_range`
    WHERE `start_ip` <= `my_ip`
    ORDER BY `start_ip` DESC LIMIT 1


现在,我们要找的是就是符合“我们的IP大于区域起始IP”这个条件的第一个结果。优化器就是要使用索引找到第一个,然后无需再继续了,于是加上LIMIT 1。

 

如果觉得迷惑,可以做相反的操作。定义如下:
    KEY end_ip_idx (end_ip)

然后修改查询:
    SELECT * FROM regions_ip_range
    WHERE my_ip <= end_ip
    ORDER BY end_ip ASC LIMIT 1


有趣的是,由于没有考虑LIMIT 1,EXPLAIN仍然声明将要检查很多行记录。

 

我曾经写过关于“存储引擎之间对于优化器使用(或不使用)索引的不同”的文章。所以,最终可能要使用FORCE_INDEX。

 

假设

这里,我有几个假设:

   1. 表列出的IP段是从0.0.0.0到255.255.255.255。
   2. 对于给定的一个IP,其所属的IP段都有明确的上下界。
   3. 各IP段相互排斥(不存在分层的IP段)

如果前两个假设不满足,则查询返回时,应该检查my_ip是否确实在start_ip和end_ip之间。

如果第三个假设不满足,可以将数据分拆到两个表中:一个包含互斥的数据。另一个包含其他部分数据,也许可以利用用一些分层算法(嵌套集合等)。


备注

 

1. 评论中有人提到了不该用数据库做这样的功能,相应的应该使用libgeoip。

2. 还有一篇类似的博文:http://jcole.us/blog/archives/2007/11/24/on-efficiently-geo-referencing-ips-with-maxmind-geoip-and-mysql-gis/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章