SP-GiSTは高度に抽象化されたインタフェースを提供します。アクセスメソッドの開発者は特定のデータ型専用のメソッドだけを開発する必要があります。 SP-GiSTのコアは効率的なディスクマッピングと木構造の探索を担当します。 また、同時実行制御とログ出力も担当します。
SP-GiSTのツリーのリーフタプルは、インデックスの付けられた列と同じデータ型の値を含んでいます。 ルートレベルにあるリーフタプルは、必ずインデックスが付けられた元のデータの値を含んでいますが、より下のレベルのリーフタプルは、接尾辞など、圧縮された表現しか含んでいないかも知れません。 この場合、演算子クラスのサポート関数が、内部タプルをリーフレベルまでたどりながら集める情報を使って元の値を再構築できる必要があります。
内部タプルは、探索木の分岐点となるため、もっと複雑です。 それぞれの内部タプルは1つ以上のノードの集合を含んでおり、ノードは類似のリーフ値のグループを表現します。 ノードは下向きのリンクを含んでおり、これは下のレベルの別の内部タプルを指すか、あるいはすべて同じインデックスページ上に載っているリーフタプルの短いリストを指しています。 それぞれのノードは、通常、それを記述するラベルを持っています。 例えば、基数木では、ノードのラベルは文字列の値の次の文字にすることができます。 (あるいは、すべての内部タプルについて、決まったノードの集合しか扱わないのであれば、演算子クラスはノードのラベルを省略することができます。 65.4.2を参照してください。) 省略可能ですが、内部タプルはそのすべてのメンバーを記述する接頭辞の値を持つことができます。 基数木では、これは表現される文字列に共通の接頭辞とすることができます。 接頭辞の値は、必ずしも本当の接頭辞である必要はなく、演算子クラスが必要とする任意の値で良いです。 例えば四分木では、その中心点を保持し、4つの象限をそこから相対的に測るようにできます。 そうすると、四分木の内部タプルはこの中心点の周りの象限に対応する4つのノードも含むことになるでしょう。
木構造のアルゴリズムには、現在のタプルのレベル(深さ)を知っていることが必要なものがあります。そこで、SP-GiSTのコアは、演算子クラスが木構造をたどって下がるときにレベル数の管理を可能にしています。 また、必要であれば、表現される値を加算的に再構築すること、また木構造を下る間に追加データ(探索値と呼ばれます)を渡すこともサポートしています。
SP-GiSTのコアのコードはnullエントリについても対応しています。 SP-GiSTのインデックスはインデックス列がnullのエントリについても格納しますが、これはインデックスの演算子クラスのコードからは隠されているので、nullのインデックスエントリや検索条件が演算子クラスのメソッドに渡されることはありません。 (SP-GiSTの演算子は厳格なのでNULL値について成功を返すことはできないと想定されています。) 従って、ここではこれ以上、NULLについて議論しません。
SP-GiSTのインデックス演算子クラスが提供しなければならないユーザ定義メソッドは5つあり、加えて、オプションのメソッドが1つあります。
5つの必須メソッド全ては2つのinternal引数を受け付けるというしきたりに従い、1つ目はサポートメソッドへの入力値を含むC構造体へのポインタで、一方2つ目は出力値が配置されるであろうC構造体へのポインタです。
4つの必須メソッドでは、その結果がすべて出力構造体の中にあるので、単にvoidを返します。leaf_consistentは、さらにbooleanの結果を返します。
メソッドは、その入力構造体のどのフィールドも変更してはいけません。
どんな場合でも、出力構造体はユーザ定義メソッドを呼び出す前にゼロに初期化されます。
オプションの6番目のメソッドcompressは、唯一の引数としてインデックス付けされるデータを受け付け、リーフタプルの物理格納に適した値を返します。
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にします(65.4.1参照)。
leafTypeは一般的にはattTypeと同じです。
後方互換性のため、configメソッドはleafTypeを初期化しないままにすることができます。
このことはleafTypeをattTypeと同一に設定するのと同じ効果をもたらすでしょう。
attTypeとleafTypeが異なるときには、オプションメソッドcompressが提供されなければなりません。
メソッドcompressはattTypeからleafTypeへのインデックス付けされたデータの変換に責任を持ちます。
注意: 両consistent関数は、compressを使った変換を伴わない、変更されていないscankeysを取得します。
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にします(65.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関数は、これらの内部タプルについて、適切な注意をして取り扱わなければなりません。
詳細な情報は65.4.3を参照してください。
config関数がlongValuesOKをtrueにセットし、1ページよりも大きな入力値を与える場合にのみ、picksplitを1つだけのリーフタプルに適用できます。
この場合の操作の重要な点は、接頭辞をはがして、新しい、より短いリーフデータの値を生成することです。
この呼出は、1ページに収まる短さのリーフデータが生成されるまで繰り返されます。
詳細な情報は65.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; /* 演算子と比較する値の配列 */
int nkeys; /* 配列の長さ */
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; /* 演算子クラスに固有の探索値 */
} spgInnerConsistentOut;
配列scankeysは長さがnkeysで、インデックス検索の条件を記述します。
複数の条件はANDで結合されます。つまり、条件のすべてを満たすインデックスエントリのみが対象となります。
(nkeys = 0 は全インデックスエントリが問い合わせを満たす意味になる、ということに注意してください。)
通常、consistent関数では、配列のそれぞれのエントリのsk_strategyおよびsk_argumentフィールドのみが問題となります。これらのフィールドにはそれぞれインデックス付け可能な演算子と比較値が入ります。
とりわけ、比較値がNULLかどうかを確認するためにsk_flagsを検査する必要はありません。なぜならSP-GiSTのコアのコードがそのような条件を除外するからです。
reconstructedValueは親タプルのために再構築された値で、ルートレベルの場合、あるいは親レベルのinner_consistent関数が値を返さなかった場合は(Datum) 0となります。
reconstructedValueは常にspgConfigOut.leafType型です。
traversalValueは親インデックスのタプルのinner_consistentの前の呼び出しから渡された探索データへのポインタで、ルートレベルならNULLです。
traversalMemoryContextは出力探索値が格納されるメモリコンテキストです(以下を参照)。
levelは現在の内部タプルのレベルを、ルートレベルを0として数えたものです。
returnDataは、この問い合わせで再構築されたデータが必要な場合にtrueとなりますが、これはconfig関数がcanReturnDataであると主張した場合にのみ、そうなります。
現在の内部タプルが「all-the-same」と印付けされているなら、allTheSameは真になります。この場合、(もしあるなら)全てのノードが同じラベルを持ち、問い合わせに全てが一致するか、全く一致しないかのいずれかになります(65.4.3を参照)。
現在の内部タプルがプレフィックスを含んでいるならhasPrefixは真になります。その場合、prefixDatumがその値です。
nNodesは内部タプルに含まれる子ノードの数で、nodeLabelsはそれらのラベル値の配列、あるいは、ノードがラベルを持たないならNULLです。
nNodesは探索で訪れる必要のある子ノードの数にセットされなければなりません。また、nodeNumbersはそれらの番号の配列にセットされなければなりません。
演算子クラスがレベルを監視しているときは、それぞれのノードへと下って訪れるときに必要なレベルの増分の配列をlevelAddsにセットします。
(この増分はすべてのノードについて同じになることも多いですが、必ずしもそうなるとは限らないので配列が使われます。)
値の再構築が必要なときには、訪れるそれぞれの子ノードについて再構築された値の配列をreconstructedValuesにセットします。再構築が必要でなければ、reconstructedValuesをNULLのままにします。
追加の外部情報(「探索値」)をツリー探索の下位レベルに渡したい場合は、traversalValuesを適切な探索値、訪れるそれぞれの子ノードについて1つの配列にセットします。
それ以外の場合はtraversalValuesをNULLのままにします。
inner_consistent関数は、現在のメモリコンテキスト内のnodeNumbers、levelAdds、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; /* 演算子と比較する値の配列 */
int nkeys; /* 配列の長さ */
Datum reconstructedValue; /* 親で再構築された値 */
void *traversalValue; /* 演算子クラスに固有の探索値 */
int level; /* (0から数えた)現在のレベル */
bool returnData; /* 元のデータを返さなければならないか */
Datum leafDatum; /* リーフタプルのデータ */
} spgLeafConsistentIn;
typedef struct spgLeafConsistentOut
{
Datum leafValue; /* もしあれば、再構築された元のデータ */
bool recheck; /* 演算子を再チェックする必要があればtrue */
} spgLeafConsistentOut;
配列scankeysは長さがnkeysで、インデックス探索の条件を記述します。
複数の条件はANDで結合されます。つまり、条件のすべてを満たすインデックスエントリのみが対象となります。
(nkeysが0ならば、すべてのエントリが検索条件を満たすことになる、ということに注意してください。)
通常、consistent関数では、配列のそれぞれのエントリのsk_strategyおよびsk_argumentフィールドのみが問題となります。これらのフィールドにはそれぞれインデックス付け可能な演算子と比較値が入ります。
なお、比較値がNULLかどうかを確認するためにsk_flagsを検査する必要はありません。なぜならSP-GiSTのコアのコードがそのような条件を除外するからです。
reconstructedValueは親タプルのために再構築された値で、ルートレベルの場合、あるいは親レベルのinner_consistent関数が値を返さなかった場合は(Datum) 0となります。
reconstructedValueは常にspgConfigOut.leafType型です。
traversalValueは親インデックスのタプルのinner_consistentの前の呼び出しから渡された探索データへのポインタで、ルートレベルならNULLです。
levelは現在のリーフタプルのレベルを、ルートレベルを0として数えたものです。
returnDataは、この問い合わせで再構築されたデータが必要な場合にtrueとなりますが、これはconfig関数がcanReturnDataを確認した場合にのみ、そうなります。
leafDatumは現在のリーフタプルに格納されている鍵の値です。
この関数は、リーフタプルが問い合わせにマッチすればtrueを返し、マッチしなければfalseを返します。
trueの場合、returnDataがtrueであれば、leafValueは、このリーフタプルにインデックス付けするために元々提供されたspgConfigIn.attType型の値にセットされなければなりません。
また、マッチするかどうかが不確実で、マッチするかの確認のために実際のヒープタプルに演算子を再適用しなければならないときは、recheckがtrueにセットされることがあります。
オプションのユーザ定義メソッドは以下です。
Datum compress(Datum in)
インデックページのリーフタプルでデータ項目を物理ストレージに適した形式に変換します。
spgConfigIn.attTypeの値を受け付け、spgConfigOut.leafTypeの値を返します。
出力値はTOASTされていないべきです。
SP-GiSTのすべてのサポートメソッドは、通常は短期間有効なメモリコンテキスト内で呼び出されます。つまり、それぞれのタプルについて処理した後でCurrentMemoryContextはリセットされます。
したがって、pallocしたものすべてについてpfreeすることを気にかけることはあまり重要ではありません。
(configメソッドは例外で、メモリリークを避けるようにする必要があります。
しかし、通常はconfigメソッドは、パラメータとして渡された構造体に定数を代入する以外、何もする必要がありません。)
インデックス付けされた列が照合可能なデータ型の場合、インデックスの照合は、標準的なPG_GET_COLLATION()の仕組みを使ってすべてのサポートメソッドに渡されます。