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関数が、通常、ノードの意味についてどの程度のことを仮定しているかに依存します。