SP-GiST 是 space-partitioned GiST 的缩写。SP-GiST 支持分区搜索树,这使得开发多种不同的 非平衡数据结构成为可能,例如四叉树、k-d 树以及基数树(trie)。这些结构 的共同特征是,它们会反复将搜索空间划分为不必等大的分区。与这种划分规则良 好匹配的搜索可以非常快。
这些常见数据结构最初是为内存中使用而开发的。在主存中,它们通常被设计成一 组由指针链接的动态分配结点。由于这些指针链可能相当长,直接存储到磁盘上并 不合适,因为那会需要过多的磁盘访问。相比之下,基于磁盘的数据结构应当具有 较高的扇出,以尽量减少 I/O。SP-GiST 要解决的难题是, 如何以这样的方式将搜索树结点映射到磁盘页:即使搜索遍历了许多结点,也只需 访问少数几个磁盘页。
像 GiST 一样,SP-GiST 的目标是让 数据类型领域专家而非数据库专家,能够针对自定义数据类型开发合适的访问方法。
这里的一些信息来自普渡大学的 SP-GiST 索引项目 网站。 SP-GiST 在 PostgreSQL 中的实现主要由 Teodor Sigaev 和 Oleg Bartunov 维护,他们的 网站 上还有更多信息。
核心 PostgreSQL 发行版包含了 SP-GiST 操作符类,如 Table 401 所示。
Table 401. 内置 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 的两个操作符类, 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 操作符类无关,因此此处不再展开讨论。
内部元组更为复杂,因为它们是搜索树中的分支点。每个内部元组都包含一个或 多个结点,代表相似叶子值的分组。一个结点包含一 个向下链接,它要么指向更低层级的另一个内部元组,要么指向一小组位于 同一索引页上的叶子元组。每个结点通常都有一个描述它的标签; 例如在基数树中,结点标签可以是字符串值的下一个字符。(或者,如果某个 操作符类对所有内部元组都使用固定的一组结点,也可以省略结点标签;参见 Section 4.4.2。)内部元组还可以选择带有一个描 述其所有成员的前缀值。在基数树中,这可以是所 表示字符串的公共前缀。前缀值不一定真的是前缀,它也可以是操作符类需要的 任何数据;例如在四叉树中,它可以存储划分四个象限所依据的中心点。这样, 四叉树的内部元组还会包含四个结点,对应于该中心点周围的四个象限。
某些树算法需要知道当前元组所在的层级(或深度),因此 SP-GiST 核心允许操作符类在沿树向下遍历时管理层级计 数。它还支持在需要时增量重建所表示的值,并支持在树下降过程中向下传递额 外数据(称为遍历值)。
SP-GiST 核心代码会处理空项。虽然 SP-GiST 索引确实会为被索引列中的空值存储项,但这一 点对索引操作符类代码是隐藏的:不会有空的索引项或搜索条件传递给操作符类 方法。(这里假定 SP-GiST 操作符是严格的,因此对空 值不可能成功。)因此,这里不再进一步讨论空值。
SP-GiST 的索引操作符类必须提供五个用户定义方法,另 外还有两个可选方法。五个必需方法都遵循这样的约定:接受两个 internal 参数,第一个参数是指向某个 C 结构的指针,其中包 含该支持方法的输入值;第二个参数也是指向某个 C 结构的指针,方法必须将输 出值写入其中。四个必需方法只返回 void,因为它们的全部结果 都体现在输出结构中;但 leaf_consistent 会返回一个 boolean 结果。这些方法不得修改其输入结构中的任何字段。在 所有情况下,调用用户定义方法之前,输出结构都会先被清零。可选的第六个方 法 compress 只接受一个待索引的 datum 参数,并返回适合在叶子元组中物理存储的值。可选的第七个方法 options 接受一个指向 C 结构的 internal 指针,应将操作符类特定的参数填入其中,并返回 void。
五个必需的用户定义方法是:
config返回索引实现的静态信息,包括前缀和结点标签数据类型的 OID。
该函数的 SQL 声明必须如下所示:
CREATE FUNCTION my_config(internal, internal) RETURNS void ...
第一个参数是一个指向 spgConfigIn C 结构的 指针,其中包含该函数的输入数据。第二个参数是一个指向 spgConfigOut C 结构的指针,函数必须将结果 数据填入其中。
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 设为真。只有在 attType 是变长类型,并且该操作符类能够通 过反复取后缀来切分长值时,才应将 longValuesOK 设为真(参见 Section 4.4.1)。
leafType 应与该操作符类在目录项 opckeytype 中定义的索引存储类型相匹配。 (注意,opckeytype 可以为零,这表示存储 类型与操作符类的输入类型相同,这也是最常见的情况。)出于向后兼容的原 因,config 方法也可以将 leafType 设为其他值,并且该值将被使用; 但这已被废弃,因为这样会导致目录中对索引内容的标识不正确。此外,也允 许不初始化 leafType(即保持为零);这会 被解释为使用从 opckeytype 派生的索引存 储类型。
当 attType 与 leafType 不同时,必须提供可选方法 compress。compress 方法 负责将待索引的 datum 从 attType 转换为 leafType。
choose为向内部元组插入新值选择一种方法。
该函数的 SQL 声明必须如下所示:
CREATE FUNCTION my_choose(internal, internal) RETURNS void ...
第一个参数是一个指向 spgChooseIn C 结构的 指针,其中包含该函数的输入数据。第二个参数是一个指向 spgChooseOut C 结构的指针,函数必须将结果 数据填入其中。
typedef struct spgChooseIn
{
Datum datum; /* 要被索引的原始 datum */
Datum leafDatum; /* 当前要存储在叶子中的 datum */
int level; /* 当前层级(从零开始计) */
/* 来自当前内部元组的数据 */
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; /* 新的叶子 datum */
} matchNode;
struct /* spgAddNode 的结果 */
{
Datum nodeLabel; /* 新结点的标签 */
int nodeN; /* 在哪里插入它(索引从 0 开始) */
} addNode;
struct /* spgSplitTuple 的结果 */
{
/* 构造只有一个子元组的新上层内部元组所需的信息 */
bool prefixHasPrefix; /* 元组应有前缀? */
Datum prefixPrefixDatum; /* 如果有,前缀值 */
int prefixNNodes; /* 结点数 */
Datum *prefixNodeLabels; /* 它们的标签(或为 NULL 表示
* 没有标签) */
int childNodeN; /* 哪个结点接收子元组 */
/* 构造包含所有旧结点的新下层内部元组所需的信息 */
bool postfixHasPrefix; /* 元组应有前缀? */
Datum postfixPrefixDatum; /* 如果有,前缀值 */
} splitTuple;
} result;
} spgChooseOut;
datum 是将要插入索引的、类型为 spgConfigIn.attType 的原始 datum。leafDatum 是一个类型为 spgConfigOut.leafType 的值;如果提供了 compress 方法,它最初是将 datum 交给 compress 的结果,否则与 datum 相同。如果 choose 或 picksplit 方法修改了它,那么在树的更低层级中 leafDatum 可能会发生变化。当插入搜索到达 叶子页时,leafDatum 的当前值将存储在新创 建的叶子元组中。level 是当前内部元组的层 级,根层为零。若当前内部元组被标记为包含多个等价结点(参见 Section 4.4.3),则 allTheSame 为真。若当前内部元组包含前缀, 则 hasPrefix 为真;若是如此, prefixDatum 就是该前缀值。 nNodes 是内部元组中包含的子结点数量, nodeLabels 是它们的标签值数组;如果没有 标签,则为 NULL。
choose 函数可以判定:新值要么匹配某个现有子结 点,要么必须添加一个新子结点,要么与该元组的前缀不一致,因此必须拆分 该内部元组以创建限制性更弱的前缀。
如果新值匹配某个现有子结点,则将 resultType 设为 spgMatchNode。将 nodeN 设为该结点在结点数组中的索引(从零开始)。将 levelAdd 设为通过该结点向下下降所导致的 level 增量;如果操作符类不使用层级,则保 持为零。若操作符类不会在层级之间修改 datum,则将 restDatum 设为与 leafDatum 相等;否则,将它设为下一层要用 作 leafDatum 的修改后值。
如果必须添加一个新子结点,则将 resultType 设为 spgAddNode。将 nodeLabel 设为新结点要使用的标签,并将 nodeN 设为 该结点应插入到结点数组中的位置索引(从零开始)。添加该结点后, choose 函数会使用修改后的内部元组再次被调用; 这次调用应返回 spgMatchNode。
如果新值与该元组的前缀不一致,则将 resultType 设为 spgSplitTuple。这个动作会把所有现有结点移动到一 个新的较低层内部元组中,并用一个仅含单个向下链接、指向该新下层内部元 组的元组替换现有内部元组。将 prefixHasPrefix 设为指示新的上层元组是 否应有前缀;若应有,则将 prefixPrefixDatum 设为该前缀值。这个新前 缀值必须比原来的限制性更弱,以便能够接受将要索引的新值。将 prefixNNodes 设为新元组所需的结点数,并 将 prefixNodeLabels 设为一个通过 palloc 分配的数组,其中保存这些结点的标签;如果不需要结点标签,则设为 NULL。注意,新上层元组的总大小不得超过它所替换的元组的总大小;这限 制了新前缀和新标签的长度。将 childNodeN 设为将向下链接到新的较低层内部元 组的那个结点的索引(从零开始)。将 postfixHasPrefix 设为指示新的较低层内部 元组是否应有前缀;若应有,则将 postfixPrefixDatum 设为该前缀值。这两个前 缀与向下链接结点的标签(如果有)的组合,必须与原始前缀具有相同的含 义,因为没有机会修改被移动到新下层元组中的结点标签,也不能更改任何子 索引项。结点拆分完成后,choose 函数会用替换后 的内部元组再次被调用。如果 spgSplitTuple 动作没有 创建合适的结点,这次调用可以返回 spgAddNode。最 终,choose 必须返回 spgMatchNode,以便插入继续下降到下一层。
picksplit决定如何在一组叶子元组上创建一个新的内部元组。
该函数的 SQL 声明必须如下所示:
CREATE FUNCTION my_picksplit(internal, internal) RETURNS void ...
第一个参数是一个指向 spgPickSplitIn C 结构的 指针,其中包含该函数的输入数据。第二个参数是一个指向 spgPickSplitOut C 结构的指针,函数必须将结 果数据填入其中。
typedef struct spgPickSplitIn
{
int nTuples; /* 叶子元组的数量 */
Datum *datums; /* 它们的 datum(长度为 nTuples 的数组) */
int level; /* 当前层级(从零开始计) */
} spgPickSplitIn;
typedef struct spgPickSplitOut
{
bool hasPrefix; /* 新内部元组应有前缀? */
Datum prefixDatum; /* 如果有,前缀值 */
int nNodes; /* 新内部元组的结点数 */
Datum *nodeLabels; /* 它们的标签(或为 NULL 表示无标签) */
int *mapTuplesToNodes; /* 每个叶子元组对应的结点索引 */
Datum *leafTupleDatums; /* 每个新叶子元组中存储的 datum */
} spgPickSplitOut;
nTuples 是提供的叶子元组数量。 datums 是由它们的 datum 值组成的数组,类 型为 spgConfigOut.leafType。 level 是这些叶子元组共享的当前层级,它将 成为新内部元组的层级。
将 hasPrefix 设为指示新的内部元组是否应 有前缀;若应有,则将 prefixDatum 设为该 前缀值。将 nNodes 设为新内部元组将包含 的结点数,并将 nodeLabels 设为这些结点 的标签值数组;如果不需要结点标签,则设为 NULL。将 mapTuplesToNodes 设为一个数组,其中给出 每个叶子元组应分配到的结点索引(从零开始)。将 leafTupleDatums 设为要存储在新叶子元组 中的值数组(如果操作符类不会在层级之间修改 datum,这些值就与输入的 datums 相同)。注意, picksplit 函数负责为 nodeLabels、 mapTuplesToNodes 和 leafTupleDatums 数组执行 palloc。
如果提供了多于一个叶子元组,则期望 picksplit 函数把它们划分到多于一个结点中;否 则就无法把叶子元组拆分到多个页上,而这正是此操作的最终目的。因此, 如果 picksplit 最终把所有叶子元组都放进同一个 结点,SP-GiST 核心代码会覆盖这一决定,生成一个内部元组,并将叶子元 组随机分配到多个标签相同的结点上。这样的元组会被标记为 allTheSame,以表明发生了这种情况。 choose 和 inner_consistent 函数必须对这种内部元组做出恰当处理。更多信息见 Section 4.4.3。
只有在 config 函数将 longValuesOK 设为真,并且提供了一个大于一 页的输入值时,picksplit 才会应用到单个叶子元 组。在这种情况下,这个操作的目的是剥离一个前缀,并产生一个新的、更短 的叶子 datum 值。该调用会重复进行,直到生成足够短、能够放入一页的叶 子 datum。更多信息见 Section 4.4.1。
inner_consistent在树搜索期间返回需要继续跟随的一组结点(分支)。
该函数的 SQL 声明必须如下所示:
CREATE FUNCTION my_inner_consistent(internal, internal) RETURNS void ...
第一个参数是一个指向 spgInnerConsistentIn C 结构的指针,其中包含该函数的输入数据。第二个参数是一个指向 spgInnerConsistentOut C 结构的指针,函数必 须将结果数据填入其中。
typedef struct spgInnerConsistentIn
{
ScanKey scankeys; /* 操作符和比较值的数组 */
ScanKey orderbys; /* 排序操作符和比较值的数组 */
int nkeys; /* scankeys 数组的长度 */
int norderbys; /* orderbys 数组的长度 */
Datum reconstructedValue; /* 在父元组处重构的值 */
void *traversalValue; /* 操作符类特定的遍历值 */
MemoryContext traversalMemoryContext; /* 将新的遍历值放在这里 */
int level; /* 当前层级(从零开始计) */
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;
长度为 nkeys 的 scankeys 数组描述索引搜索条件。这些条件 用 AND 组合 — 只有满足全部条件的索引项才是我们关心的。(注 意, nkeys = 0 表示所有索引项都满足该查 询。)通常 consistent 函数只关心每个数组元素的 sk_strategy 和 sk_argument 字段,它们分别给出可索引操 作符和比较值。特别地,无需检查 sk_flags 以判断比较值是否为 NULL,因为 SP-GiST 核心代码会过滤掉此类条件。长 度为 norderbys 的 orderbys 数组以相同方式描述排序操作符 (如果有)。reconstructedValue 是为父元 组重建的值;在根层,或者当 inner_consistent 函数没有在父层提供该值时,它为 (Datum) 0。 traversalValue 是指向任意遍历数据的指针, 这些数据由上一次针对父索引元组调用 inner_consistent 时向下传递;在根层时则为 NULL。traversalMemoryContext 是存放输出 遍历值(见下文)的内存上下文。level 是 当前内部元组的层级,根层为零。如果本查询需要重建数据,则 returnData 为 true; 只有在 config 函数声明了 canReturnData 时才会如此。若当前内部元组 被标记为 “all-the-same”,则 allTheSame 为真;在这种情况下,所有结点 都具有相同的标签(如果有),因此要么全部匹配该查询,要么全部不匹配 (参见 Section 4.4.3)。若当前内部元组包 含前缀,则 hasPrefix 为真;若是如此, prefixDatum 就是该前缀值。 nNodes 是内部元组中包含的子结点数量, nodeLabels 是它们的标签值数组;如果结点 没有标签,则为 NULL。
nNodes 必须设为搜索需要访问的子结点数 量,并且 nodeNumbers 必须设为这些结点 索引的数组。如果操作符类跟踪层级,则将 levelAdds 设为一个数组,其中给出下降到 每个待访问结点时所需增加的层数。(这些增量常常对所有结点都相同,但并 非必然如此,所以这里使用数组。)如果需要值重建,则将 reconstructedValues 设为一个数组,其中包 含为每个待访问子结点重建的值;否则,将 reconstructedValues 保持为 NULL。重构 值假定具有 spgConfigOut.leafType 类型。(不过,由于核心系统除了可能复制它们以外不会对其做任何处理,只 要它们具有与 leafType 相同的 typlen 和 typbyval 属性就足 够了。)如果执行的是有序搜索,则根据 orderbys 数组将 distances 设为距离值数组(距离最小的结点 将最先处理);否则保持为 NULL。如果希望将额外的带外信息 (“遍历值”)向下传递到树搜索的更低层,则将 traversalValues 设为适当遍历值的数组,每 个待访问子结点对应一个;否则,将 traversalValues 保持为 NULL。注意, inner_consistent 函数负责在当前内存上下文中为 nodeNumbers、 levelAdds、 distances、 reconstructedValues 和 traversalValues 数组执行 palloc。不过, traversalValues 数组所指向的任何输出遍历 值都应在 traversalMemoryContext 中分配。 每个遍历值都必须是单独 palloc 的一个块。
leaf_consistent如果叶子元组满足查询,则返回 true。
该函数的 SQL 声明必须如下所示:
CREATE FUNCTION my_leaf_consistent(internal, internal) RETURNS bool ...
第一个参数是一个指向 spgLeafConsistentIn C 结构的指针,其中包含该函数的输入数据。第二个参数是一个指向 spgLeafConsistentOut C 结构的指针,函数必 须将结果数据填入其中。
typedef struct spgLeafConsistentIn
{
ScanKey scankeys; /* 操作符和比较值的数组 */
ScanKey orderbys; /* 排序操作符和比较值的数组 */
int nkeys; /* scankeys 数组的长度 */
int norderbys; /* orderbys 数组的长度 */
Datum reconstructedValue; /* 在父元组处重构的值 */
void *traversalValue; /* 操作符类特定的遍历值 */
int level; /* 当前层级(从零开始计) */
bool returnData; /* 必须返回原始数据? */
Datum leafDatum; /* 叶子元组中的 datum */
} spgLeafConsistentIn;
typedef struct spgLeafConsistentOut
{
Datum leafValue; /* 重构出的原始数据(如果有) */
bool recheck; /* 如果必须重新检查操作符则设为真 */
bool recheckDistances; /* 如果必须重新检查距离则设为真 */
double *distances; /* 关联距离 */
} spgLeafConsistentOut;
长度为 nkeys 的 scankeys 数组描述索引搜索条件。这些条件 用 AND 组合在一起 — 只有满足全部条件的索引项才满足该查询。(注 意 nkeys = 0 表示所有索引项都满足该查 询。)通常 consistent 函数只关心每个数组元素的 sk_strategy 和 sk_argument 字段,它们分别给出可索引操 作符和比较值。特别地,无需检查 sk_flags 以判断比较值是否为 NULL,因为 SP-GiST 核心代码会过滤掉此类条件。长 度为 norderbys 的 orderbys 数组以相同方式描述排序操作符。 reconstructedValue 是为父元组重建的值; 在根层,或者当 inner_consistent 函数没有在父层 提供该值时,它为 (Datum) 0。 traversalValue 是指向任意遍历数据的指针, 这些数据由上一次针对父索引元组调用 inner_consistent 时向下传递;在根层时则为 NULL。level 是当前叶子元组的层级,根层为 零。如果本查询需要重建数据,则 returnData 为 true; 只有在 config 函数声明了 canReturnData 时才会如此。 leafDatum 是当前叶子元组中存储的、类型为 spgConfigOut.leafType 的键值。
如果叶子元组匹配查询,则该函数必须返回 true, 否则返回 false。在返回 true 的情况下,如果 returnData 为 true, 则必须将 leafValue 设为最初为该叶子元组 提供并建立索引的值(类型为 spgConfigIn.attType)。 此外,如果匹配结果不确定,因而必须将操作符重新应用到实际的堆元组上以 验证匹配,则可以将 recheck 设为 true。如果执行的是有序搜索,则根据 orderbys 数组将 distances 设为距离值数组;否则保持为 NULL。如果返回的距离中至少有一个不是精确值,则将 recheckDistances 设为 true。在这种情况 下,执行器会在从堆中取回元组后计算精确距离,并在需要时重新排列元组。
可选的用户定义方法是:
Datum compress(Datum in)将数据项转换为一种适合在索引叶子元组中物理存储的格式。它接受一个类型 为 spgConfigIn.attType 的值,并返回一个类型为 spgConfigOut.leafType 的值。输出值不得包含行外 TOAST 指针。
注意:compress 方法只作用于需要存储的值。一致 性方法接收到的查询 scankeys 不会改变, 不会经过 compress 的转换。
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 都会被 重置。因此,通常不必太担心是否 pfree 了你用 palloc 分配的所有内容。 (config 方法是个例外:它应尽量避免内存泄漏。不 过通常 config 方法只需把常量赋入传入的参数结构即 可。)
如果被索引列属于可应用排序规则的数据类型,则索引排序规则会通过标准的 PG_GET_COLLATION() 机制传递给所有支持方法。
本节介绍实现细节以及其他一些对 SP-GiST 操作符类实 现者有用的技巧。
单个叶子元组和内部元组都必须能放入单个索引页中(默认 8kB)。因此,在对 变长数据类型的值建立索引时,只有像基数树这类方法才能支持长值:树的 每一层都包含足够短、能放进一页的前缀,而最终叶子层也包含足够短、能放进 一页的后缀。只有当操作符类准备好保证这一点时,才应将 longValuesOK 设为 true。否则, SP-GiST 核心会拒绝为过大、无法放入索引页的值建立 索引的请求。
同样,确保内部元组不会增长到大得无法放入索引页,也是操作符类的责任;这 限制了单个内部元组中可使用的子结点数量,以及前缀值的最大大小。
另一个限制是,当内部元组的某个结点指向一组叶子元组时,这些元组必须全部 位于同一个索引页上。(这是一个设计决策,目的是减少寻道,并节省把这类元 组链接成链时所需链接占用的空间。)如果一组叶子元组增长到单页无法容纳,就会执行拆分 并插入一个中间内部元组。要解决这个问题,新的内部元组必须 把叶子值集合划分成多个结点组。如果操作符类的 picksplit 函数做不到这一点, SP-GiST 核心就会诉诸 Section 4.4.3 中描述的非常规措施。
当 longValuesOK 为真时,预期 SP-GiST 树的连续各层会把越来越多的信息吸收到内部 元组的前缀和结点标签中,从而使所需的叶子 datum 越来越小,最终能够放入 一页。为了防止操作符类中的缺陷导致插入陷入无限循环,如果在连续十次调 用 choose 方法之后叶子 datum 仍没有变小, SP-GiST 核心就会报错。
某些树算法对每个内部元组都使用固定的一组结点;例如在四叉树中,总是恰好 有四个结点,对应于内部元组中心点周围的四个象限。在这种情况下,代码通常 按编号处理结点,因此不需要显式的结点标签。为了省略结点标签(从而节省一 些空间),picksplit 函数可以为 nodeLabels 数组返回 NULL,同样在执行 spgSplitTuple 动作期间, choose 函数也可以为 prefixNodeLabels 数组返回 NULL。随后对 choose 和 inner_consistent 的调用中,nodeLabels 也将为 NULL。原则上, 同一个索引中可以对某些内部元组使用结点标签,而对另一些省略。
当处理具有无标签结点的内部元组时,choose 返回 spgAddNode 是错误的,因为在这种情况下结点集合应被 视为固定不变。
当 picksplit 无法把提供的叶子值划分为至少两个结点 类别时,SP-GiST 核心可以覆盖操作符类 picksplit 函数的结果。在这种情况下,会创建一个新 的内部元组,其中有多个结点,而每个结点都具有相同的标签(如果有);这个 标签就是 picksplit 给它唯一使用的那个结点分配的 标签。叶子值会被随机分配到这些等价结点中。该内部元组会设置 allTheSame 标志,用来提醒 choose 和 inner_consistent 函数:这个元组的结点集合并不是它们通常会预期的那种。
在处理 allTheSame 元组时, choose 返回 spgMatchNode 表示新值可以分配给任意一个等价结点;核心代码会忽略给出的 nodeN 值,并随机下降到其中一个结点(以保持 树的平衡)。choose 返回 spgAddNode 则属于错误,因为那会使结点不再全部等 价;如果待插入的值与现有结点不匹配,就必须使用 spgSplitTuple 动作。
在处理 allTheSame 元组时, inner_consistent 函数应当要么把全部结点返回为继 续索引搜索的目标,要么一个也不返回,因为它们都是等价的。这是否需要编写特殊情 况代码,取决于 inner_consistent 函数平常对这些结 点含义做了多大程度的假定。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。