空間データを効率良く検索するためには、インデックス構造が必要です.
しかしながら B-tree INDEXのような一次元的なデータ構造は、空間データといった多次元空間に属するデータを扱うには不向きです. (Geohashという緯度経度の情報を文字列のような B-tree に適したデータにハッシングする方法もありますが)
理論編では R-tree INDEX という多次元データを効率良く扱うためのデータ構造についてと、実践編ではPostgreSQL(PostGIS) や MySQL を用いた空間インデックスの使い方について紹介していこうと思います。
R-treeインデックスは多次元のデータを格納することができますが、以下では簡単のため2次元のデータを例にとって解説していきます。
この記事を読むにあたって B-tree についてある程度の知識があると分かり易いかもしれません。
空間データ
空間データとしては
- 一点を指す
POINT
- 二つの地点を結ぶ線分である
LINESTRING
- ある空間を囲う領域を示す
POLYGON
などがあります。PostGISだと以下のようなSQLでデータを挿入することになります。
CREATE TABLE geometries (name varchar, geom geometry); INSERT INTO geometries VALUES /* (0, 0) の点 */ ('Point', 'POINT(0 0)'), /* (0, 0) と (1, 1) を結ぶ線分 */ ('Linestring', 'LINESTRING(0 0, 1 1)'), /* (0, 0) と (1, 0) と (1, 1) を頂点とする領域 */ ('Polygon', 'POLYGON((0 0, 1 0, 1 1))');
R-tree インデックスを使うことで
- ある点を包含するレコードを取得
- 指定した矩形の範囲内に含まれるレコードを取得
などといった検索を効率的に実行することができます。
R-tree INDEX
R-tree はそれぞれの節点の要素に
- 子節点へのポインタ
- 子節点が持つすべての要素の空間データを含む 最小外接矩形(Minimum Bounding Rectangle 以下MBR)のデータ
のタプルを持ち、葉節点の場合は
を持ちます。
例えば以下の図において赤線部分がデータオブジェクトとすると、それを囲うMBRは破線部分となります。
それでは R-tree インデックスの例を見ていきましょう。
上の図が実際のデータ構造、下の長方形がたくさんある図は各節点が持つ最小外接矩形(MBR)を描いたイメージ図です。
- B(+)-TREE と同じようにひとつのノードに直接ぶら下がる部分木の高さの差が1であり
- 葉ノードにはデータオブジェクトへのポインタが格納されています
(これはB+Treeと同じですね)
各節点の要素数
各節点が持つ最大要素数をM
とするとき(上の図では3のようです)、m <= M/2
というようにm
を定めると
- 各節点はm個以上M個以下の要素を持つ
- 根節点は(それが葉節点でない限り)最低2つの要素を持つ
mの値を変えることによって、各節点のデータサイズの大小、木の高さが変わってくるものでありパフォーマンスにも関わってきます。
参照処理
点検索
上の図の赤い地点を包含するレコードを探索したい場合(答えはR8, R9)のアルゴリズムは以下のようになります。
- 根節点から出発
- もしその節点が葉節点でない場合、節点内の各要素に関して検索地点を包含する要素すべて探索し、ヒットしたそれぞれの要素の指す子節点について再帰的に2を実行
- その節点が葉節点な場合、その節点の各要素が持つに関して検索地点を包含する要素すべて探索しヒットしたそれぞれが持つデータオブジェクトidを取得
上の図の例の場合だと
- 根節点から探索(R1がマッチ)
- R1に進む、R1は葉節点でないのでR1の要素を探索(R3,R4がマッチ)
- R3,R4に進む、R3ではR8が、R4ではR9がマッチ
- R8, R9 はそれぞれ葉節点なのでこれらの節点が持つデータオブジェクトのidを取得
という感じで探索が進みます。
範囲検索
上の図の赤い部分と重なる空間データを取得するクエリです。(答えはR7, R8, R9, R10)
これを探索するアルゴリズムは以下のような感じ
- 根節点からスタート
- その節点が葉節点でない場合、節点内で検索範囲と重なる要素を探索 ヒットしたそれぞれの要素の指す子節点について再帰的に2を実行
- その節点が葉節点な場合、その節点の各要素が持つに関して検索地点を包含する要素すべて探索しヒットしたそれぞれが持つデータオブジェクトidを取得
上の図の例の場合だと
- 根節点から探索(R1, R2がマッチ)
- R1以下ではR3, R2内ではR4がマッチ
- R3,R4に進む、R3ではR7, R8が、R4ではR9, R10がマッチ
- R7~R10 はそれぞれ葉節点なのでこれらの節点が持つデータオブジェクトのidを取得
データの挿入・削除
R-tree では B-tree と同様に挿入・削除・更新処理の際に木の再構築が行われ平衡な木の状態が保たれます。 これにより木が偏ることによって参照処理の最悪計算量が増大してしまうことを防ぐことができます。
挿入処理
この記事では少し噛み砕いた形でアルゴリズムを紹介します。厳密なアルゴリズムを知りたい場合はこちらの論文の3.2を参照してください:bow:
新しく木に追加する要素をE
とします。
R5
には空きがなかったので、以下のように"良い感じ"にR16
とR17
に分割されます
(このときR15
,R16
,R6
の節点が溢れた場合同様の分割がどんどん行われていくことになります)
節点の分割
上で述べたように、満杯になった節点に要素を追加するためには節点を2つのグループに分割が必要です。インデックス構造を平衡に保ち・空間的な効率も良くデータを格納するためにはできるだけ各節点のMBRが小さくなるように工夫して節点の分割を行う必要があります。
分割の方法として以下のようなアルゴリズムがあります。
Exhaustive Algorithm
MBRが最小になるよう分割するにはすべての組み合わせに対して全探索を行います。
確実ではありますが計算量はO(2^M)
とかなり大きくなり挿入処理の効率が悪くなってしまうかもしれません。
Quadratic-Cost Algorithm
名前の通り計算量はO(M^2)
ですが、以下に示すアルゴリズムの1の計算量がO(M^2)
なだけであり残りの処理は少ない計算量で実行することができるため、Exhastive Algorithm よりも少ない試行回数で実行することができます。
- その節点中の要素から最も
area(MBR)-area(要素1)-area(要素2)
が大きくなるような要素のペアを選択し、それぞれの要素をそれぞれのグループに分配(O(M^2)
)- もし全ての要素が分配完了し、それぞれのグループの要素数がm(各節点の最低要素数)以上ならば分割を完了する (
O(1)
)- まだグループに分配されてない要素それぞれについて、各グループに分配された場合のMBRの大きさを計算 (
O(M)
)
すべての要素から、グループ1に分配された場合のarea(MBR)-area(要素)-area(グループ1)
の大きさと、グループ2に分配された場合のarea(MBR)-area(要素)-area(グループ2)
の大きさの差が一番大きいものを選択- 選択された要素を分配後のMBRのサイズが小さくなる方のグループに分配
分配後2に戻る
下図のような節点(赤い四角が要素)を分割する例を示そうと思います。(外枠は節点に対応するMBRを表します)
以下のようにarea(MBR)-area(要素1)-area(要素2)
(青い部分)が最大となるペアを選択
黄枠をグループ1、青枠をグループ2、赤枠を未分類の要素とします。
d=area(MBR)-area(要素)-area(グループ)
の差が一番大きいものを選択し(真ん中にある赤枠の要素)次の分配対象とします。
そして中心の要素の分配では、今回の場合d
の値がより小さい青のグループに分配します。
Linear-Cost Algorithm
Linear-Cost Algorithm 基本的には Quadratic-Cost Algotithm と同じなのですが、グループに分配する要素の選択に用いられるアルゴリズムが異なります。
Quadratic-Cost Algorithm の一番最初の要素のペアを選択するアルゴリズムが以下のようなものになります。(Quadの方では計算量O(M^2)
なのに対してLinearではO(M)
)
- 節点に対応するMBR内の各要素のうち、各軸(2次元ならx軸y軸)について最も離れた要素のペアを選択
- 各軸のペアについて、正規化された距離を計算
(各要素の距離 / MBRの対応する軸の長さ
)- 計算した正規化した距離のうち、より大きい距離のペアの要素をそれぞれのグループに分配
以下の場合、縦軸で最も離れたペアはE1とE2、横軸で最も離れたペアはE1とE3
それぞれの正規化された距離はd1/D1
とd2/D2
となります。今回の場合d1/D1
の方が大きいため、E1
とE3
がそれぞれのグループの一つ目の要素として分配されることになります。
また残りの要素をグループに分配していく処理は、それぞれランダムに分配していきます(O(1)
)
最初のペアの選択である程度分割後の節点に対応するMBRを小さくし、残りの要素の分配は正確性を犠牲にパフォーマンスをとったという感じです。
削除
木の再構築のアルゴリズム
削除対象となった葉節点について以下のようなアルゴリズムを用いて木の再構築を行う
葉節点から要素が少なくなった節点を削除という処理を親節点にどんどん適応していき、最後に削除した要素をすべて再挿入するというアルゴリズムです。
B-tree の場合、要素数が少なくなりすぎて節点は兄弟節点のうち空きのある節点と要素数を調整・マージという処理が行われるのに対して、R-tree では一度データを木から削除してそれを再挿入するというヘビーな処理が行われています。しかしこのような処理を行う理由は以下の二つがあります。
- 挿入処理を再度利用することができて簡単
- 再挿入処理で空間的な構造を再構築することによって、各要素が(効率が悪いのに)同一の節点にぶら下がり続けることによる検索効率の悪化を防ぐことができる
更新処理
更新の際もレコードをインデックスから削除して、再挿入が行われます。
まとめ
このように R-tree インデックスを用いることで空間データを効率的に扱うことができるようになります。
空間インデックスの方法として他にも空間充填曲線を用いたインデックス方法や、R-tree の改良として R+-tree や R*-tree などがありますが MySQL や PostGIS などで実際に使われているインデックス構造の元となる R-tree を紹介しました。(機会があればそちらも紹介していきたい)
SPATIAL INDEX
の動作の理解の助けとなれば幸いです。
また今回参照した論文には、この記事の内容に加えてm
の値を変動・節点の分割アルゴリズムの変更によるパフォーマンスの計測・CPUや空間効率性の計測データが掲載されています、詳細が気になる方は論文を読むと良さそうです。
実践編では MySQL・PostGIS を用いた空間データ処理について紹介したいと思います。(5月くらいまでには書きたい)
参考文献
- R-TREES. A DYNAMIC INDEX STRUCTURE FOR SPATIAL SEARCHING - Antomn Guttman - University of Cahforma Berkeley
- R-Trees: A Dynamic Index Structure for Spatial Searching - Antonin Guttman (slide)
- Boundless : Introduction to PostGIS (Japanese) : 第8章: ジオメトリ
- 空間アクセス法 - 京都大学 田中克己 角谷和俊
- MySQL 5.7 Reference Manual - Creating Spatial Indexes
- PostGIS 2.2.3dev Manual