基於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/

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