データベース上の位置情報を効率的に検索する方法(PostgreSQL編)
結論はPostgreSQLの幾何データ機能を有効に使おうということである(MySQLでも同様の機能はあるけど)。 とにかくあいかわらず地理情報データとGoogleMaps APIの組み合わせが アツイようなので、その基本的な要件となるであろう、 データベース上の位置情報を効率的に検索する方法を紹介したい。
たとえばおいしいケーキ屋さんの位置情報がデータベース上にあるとしよう。 お店の名前とその緯度経度の情報が数百件あるとする。 GoogleMapsなどである範囲の地図を表示したとして、 お店の位置を地図上にマーキングさせたい場合には、 その地図の範囲の情報をキーにしてデータベース上の緯度経度を検索する必要がある。
普通に考えうるSELECT文は次のとおり。なお、x1,y1は地図の左上の位置、 x2y2は地図の右下の位置とする。
select 店名,緯度,経度 from shopひとまずこれで要件は満たせるだろう。 だが、ひとたび
where 緯度<=y1 and 緯度>=y2 and 経度<=x2 and 経度>=x1
- ある1点から半径rの円内に該当するデータを検索したい
- さらにその検索結果を、中心点からの距離でソートしたい
まずは実験環境を用意する。PostgreSQL8.1.4をインストールし、 2次元座標データ型を持つテーブルを作成。こんな感じ。
create table geodata (id int4, geo point);pointという聞きなれないデータ型が指定されていることにご注目。 VARCHAR型とかDATE型といった一般的な型しかないOracleなどと違って、 PostgreSQLはこういう多彩なデータ型を利用することができる。
実際にデータを入れる場合はこんな感じになる。
insert into geodata (id, geo) values (1, point(3,4))x座標で3、y座標で4というデータがはいる。 point()は通常の数値を幾何型に変換する関数。
テストのために、アトランダムな座標を持つデータを10万件ほどつっこんでおく。
testdb=> select * from geodata;
id | geo
-------+--------------
0 | (726,3654)
1 | (3804,8386)
2 | (3644,2121)
3 | (3079,1080)
(以下省略。全10万レコード)
ちなみにx,y座標を明確に取り出したい場合には
といった感じで可能。testdb=> select geo[0] as x, geo[1] as y from geodata; x | y -------+------- 726 | 3654 3804 | 8386
手始めに、ある矩形(長方形のこと)の範囲内にあるレコードを検索してみる。
成功。@は「含む、または境界上」という演算子。 boxは二つのpointを対角とした長方形を示す。testdb=> select * from geodata where geo @ box(point(100,100),point(200,200)); id | geo -------+----------- 41282 | (198,151) 44787 | (105,103) 91065 | (179,104) 96581 | (125,100) 97615 | (112,142) (5 rows)
次に、ある円の範囲内にあるレコードを検索してみる。
circle(point(300,300),40) がミソ。これはx=300,y=300の座標を中心とする半径40の円を表す。testdb=> select * from geodata where geo @ circle(point(300,300),40); id | geo -------+----------- 5501 | (289,336) 20403 | (270,306) 43715 | (319,265) 74237 | (274,293) 84113 | (326,327) (5 rows)
さらに、上の例で、中心からの距離で結果をソートしてみる。
疑り深い人は↓testdb=> select * from geodata where geo @ circle(point(300,300),40) order by geo <-> point(300,300); id | geo -------+----------- 74237 | (274,293) 20403 | (270,306) 84113 | (326,327) 5501 | (289,336) 43715 | (319,265) (5 rows)
testdb=> select id, geo, geo <-> point(300,300) as dist from geodata where geo @ circle(point(300,300),40) order by geo <-> point(300,300); id | geo | dist -------+-----------+------------------ 74237 | (274,293) | 26.9258240356725 20403 | (270,306) | 30.5941170815567 84113 | (326,327) | 37.4833296279826 5501 | (289,336) | 37.6430604494374 43715 | (319,265) | 39.8246155034798 (5 rows)
さて気になるのは性能だ。インデックスは効くのだろうか?
おっと。インデックスを張れない! インデックスの種類の問題かと思ったがtestdb=> create index testindex on geodata (geo); ERROR: data type point has no default operator class for access method "btree" HINT: You must specify an operator class for the index or define a default operator class for the data type.
デフォルトのbtreeはもちろんgistもrtreeもだめ。 こいつは困ったなんでだろう?testdb=> create index testindex on geodata using gist (geo); ERROR: data type point has no default operator class for access method "gist" HINT: You must specify an operator class for the index or define a default operator class for the data type. testdb=> create index testindex on geodata using rtree (geo); ERROR: data type point has no default operator class for access method "rtree" HINT: You must specify an operator class for the index or define a default operator class for the data type.
実は、現在のPostgreSQLでは、インデックスを張ることができるのは box型(長方形)、circle型(円)、polygon型(多角形)の3種類だけ。 point型にはインデックスを張れない。 マニュアルのここの下のほうにあるSELECTを投げて調べてみたが、 幾何データ型と思われるものは上の3つの演算子についてしか見当たらない。 つまり、「boxをboxでインデックス検索」「円を円でインデックス検索」 「多角形を多角形でインデックス検索」ということはできても、 「円で点をインデックス検索」といったことができないのだ。 なんでそんなことになっているのかは知らないがとにかくそうなっている。 もちろん将来のバージョンで改善されることを期待したいところだ。 DBにおけるインデックスの内部構造とC言語に自信のある人はpoint型用のgist演算子クラスの実装にチャレンジしてみてはいかがだろう。
しかし、現在のPostgreSQLであってもちょっとした方法で解決できる。 PostgreSQLには関数インデックスという便利なインデックスの張り方がある。 値に対してそのままインデックス化するのではなく、 何らかの一定の関数を通した結果をインデックスしておくというものだ。 この機能と、「点は、半径ゼロの円である」「点は、4点全てが同じ長方形である」 といった幾何学的トリックを組み合わせればいい。
これでgeo列(point型)に対してそれを半径ゼロのcircleとする関数をかませて circle型とし、それに対してインデックスが張れたことになる。 検索する場合には次のようにする。testdb=> create index testindex on geodata using gist(circle(geo,0)); CREATE INDEX testdb=>
OK。さっきと結果は同じ。testdb=> select * from geodata where circle(geo,0) @ circle(point(300,300),40); id | geo -------+----------- 5501 | (289,336) 20403 | (270,306) 43715 | (319,265) 74237 | (274,293) 84113 | (326,327) (5 rows)
では、インデックスを張ったことでどの程度の効果が出たのだろうか。 先ほどのインデックスを一旦dropして、explainで速度を量ってみる。
testdb=> drop index testindex;
DROP INDEX
testdb=> explain analyze select * from geodata where circle(geo,0) @ circle(point(300,300),40);
QUERY PLAN
----------------------------------------------------------------------------------------------------------
Seq Scan on geodata (cost=0.00..2137.00 rows=100 width=20) (actual time=21.102..381.205 rows=5 loops=1)
Filter: (circle(geo, 0::double precision) @ '<(300,300),40>'::circle)
Total runtime: 404.447 ms
(3 rows)
当然ながらSeq Scan(全走査)されている。インデックスを張った状態で同じSELECTをするとどうなるか。
testdb=> create index testindex on geodata using gist(circle(geo,0));
CREATE INDEX
testdb=> explain analyze select * from geodata where circle(geo,0) @ circle(point(300,300),40);
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on geodata (cost=2.35..268.61 rows=100 width=20) (actual time=0.720..0.781 rows=5 loops=1)
Filter: (circle(geo, 0::double precision) @ '<(300,300),40>'::circle)
-> Bitmap Index Scan on testindex (cost=0.00..2.35 rows=100 width=0) (actual time=0.654..0.654 rows=5 loops=1)
Index Cond: (circle(geo, 0::double precision) @ '<(300,300),40>'::circle)
Total runtime: 0.978 ms
(5 rows)
インデックススキャンが走るようになった。実行時間も、404ミリ秒から0.9ミリ秒へ。
実に400倍のスピードアップである。
このように、PostgreSQLの幾何データ機能は有効に使えるものだ。 詳しくはマニュアルを。
see also:
Google Maps とIEの「開けません。 操作は中断されました」(2006.4)
Google Maps APIと文字コード(2005.9)
地図情報と郵便番号(2005.6)

コメント
そういうインデックスの張り方がありましたか!
ちょっと感動的。
Posted by Minoru Araki at 2006年6月17日
おお。これはいいですね。めもめも。
いまMysqlで同じようなことやってますが、
あっちは直接計算させて出してました・・・。
曲率を考慮した楕円体からの距離計算とかやるとなるとめんどそうですね。
Posted by makinux at 2006年6月18日
「VARCHAR型とかDATE型といった一般的な型しかないOracleなどと違って」とのことですが、Oracle Spatial というものもあるようです。
http://otn.oracle.co.jp/products/spatial/
OTN Japan - 製品情報 : Oracle Spatial
Posted by NI-Lab. at 2006年6月18日
Oracle Spatialってお値段おいくらなんでしょうね。
Posted by watanabe at 2006年6月19日
コメントする
(初めてのコメントの時は、コメントが表示されるためにこのブログのオーナーの承認が必要になることがあります。承認されるまでコメントは表示されませんのでしばらくお待ちください)