SP-GiSTは、空間分割された(Space-Partitioned)GiSTを短縮した語です。 SP-GiSTは分割された探索木をサポートし、四分木、kd木、基数木(トライ木)など広範にわたる様々な非平衡データ構造の開発を可能にします。 これらの構造に共通の特徴は、それらが探索空間を繰り返し小さな領域に分割し、その領域の大きさが必ずしも等しくない、ということです。 分割規則によく適合した検索は非常に高速になります。
これらのよく使われるデータ構造は、元々はメモリ内での利用のために開発されたものでした。 主記憶上では、それらは通常、ポインタにより接続され、動的に割り当てられるノードの集合として設計されます。 このようなポインタのチェーンは長くなりがちで、非常に多くのディスクアクセスが必要となるため、ディスク上に直接格納するには適しません。 これとは反対に、ディスクベースのデータ構造は、I/Oを最小化する、大きな論理出力数を持つべきです。 SP-GiSTによって解決される困難とは、探索木のノードをディスクのページにマップするときに、多数のノードを通り抜ける場合であっても、探索ではごく少数のディスクページにしかアクセスしないですむようにすることです。
GiSTと同じく、SP-GiSTは適切なアクセス方法のある独自のデータ型の開発を可能にするためのもので、データベースのエキスパートよりもむしろ、そのデータ型の領域のエキスパートによる開発を可能にします。
ここで記述する情報の一部はPurdue大学のSP-GiSTインデックスプロジェクトWEBサイトによるものです。 PostgreSQLでのSP-GiSTの実装は、おもにTeodor SigaevとOleg Bartunovによって保守されており、詳しい情報は彼らのWEBサイトにあります。
PostgreSQLのコアディストリビューションには表 64.2に示されるSP-GiSTの演算子クラスが含まれます。
表64.2 組み込みSP-GiST演算子クラス
名前 | インデックス可能な演算子 | 順序付け演算子 |
---|---|---|
box_ops | << (box,box) | <-> (box,point) |
&< (box,box) | ||
&> (box,box) | ||
>> (box,box) | ||
<@ (box,box) | ||
@> (box,box) | ||
~= (box,box) | ||
&& (box,box) | ||
<<| (box,box) | ||
&<| (box,box) | ||
|&> (box,box) | ||
|>> (box,box) | ||
inet_ops | << (inet,inet) | |
<<= (inet,inet) | ||
>> (inet,inet) | ||
>>= (inet,inet) | ||
= (inet,inet) | ||
<> (inet,inet) | ||
< (inet,inet) | ||
<= (inet,inet) | ||
> (inet,inet) | ||
>= (inet,inet) | ||
&& (inet,inet) | ||
kd_point_ops | |>> (point,point) | <-> (point,point) |
<< (point,point) | ||
>> (point,point) | ||
<<| (point,point) | ||
~= (point,point) | ||
<@ (point,box) | ||
poly_ops | << (polygon,polygon) | <-> (polygon,point) |
&< (polygon,polygon) | ||
&> (polygon,polygon) | ||
>> (polygon,polygon) | ||
<@ (polygon,polygon) | ||
@> (polygon,polygon) | ||
~= (polygon,polygon) | ||
&& (polygon,polygon) | ||
<<| (polygon,polygon) | ||
&<| (polygon,polygon) | ||
|>> (polygon,polygon) | ||
|&> (polygon,polygon) | ||
quad_point_ops | |>> (point,point) | <-> (point,point) |
<< (point,point) | ||
>> (point,point) | ||
<<| (point,point) | ||
~= (point,point) | ||
<@ (point,box) | ||
range_ops | = (anyrange,anyrange) | |
&& (anyrange,anyrange) | ||
@> (anyrange,anyelement) | ||
@> (anyrange,anyrange) | ||
<@ (anyrange,anyrange) | ||
<< (anyrange,anyrange) | ||
>> (anyrange,anyrange) | ||
&< (anyrange,anyrange) | ||
&> (anyrange,anyrange) | ||
-|- (anyrange,anyrange) | ||
text_ops | = (text,text) | |
< (text,text) | ||
<= (text,text) | ||
> (text,text) | ||
>= (text,text) | ||
~<~ (text,text) | ||
~<=~ (text,text) | ||
~>=~ (text,text) | ||
~>~ (text,text) | ||
^@ (text,text) |
point
型の2つの演算子クラスのうち、quad_point_ops
がデフォルトです。
kd_point_ops
は同じ演算子をサポートしますが、異なるインデックスデータ構造を使うため、アプリケーションによってはより良いパフォーマンスを提供することがあります。
quad_point_ops
、kd_point_ops
、poly_ops
演算子クラスは<->
順序付け演算子をサポートしますので、インデックス付けされた点や多角形のデータ集合に対してk近傍(k-NN
)探索が可能です。
SP-GiSTは高度に抽象化されたインタフェースを提供します。アクセスメソッドの開発者は特定のデータ型専用のメソッドだけを開発する必要があります。 SP-GiSTのコアは効率的なディスクマッピングと木構造の探索を担当します。 また、同時実行制御とログ出力も担当します。
SP-GiSTのツリーのリーフタプルは、インデックスの付けられた列の損失のある表現を含むこともできますが、通常はインデックスの付けられた列と同じデータ型の値を含んでいます。 ルートレベルに格納されたリーフタプルは、インデックスが付けられた元のデータの値を直接表現していますが、より下のレベルのリーフタプルは、接尾辞など、部分的な値しか含んでいないかも知れません。 この場合、演算子クラスのサポート関数が、内部タプルをリーフレベルまでたどりながら集める情報を使って元の値を再構築できる必要があります。
SP-GiSTインデックスがINCLUDE
列を付けて作成された場合には、その列の値もリーフタプルに格納されます。
INCLUDE
列はSP-GiST演算子クラスとは関係ありませんので、ここではこれ以上説明しません。
内部タプルは、探索木の分岐点となるため、もっと複雑です。 それぞれの内部タプルは1つ以上のノードの集合を含んでおり、ノードは類似のリーフ値のグループを表現します。 ノードは下向きのリンクを含んでおり、これは下のレベルの別の内部タプルを指すか、あるいはすべて同じインデックスページ上に載っているリーフタプルの短いリストを指しています。 それぞれのノードは、通常、それを記述するラベルを持っています。 例えば、基数木では、ノードのラベルは文字列の値の次の文字にすることができます。 (あるいは、すべての内部タプルについて、決まったノードの集合しか扱わないのであれば、演算子クラスはノードのラベルを省略することができます。 64.3.4.2を参照してください。) 省略可能ですが、内部タプルはそのすべてのメンバを記述する接頭辞の値を持つことができます。 基数木では、これは表現される文字列に共通の接頭辞とすることができます。 接頭辞の値は、必ずしも本当の接頭辞である必要はなく、演算子クラスが必要とする任意の値で良いです。 例えば四分木では、その中心点を保持し、4つの象限をそこから相対的に測るようにできます。 そうすると、四分木の内部タプルはこの中心点の周りの象限に対応する4つのノードも含むことになるでしょう。
木構造のアルゴリズムには、現在のタプルのレベル(深さ)を知っていることが必要なものがあります。そこで、SP-GiSTのコアは、演算子クラスが木構造をたどって下がるときにレベル数の管理を可能にしています。 また、必要であれば、表現される値を加算的に再構築すること、また木構造を下る間に追加データ(探索値と呼ばれます)を渡すこともサポートしています。
SP-GiSTのコアのコードはnullエントリについても対応しています。 SP-GiSTのインデックスはインデックス列がnullのエントリについても格納しますが、これはインデックスの演算子クラスのコードからは隠されているので、nullのインデックスエントリや検索条件が演算子クラスのメソッドに渡されることはありません。 (SP-GiSTの演算子は厳格なのでNULL値について成功を返すことはできないと想定されています。) 従って、ここではこれ以上、NULLについて説明しません。
SP-GiSTのインデックス演算子クラスが提供しなければならないユーザ定義メソッドは5つあり、加えて、オプションのメソッドが2つあります。
5つの必須メソッド全ては2つのinternal
引数を受け付けるというしきたりに従い、1つ目はサポートメソッドへの入力値を含むC構造体へのポインタで、一方2つ目は出力値が配置されるであろうC構造体へのポインタです。
4つの必須メソッドでは、その結果がすべて出力構造体の中にあるので、単にvoid
を返します。ですが、leaf_consistent
はboolean
の結果を返します。
メソッドは、その入力構造体のどのフィールドも変更してはいけません。
どんな場合でも、出力構造体はユーザ定義メソッドを呼び出す前にゼロに初期化されます。
オプションの6番目のメソッドcompress
は、唯一の引数としてインデックス付けされるdatum
を受け付け、リーフタプルの物理格納に適した値を返します。
オプションの7番目のメソッドoptions
は、演算子クラスに固有のパラメータを入れるC構造体へのinternal
ポインタを受け付け、void
を返します。
5つの必須ユーザ定義メソッドは以下のとおりです。
config
接頭辞とノードラベルのデータ型のデータ型OIDを含め、インデックスの実装に関する静的情報を返します。
この関数のSQL宣言は以下のようになります。
CREATE FUNCTION my_config(internal, internal) RETURNS void ...
1番目の引数はCのspgConfigIn
構造体へのポインタで、関数の入力データを含みます。
2番目の引数はCのspgConfigOut
構造体へのポインタで、関数が結果のデータを入れます。
typedef struct spgConfigIn { Oid attType; /* インデックス付けされるデータ型 */ } spgConfigIn; typedef struct spgConfigOut { Oid prefixType; /* 内部タプルの接頭辞のデータ型 */ Oid labelType; /* 内部タプルのノードのラベルのデータ型 */ Oid leafType; /* リーフタプル値のデータ型 */ bool canReturnData; /* 演算子クラスは元のデータを再構築できる */ bool longValuesOK; /* 演算子クラスは1ページより大きな値を扱える */ } spgConfigOut;
attType
は多様のインデックス演算子クラスをサポートするために渡されます。
通常の固定データ型の演算子クラスでは、それは常に同じ値を持っているので無視できます。
接頭辞を使わない演算子クラスでは、prefixType
をVOIDOID
に設定することができます。
同様に、ノードラベルを使わない演算子クラスでは、labelType
をVOIDOID
に設定することができます。
演算子クラスが、元々提供されていたインデックスの値を再構築できるときは、canReturnData
をtrueにします。
attType
が可変長で、演算子クラスが接尾辞付けの繰り返しによって長い値を分割できるときにのみ、longValuesOK
をtrueにします(64.3.4.1参照)。
leafType
は、演算子クラスのopckeytype
カタログエントリにより定義されたインデックス格納型と一致しなければなりません。
(opckeytype
は0の場合もあり得て、それは格納型が演算子クラスの入力型と同じであることを意味しています。これが最も一般的な状況であることに注意してください。)
後方互換性のため、config
メソッドはleafType
を他の値に設定して、その値を使うことができます。ですが、インデックスの内容がカタログでは誤って特定されますので、これは非推奨です。
また、leafType
を初期化しないまま(0)にできます。これはopckeytype
から導かれたインデックス格納型を意味すると解釈されます。
attType
とleafType
が異なる場合には、オプションのメソッドcompress
を提供しなければなりません。
メソッドcompress
は、インデックス付けされるデータをattType
からleafType
に変換する責任があります。
choose
内部タプルに新しい値を挿入するときのメソッドを選択します。
この関数のSQL宣言は以下のようになります。
CREATE FUNCTION my_choose(internal, internal) RETURNS void ...
1番目の引数はCのspgChooseIn
構造体へのポインタで、関数の入力データを含みます。
2番目の引数はCのspgChooseOut
構造体へのポインタで、関数が結果のデータを入れます。
typedef struct spgChooseIn { Datum datum; /* インデックス付けされる元のデータ */ Datum leafDatum; /* リーフに保存されている現在のデータ */ int level; /* (0から数えた)現在のレベル */ /* 現在の内部タプルからのデータ */ bool allTheSame; /* タプルはall-the-sameと印が付けられているか? */ bool hasPrefix; /* タプルは接頭辞を持つか? */ Datum prefixDatum; /* もしそうなら、接頭辞の値 */ int nNodes; /* 内部タプルの中のノード数 */ Datum *nodeLabels; /* ノードのラベルの値(なければNULL) */ } spgChooseIn; typedef enum spgChooseResultType { spgMatchNode = 1, /* 既存のノードに下がる */ spgAddNode, /* ノードに内部タプルを追加する */ spgSplitTuple /* 内部タプルを分割する(その接頭辞を変更する) */ } spgChooseResultType; typedef struct spgChooseOut { spgChooseResultType resultType; /* アクションコード、上記参照 */ union { struct /* spgMatchNodeの結果 */ { int nodeN; /* このノードに下がる(0からのインデックス) */ int levelAdd; /* この分だけレベルを増やす */ Datum restDatum; /* 新しいリーフデータ */ } matchNode; struct /* spgAddNodeの結果 */ { Datum nodeLabel; /* 新しいノードのラベル */ int nodeN; /* 挿入する場所(0からのインデックス) */ } addNode; struct /* spgSplitTupleの結果 */ { /* 子タプルを1つ持つ新しい上位のレベルの内部タプルを生成するための情報 */ bool prefixHasPrefix; /* タプルは接頭辞を持つか */ Datum prefixPrefixDatum; /* そうならば、その値 */ int prefixNNodes; /* ノード数 */ Datum *prefixNodeLabels; /* そのラベル(ラベルがなければNULL) */ int childNodeN; /* どのタプルが子タプルを得るか */ /* 古いノードをすべて持つ新しい低位の内部タプルを生成するための情報 */ bool postfixHasPrefix; /* タプルは接頭辞を持つか */ Datum postfixPrefixDatum; /* そうならば、その値 */ } splitTuple; } result; } spgChooseOut;
datum
はインデックスに挿入できたspgConfigIn
.attType
型の元データです。
leafDatum
はspgConfigOut
.leafType
型の値です。これは最初は、メソッドcompress
が提供されているならdatum
に適用されたメソッドcompress
の結果で、さもなくばdatum
と同じ値です。
leafDatum
は、choose
やpicksplit
メソッドがこれを変更すると、ツリーのより低いレベルで変化することがあります。
挿入の探索がリーフページに到達するとき、leafDatum
の現在値は、新しく作成されるリーフタプルに格納される値です。
level
は、ルートレベルを0として、現在の内部タプルのレベルを示します。
現在の内部タプルが複数の同等なノードを含むとして印を付けられているとき、allTheSame
をtrueにします(64.3.4.3参照)。
現在の内部タプルが接頭辞を含むとき、hasPrefix
をtrueにします。
このとき、prefixDatum
がその値になります。
nNodes
は内部タプルが含む子ノードの数で、nodeLabels
はそれらのラベル値の配列、あるいはラベルがなければNULLになります。
choose
関数は、新しい値が既存の子ノードの1つとマッチするか、新しい子ノードを追加する必要があるか、あるいは新しい値がタプルの接頭辞と適合しないので内部タプルを分割してより制限のない接頭辞を作成する必要があるか、を決定することができます。
新しい値が既存の子ノードの1つにマッチしたときは、resultType
をspgMatchNode
にセットします。
nodeN
はノードの配列中のそのノードの(0からの)番号にセットします。
levelAdd
は、そのノードをたどって下がるときに生じたlevel
の増分にセットします。あるいは演算子クラスがレベルを使っていなければ0のままにします。
restDatum
は、演算子クラスがデータをあるレベルから次のレベルに変更しないのであれば、datum
に等しくセットします。そうでなければ、次のレベルでleafDatum
として使われる修正された値にセットします。
新しい子ノードを追加しなければならないときは、resultType
をspgAddNode
にセットします。
nodeLabel
は、新しいノードで使われるラベルにセットし、nodeN
はノードの配列中の挿入される場所のノードの(0からの)番号にセットします。
ノードを追加した後で、choose
関数を修正された内部タプルを使って再び呼び出しますが、このときは、spgMatchNode
という結果になるはずです。
新しい値がタプルの接頭辞と適合しないときは、resultType
をspgSplitTuple
にセットします。
このアクションは、すべての既存のノードを新しい低位の内部タプルに移動し、新しい低位の内部タプルを指す単一の下向きのリンクを持つ新しいタプルで既存のタプルを置換します。
prefixHasPrefix
は新しい上位のタプルが接頭辞を持つかどうかを示し、持つ場合にはprefixPrefixDatum
をその接頭辞の値にセットします。
インデックスに追加される新しい値を受け入れるため、新しい接頭辞の値は元のものよりも十分に制限の緩いものになっていなければなりません。
prefixNNodes
は新しいタプルで必要なノード数にセットし、prefixNodeLabels
はラベルを保持するためにpallocされた配列に、ノードのラベルが必要でないときはNULLにセットします。
新しい上位のタプルの全サイズは置き換えるタプルの全サイズよりも大きくはないことに注意してください。これは新しい接頭辞と新しいラベルの長さを制約します。
childNodeN
は、新しい低位の内部タプルへ下向きにリンクするノードの(0からの)番号にセットします。
postfixHasPrefix
は、新しい低位のタプルが接頭辞を持つかどうかを示し、持つときにはpostfixPrefixDatum
を接頭辞の値にセットします。
新しい低位に移動したタプルのノードのラベルを変更する機会も、子のインデックスのエントリを変更する機会もありませんから、これら2つの接頭辞と(もしあれば)下向きのリンクのノードのラベルの組み合わせは、元の接頭辞と同じ意味を持つ必要があります。
ノードが分割された後で、置換した内部タプルを使ってchoose
関数を再び呼び出します。
この呼び出しは、spgSplitTuple
アクションにより適切なノードが作られなければ、spgAddNode
という結果になります。
そのうち、choose
がspgMatchNode
を返し、次のレベルに下がる挿入が可能となります。
picksplit
リーフタプルの集合に対し、新しい内部タプルをどうやって作るかを決定します。
この関数のSQL宣言は以下のようになります。
CREATE FUNCTION my_picksplit(internal, internal) RETURNS void ...
1番目の引数はCのspgPickSplitIn
構造体へのポインタで、関数の入力データを含みます。
2番目の引数はCのspgPickSplitOut
構造体へのポインタで、関数が結果のデータを入れます。
typedef struct spgPickSplitIn { int nTuples; /* リーフタプルの数 */ Datum *datums; /* そのデータ(長さnTuplesの配列) */ int level; /* (0から数えた)現在のレベル */ } spgPickSplitIn; typedef struct spgPickSplitOut { bool hasPrefix; /* 新しい内部タプルは接頭辞を持つか */ Datum prefixDatum; /* もしそうなら、その値 */ int nNodes; /* 新しい内部タプルのノード数 */ Datum *nodeLabels; /* そのラベル(ラベルがなければNULL) */ int *mapTuplesToNodes; /* 各リーフタプルへのノードのインデックス */ Datum *leafTupleDatums; /* 新しい各リーフタプルに保存されているデータ */ } spgPickSplitOut;
nTuples
は与えられるリーフタプルの個数です。
datums
はspgConfigOut
.leafType
型のそれらのデータ値の配列です。
level
はすべてのリーフタプルが共有する現在のレベルで、これが新しい内部タプルのレベルになります。
hasPrefix
は新しい内部タプルが接頭辞を持つかどうかを示し、持つ場合はprefixDatum
を接頭辞の値にセットします。
nNodes
は新しい内部タプルが含むノードの数を示し、nodeLabels
はそのラベル値の配列に、ノードのラベルが必要でないときはNULLにセットします。
mapTuplesToNodes
は、それぞれのリーフタプルが割り当てられるノードの(0からの)番号の配列にセットします。
leafTupleDatums
は新しいリーフタプルに格納される値の配列にセットします(演算子クラスがデータをあるレベルから次のレベルに変更しなければこれらは入力のdatums
と同じになります)。
picksplit
関数は、nodeLabels
、mapTuplesToNodes
、leafTupleDatums
の配列についてpallocしなければならないことに注意してください。
2つ以上のリーフタプルを与えた場合、picksplit
関数はそれらを2つ以上のノードに分類すると予想されます。そうでなければ、リーフタプルを複数のページにまたがって分割するという、この操作の究極の目的を実現できないからです。
従って、picksplit
がすべてのリーフタプルを同じノードに置くことになった場合には、SP-GiSTのコアのコードがその決定を覆して内部タプルを生成し、その中の複数の同一のラベルが付けられたノードに、リーフタプルが無作為に割り当てられます。
そのようなタプルは、このことが発生したことを明示するため、allTheSame
と印がつけられます。
choose
関数とinner_consistent
関数は、これらの内部タプルについて、適切な注意をして取り扱わなければなりません。
詳細な情報は64.3.4.3を参照してください。
config
関数がlongValuesOK
をtrueにセットし、1ページよりも大きな入力値を与える場合にのみ、picksplit
を1つだけのリーフタプルに適用できます。
この場合の操作の重要な点は、接頭辞をはがして、新しい、より短いリーフデータの値を生成することです。
この呼出は、1ページに収まる短さのリーフデータが生成されるまで繰り返されます。
詳細な情報は64.3.4.1を参照してください。
inner_consistent
ツリーの探索でたどるべきノード(枝)の集合を返します。
この関数のSQL宣言は以下のようになります。
CREATE FUNCTION my_inner_consistent(internal, internal) RETURNS void ...
1番目の引数はCのspgInnerConsistentIn
構造体へのポインタで、関数の入力データを含みます。
2番目の引数はCのspgInnerConsistentOut
構造体へのポインタで、関数が結果のデータを入れます。
typedef struct spgInnerConsistentIn { ScanKey scankeys; /* 演算子と比較する値の配列 */ ScanKey orderbys; /* 順序付け演算子と比較する値の配列 */ int nkeys; /* scankeys配列の長さ */ int norderbys; /* orderbys配列の長さ */ Datum reconstructedValue; /* 親で再構築された値 */ void *traversalValue; /* 演算子クラスに固有の探索値 */ MemoryContext traversalMemoryContext; /* 新しい探索値をここに入れる */ int level; /* (0から数えた)現在のレベル */ bool returnData; /* 元のデータを返さなければならないか */ /* 現在の内部タプルからのデータ */ bool allTheSame; /* タプルはall-the-sameと印が付けられているか? */ bool hasPrefix; /* タプルは接頭辞を持つか? */ Datum prefixDatum; /* もしそうなら、接頭辞の値 */ int nNodes; /* 内部タプルの中のノード数 */ Datum *nodeLabels; /* ノードのラベルの値(なければNULL) */ } spgInnerConsistentIn; typedef struct spgInnerConsistentOut { int nNodes; /* 訪れるべき子ノードの数 */ int *nodeNumbers; /* ノードの配列でのそのインデックス */ int *levelAdds; /* この分だけそれぞれレベルを挙げる */ Datum *reconstructedValues; /* 関連する再構築された値 */ void **traversalValues; /* 演算子クラスに固有の探索値 */ double **distances; /* 関連する距離 */ } spgInnerConsistentOut;
配列scankeys
は長さがnkeys
で、インデックス検索の条件を記述します。
複数の条件はANDで結合されます。つまり、条件のすべてを満たすインデックスエントリのみが対象となります。
(nkeys
= 0 は全インデックスエントリが問い合わせを満たす意味になる、ということに注意してください。)
通常、consistent関数では、配列のそれぞれのエントリのsk_strategy
およびsk_argument
フィールドのみが問題となります。これらのフィールドにはそれぞれインデックス付け可能な演算子と比較値が入ります。
とりわけ、比較値がNULLかどうかを確認するためにsk_flags
を検査する必要はありません。なぜならSP-GiSTのコアのコードがそのような条件を除外するからです。
配列orderbys
は長さがnorderbys
で、(もしあれば)順序付け演算子を同じように記述します。
reconstructedValue
は親タプルのために再構築された値で、ルートレベルの場合、あるいは親レベルのinner_consistent
関数が値を返さなかった場合は(Datum) 0
となります。
traversalValue
は親インデックスのタプルのinner_consistent
の前の呼び出しから渡された探索データへのポインタで、ルートレベルならNULLです。
traversalMemoryContext
は出力探索値が格納されるメモリコンテキストです(以下を参照)。
level
は現在の内部タプルのレベルを、ルートレベルを0として数えたものです。
returnData
は、この問い合わせで再構築されたデータが必要な場合にtrue
となりますが、これはconfig
関数がcanReturnData
であると主張した場合にのみ、そうなります。
現在の内部タプルが「all-the-same」と印付けされているなら、allTheSame
は真になります。この場合、(もしあるなら)全てのノードが同じラベルを持ち、問い合わせに全てが一致するか、全く一致しないかのいずれかになります(64.3.4.3を参照)。
現在の内部タプルがプレフィックスを含んでいるならhasPrefix
は真になります。その場合、prefixDatum
がその値です。
nNodes
は内部タプルに含まれる子ノードの数で、nodeLabels
はそれらのラベル値の配列、あるいは、ノードがラベルを持たないならNULLです。
nNodes
は探索で訪れる必要のある子ノードの数にセットされなければなりません。また、nodeNumbers
はそれらの番号の配列にセットされなければなりません。
演算子クラスがレベルを監視しているときは、それぞれのノードへと下って訪れるときに必要なレベルの増分の配列をlevelAdds
にセットします。
(この増分はすべてのノードについて同じになることも多いですが、必ずしもそうなるとは限らないので配列が使われます。)
値の再構築が必要なときには、訪れるそれぞれの子ノードについて再構築された値の配列をreconstructedValues
にセットします。再構築が必要でなければ、reconstructedValues
をNULLのままにします。
再構築された値はspgConfigOut
.leafType
型と仮定されます。
(しかしながら、コアシステムはそれらに対してコピー以外のことをしませんので、leafType
と同じtyplen
とtypbyval
属性を持っていれば十分です。)
順序付け検索を実行するなら、orderbys
配列に従ってdistances
に距離の値の配列を設定します(距離の最も近いノードが最初に処理されます)。
そうでなければNULLのままにします。
追加の外部情報(「探索値」)をツリー探索の下位レベルに渡したい場合は、traversalValues
を適切な探索値、訪れるそれぞれの子ノードについて1つの配列にセットします。
それ以外の場合はtraversalValues
をNULLのままにします。
inner_consistent
関数は、現在のメモリコンテキスト内のnodeNumbers
、levelAdds
、distances
、reconstructedValues
、traversalValues
の配列についてpallocしなければならないことに注意してください。
ただし、traversalValues
配列が指すすべての出力探索値はtraversalMemoryContext
内に割り当てられます。
それぞれの探索値は1つのpallocされた塊でなければなりません。
leaf_consistent
リーフタプルが問い合わせを満たす場合、trueを返します。
この関数のSQL宣言は以下のようになります。
CREATE FUNCTION my_leaf_consistent(internal, internal) RETURNS bool ...
1番目の引数はCのspgLeafConsistentIn
構造体へのポインタで、関数の入力データを含みます。
2番目の引数はCのspgLeafConsistentOut
構造体へのポインタで、関数が結果のデータを入れます。
typedef struct spgLeafConsistentIn { ScanKey scankeys; /* 演算子と比較する値の配列 */ ScanKey orderbys; /* 順序付け演算子と比較する値の配列 */ int nkeys; /* scankeys配列の長さ */ int norderbys; /* orderbys配列の長さ */ Datum reconstructedValue; /* 親で再構築された値 */ void *traversalValue; /* 演算子クラスに固有の探索値 */ int level; /* (0から数えた)現在のレベル */ bool returnData; /* 元のデータを返さなければならないか */ Datum leafDatum; /* リーフタプルのデータ */ } spgLeafConsistentIn; typedef struct spgLeafConsistentOut { Datum leafValue; /* もしあれば、再構築された元のデータ */ bool recheck; /* 演算子を再チェックする必要があればtrue */ bool recheckDistances; /* 距離を再チェックする必要があればtrue */ double *distances; /* 関連する距離 */ } spgLeafConsistentOut;
配列scankeys
は長さがnkeys
で、インデックス探索の条件を記述します。
複数の条件はANDで結合されます。つまり、条件のすべてを満たすインデックスエントリのみが対象となります。
(nkeys
が0ならば、すべてのエントリが検索条件を満たすことになる、ということに注意してください。)
通常、consistent関数では、配列のそれぞれのエントリのsk_strategy
およびsk_argument
フィールドのみが問題となります。これらのフィールドにはそれぞれインデックス付け可能な演算子と比較値が入ります。
なお、比較値がNULLかどうかを確認するためにsk_flags
を検査する必要はありません。なぜならSP-GiSTのコアのコードがそのような条件を除外するからです。
配列orderbys
は長さがnorderbys
で、順序付け演算子を同じように記述します。
reconstructedValue
は親タプルのために再構築された値で、ルートレベルの場合、あるいは親レベルのinner_consistent
関数が値を返さなかった場合は(Datum) 0
となります。
traversalValue
は親インデックスのタプルのinner_consistent
の前の呼び出しから渡された探索データへのポインタで、ルートレベルならNULLです。
level
は現在のリーフタプルのレベルを、ルートレベルを0として数えたものです。
returnData
は、この問い合わせで再構築されたデータが必要な場合にtrue
となりますが、これはconfig
関数がcanReturnData
を確認した場合にのみ、そうなります。
leafDatum
は現在のリーフタプルに格納されているspgConfigOut
.leafType
のキーの値です。
この関数は、リーフタプルが問い合わせにマッチすればtrue
を返し、マッチしなければfalse
を返します。
true
の場合、returnData
がtrue
であれば、leafValue
を、このリーフタプルにインデックス付けするために元々提供された(spgConfigIn
.attType
型の)値に設定しなければなりません。
また、マッチするかどうかが不確実で、マッチするかの確認のために実際のヒープタプルに演算子を再適用しなければならないときは、recheck
がtrue
にセットされることがあります。
順序付け検索を実行するなら、orderbys
配列に従ってdistances
に距離の値の配列を設定します。
そうでなければNULLのままにします。
返される距離の少なくとも1つが正確でないのなら、recheckDistances
にtrueを設定します。
この場合、エグゼキュータはヒープからタプルを取得した後正確な距離を計算し、必要ならタプルを並べ替えます。
オプションのユーザ定義メソッドは以下です。
Datum compress(Datum in)
インデックスのリーフタプルでデータ項目を物理ストレージに適した形式に変換します。
spgConfigIn
.attType
型の値を受け付け、spgConfigOut
.leafType
型の値を返します。
出力値は行に収まらないTOASTを含んでいてはいけません。
注意: compress
メソッドは格納される値にのみ適用されます。
適合するメソッドはcompress
を使って変換することなく、問い合わせのscankeys
をそのまま受け取ります。
options
演算子クラスの振舞いを制御するユーザに可視のパラメータの集合を定義します。
この関数のSQL宣言は以下のようになります。
CREATE OR REPLACE FUNCTION my_options(internal) RETURNS void AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
関数にはlocal_relopts
構造体へのポインタが渡されますが、構造体を演算子クラスに固有のオプションの集合で満たすことが必要です。
オプションはマクロPG_HAS_OPCLASS_OPTIONS()
とPG_GET_OPCLASS_OPTIONS()
を使って他のサポート関数からアクセスできます。
SP-GiSTでのキーの表現には柔軟性がありますので、ユーザに固有のパラメータに依存するかもしれません。
SP-GiSTのすべてのサポートメソッドは、通常は短期間有効なメモリコンテキスト内で呼び出されます。つまり、それぞれのタプルについて処理した後でCurrentMemoryContext
はリセットされます。
したがって、pallocしたものすべてについてpfreeすることを気にかけることはあまり重要ではありません。
(config
メソッドは例外で、メモリリークを避けるようにする必要があります。
しかし、通常はconfig
メソッドは、パラメータとして渡された構造体に定数を代入する以外、何もする必要がありません。)
インデックス付けされた列が照合可能なデータ型の場合、インデックスの照合は、標準的なPG_GET_COLLATION()
の仕組みを使ってすべてのサポートメソッドに渡されます。
この節では、SP-GiSTの演算子クラスを実装する人にとって知っていると役に立つ、実装についての詳細とその他の秘訣について説明します。
それぞれのリーフタプルおよび内部タプルは1つのインデックスページ内(デフォルトで8kB)に収まらなければなりません。
従って、可変長のデータ型の値をインデックス付けするときは、長い値は基数木のようなメソッドによってのみサポートされます。つまり、ツリーのそれぞれのレベルではページに収まる短さの接頭辞を含み、最後のリーフレベルでは、やはりページに収まる短さの接尾辞を含む、というようなものです。
このようなことが発生する場合の対応の準備ができている場合のみ、演算子クラスはlongValuesOK
を真にセットするべきです。
そうでなければ、SP-GiSTのコアは、インデックスページに収めるには大きすぎる値についてのインデックス付け要求を拒絶します。
同様に、内部タプルが大きくなりすぎてインデックスページに収まらない、ということにならないようにするのは、演算子クラスの責任です。 これにより、1つの内部タプルで使うことができる子ノードの数、および接頭辞の値の最大サイズが制限されます。
内部タプルのノードがリーフタプルの集合を指しているとき、それらのタプルはすべて同じインデックスページ内になければならない、という制限もあります。
(これは、シークの回数を減らし、そのようなタプルを一つにつなげるリンクに必要なスペースを減らす、という設計上の決定によるものです。)
リーフタプルの集合が大きくなって1ページに収まらなくなると、分割が実行され、中間の内部タプルが挿入されます。
これで問題を解決するためには、新しい内部タプルは、リーフの値の集合を2つ以上のノードのグループに分割しなければなりません。
演算子クラスのpicksplit
関数がそれをするのに失敗したときは、SP-GiSTのコアは、64.3.4.3に記述されている特別な手段に頼ることになります。
longValuesOK
が真であれば、SP-GiSTツリーの後続のレベルは、より多くの情報を接頭辞と内部タプルのノードラベルへと吸収し、要求されるリーフデータより小さくして、最終的には1ページに収まるようになることが期待されます。
演算子クラスのバグが無限の挿入ループを引き起こすのを防ぐために、choose
メソッドの呼び出しの10サイクル以内でリーフデータが少しも小さくならなければ、SP-GiSTコアはエラーを発生します。
木構造のアルゴリズムには、それぞれの内部タプルに対して固定された集合のノードを使うものがあります。
例えば四分木では、内部タプルの中心点の周りの4つの象限に対応するちょうど4つのノードが必ずあります。
このような場合、コードは典型的には数字を使ったノードで動作し、明示的なノードラベルは必要ありません。
ノードラベルを使わない(そしてそれによりいくらかのスペースを節約する)ために、picksplit
関数はnodeLabels
配列としてNULLを返すことができ、同様にchoose
関数はspgSplitTuple
アクションの間prefixNodeLabels
配列としてNULLを返すことができます。
この結果、その後のchoose
関数およびinner_consistent
関数の呼び出しにおいてもnodeLabels
はNULLになります。
原則として、ノードラベルは同じインデックス中の一部の内部タプルに使い、他の内部タプルには省略する、ということができます。
ラベルのないノードを持つ内部タプルを処理するときに、choose
がspgAddNode
を返すのはエラーです。というのは、この場合、ノードの集合は固定されていると想定されるからです。
picksplit
が入力のリーフ値を少なくとも2つのノード分類に分割できなかった場合、SP-GiSTのコアは演算子クラスのpicksplit
関数の結果を無効にすることがあります。
これが起きると、複数のノードを持つ新しい内部タプルが作成されます。それぞれのノードは、picksplit
が一つのノードに付与したもの(あれば)と同じラベルを持ち、リーフ値はこれらの等価なノード間でランダムに分割されます。
内部タプルにはallTheSame
のフラグがセットされ、choose
関数およびinner_consistent
関数に対し、そのタプルが通常期待されるようなノードの集合を持っていないことを警告します。
allTheSame
の処理において、choose
のspgMatchNode
という結果は、新しい値は等価なノードのどれに割り当てられても良い、という意味に解釈されます。
コアのコードは入力されたnodeN
の値を無視し、(ツリーの平衡を保つために)ノードの1つにランダムに降りていきます。
choose
がspgAddNode
を返すのはエラーです。というのは、そうするとすべてのノードが等価ではなくなるからです。
挿入する値が既存のノードとマッチしない時は、spgSplitTuple
のアクションを使わなければなりません。
allTheSame
のタプルの処理において、すべてのノードは等価なので、inner_consistent
関数は、インデックス検索を続けるためのターゲットとして、すべてのノードを返すか、ノードを1つも返さないかのいずれかであるべきです。
このために、特殊ケースを扱うコードが必要になるかもしれませんし、必要ないかもしれません。それは、inner_consistent
関数が、通常、ノードの意味についてどの程度のことを仮定しているかに依存します。