SP-GiST是空间分区的缩写。GiST. SP-GiST支持分区搜索树,这有助于开发各种不同的非平衡数据结构,例如四叉树、k-d 树和基数树(trie 树)。这些结构的共同特点是它们反复将搜索空间划分为不需要大小相等的子空间。与分区规则匹配良好的搜索可以非常快速。
这些流行的数据结构最初是为内存使用而开发的。在主内存中,它们通常被设计为一组通过指针链接的动态分配节点。这不适合直接存储到磁盘,因为这些指针链可能相当长,这将需要太多的磁盘访问。相反,基于磁盘的数据结构应该具有高扇出以最小化 I/O。解决的挑战是SP-GiST是将搜索树节点映射到磁盘页面,使得搜索只需要访问几个磁盘页面,即使它遍历许多节点。
与GiST, SP-GiST一样,它旨在允许由数据类型领域的专家(而不是数据库专家)开发具有适当访问方法的自定义数据类型。
这里的一些信息来自普渡大学的 SP-GiST 索引项目网站。在PostgreSQL中,SP-GiST的实现主要由 Teodor Sigaev 和 Oleg Bartunov 维护,他们的网站上有更多信息。
核心 PostgreSQL 发行版包含SP-GiST操作符类如表 65.2所示。
表 65.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类型的两个操作符类中,quad_point_ops是默认的。kd_point_ops支持相同的操作符,但使用不同的索引数据结构,这在某些应用中可能提供更好的性能。
quad_point_ops、kd_point_ops和poly_ops操作符类支持<->排序操作符,该操作符可以在索引点或多边形数据集上启用 k-最近邻(k-NN)搜索。
SP-GiST提供了一个高度抽象的接口,要求访问方法开发人员只实现特定于给定数据类型的方法。SP-GiST核心负责高效的磁盘映射和树结构的搜索。它还负责并发和日志记录方面的问题。
一个SP-GiST树的叶元组通常包含与索引列相同数据类型的值,尽管它们也可以包含索引列的有损表示。存储在根级别的叶元组将直接表示原始索引数据值,但较低级别的叶元组可能只包含部分值,例如后缀。在这种情况下,操作符类支持函数必须能够使用从传递到叶级别的内部元组中积累的信息来重建原始值。
当使用INCLUDE列创建SP-GiST索引时,这些列的值也会存储在叶元组中。INCLUDE列与SP-GiST操作符类无关,因此此处不再赘述。
内部元组更复杂,因为它们是搜索树中的分支点。每个内部元组包含一个或多个节点的集合,这些节点表示相似叶值的组。一个节点包含一个向下链接,该链接要么指向另一个更低级别的内部元组,要么指向一个短的叶元组列表,这些叶元组都位于同一个索引页上。每个节点通常有一个描述它的标签;例如,在基数树中,节点标签可以是字符串值的下一个字符。(或者,如果操作符类对所有内部元组都使用固定节点集,则可以省略节点标签;请参阅第 65.3.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; /* Data type to be indexed */
} spgConfigIn;
typedef struct spgConfigOut
{
Oid prefixType; /* Data type of inner-tuple prefixes */
Oid labelType; /* Data type of inner-tuple node labels */
Oid leafType; /* Data type of leaf-tuple values */
bool canReturnData; /* Opclass can reconstruct original data */
bool longValuesOK; /* Opclass can cope with values > 1 page */
} spgConfigOut;
传入attType是为了支持多态索引操作符类;对于普通的固定数据类型操作符类,它的值将始终相同,因此可以忽略。
对于不使用前缀的操作符类,prefixType可以设置为VOIDOID。同样,对于不使用节点标签的操作符类,labelType可以设置为VOIDOID。canReturnData应设置为 true,如果操作符类能够重建最初提供的索引值。longValuesOK应仅在attType是变长且操作符类能够通过重复后缀分割长值时(请参阅第 65.3.4.1 节)设置为 true。
leafType应与操作符类的opckeytype目录条目定义的索引存储类型匹配。(请注意,opckeytype可以为零,这意味着存储类型与操作符类的输入类型相同,这是最常见的情况。)出于向后兼容性原因,config方法可以将leafType设置为其他值,并且将使用该值;但此操作已被弃用,因为索引内容随后在目录中被错误识别。此外,允许将leafType未初始化(零);这被解释为表示从opckeytype派生的索引存储类型。
当attType和leafType不同时,必须提供可选方法compress。方法compress负责将要索引的数据项从attType转换为leafType。
选择选择将新值插入内部元组的方法。
该SQL函数声明必须如下所示
CREATE FUNCTION my_choose(internal, internal) RETURNS void ...
第一个参数是指向spgChooseIn C 结构体的指针,其中包含函数的输入数据。第二个参数是指向spgChooseOut C 结构体的指针,函数必须用结果数据填充该结构体。
typedef struct spgChooseIn
{
Datum datum; /* original datum to be indexed */
Datum leafDatum; /* current datum to be stored at leaf */
int level; /* current level (counting from zero) */
/* Data from current inner tuple */
bool allTheSame; /* tuple is marked all-the-same? */
bool hasPrefix; /* tuple has a prefix? */
Datum prefixDatum; /* if so, the prefix value */
int nNodes; /* number of nodes in the inner tuple */
Datum *nodeLabels; /* node label values (NULL if none) */
} spgChooseIn;
typedef enum spgChooseResultType
{
spgMatchNode = 1, /* descend into existing node */
spgAddNode, /* add a node to the inner tuple */
spgSplitTuple /* split inner tuple (change its prefix) */
} spgChooseResultType;
typedef struct spgChooseOut
{
spgChooseResultType resultType; /* action code, see above */
union
{
struct /* results for spgMatchNode */
{
int nodeN; /* descend to this node (index from 0) */
int levelAdd; /* increment level by this much */
Datum restDatum; /* new leaf datum */
} matchNode;
struct /* results for spgAddNode */
{
Datum nodeLabel; /* new node's label */
int nodeN; /* where to insert it (index from 0) */
} addNode;
struct /* results for spgSplitTuple */
{
/* Info to form new upper-level inner tuple with one child tuple */
bool prefixHasPrefix; /* tuple should have a prefix? */
Datum prefixPrefixDatum; /* if so, its value */
int prefixNNodes; /* number of nodes */
Datum *prefixNodeLabels; /* their labels (or NULL for
* no labels) */
int childNodeN; /* which node gets child tuple */
/* Info to form new lower-level inner tuple with all old nodes */
bool postfixHasPrefix; /* tuple should have a prefix? */
Datum postfixPrefixDatum; /* if so, its value */
} splitTuple;
} result;
} spgChooseOut;
datum是要插入索引的spgConfigIn.attType类型的原始数据。leafDatum是spgConfigOut.leafType类型的值,当提供了compress方法时,它最初是应用于datum的compress方法的结果,否则与datum相同。leafDatum可以在树的较低级别更改,如果choose或picksplit方法更改了它。当插入搜索到达叶页时,leafDatum的当前值将存储在新创建的叶元组中。level是当前内部元组的级别,根级别从零开始。allTheSame如果当前内部元组被标记为包含多个等效节点(请参阅第 65.3.4.3 节),则为 true。hasPrefix如果当前内部元组包含前缀,则为 true;如果是,则prefixDatum是其值。nNodes是内部元组中包含的子节点数量,nodeLabels是其标签值的数组,如果不存在标签则为 NULL。
choose函数可以确定新值是否与现有子节点之一匹配,或者必须添加新子节点,或者新值与元组前缀不一致,因此必须拆分内部元组以创建限制较少的前缀。
如果新值与现有子节点之一匹配,则将resultType设置为spgMatchNode。将nodeN设置为该节点在节点数组中的索引(从零开始)。将levelAdd设置为通过该节点下降导致的level增量,如果操作符类不使用级别,则将其保留为零。如果操作符类不修改从一个级别到下一个级别的数据,则将restDatum设置为等于leafDatum,否则将其设置为修改后的值,以便在下一个级别用作leafDatum。
如果必须添加新的子节点,则将resultType设置为spgAddNode。将nodeLabel设置为新节点要使用的标签,并将nodeN设置为在节点数组中插入节点的索引(从零开始)。添加节点后,将再次使用修改后的内部元组调用choose函数;该调用应导致spgMatchNode结果。
如果新值与元组前缀不一致,则将resultType设置为spgSplitTuple。此操作将所有现有节点移动到一个新的较低级别内部元组中,并将现有内部元组替换为一个具有指向新的较低级别内部元组的单个向下链接的元组。设置prefixHasPrefix以指示新的上层元组是否应具有前缀,如果是,则将prefixPrefixDatum设置为前缀值。此新前缀值必须比原始前缀限制足够少,以接受要索引的新值。设置prefixNNodes以指示新元组中所需的节点数量,并将prefixNodeLabels设置为存储其标签的 palloc'd 数组,如果不需要节点标签则设置为 NULL。请注意,新上层元组的总大小不得超过其替换元组的总大小;这限制了新前缀和新标签的长度。将childNodeN设置为将向下链接到新的较低级别内部元组的节点的索引(从零开始)。设置postfixHasPrefix以指示新的较低级别内部元组是否应具有前缀,如果是,则将postfixPrefixDatum设置为前缀值。这两个前缀和向下链接节点的标签(如果有)的组合必须与原始前缀具有相同的含义,因为没有机会更改移动到新的较低级别元组的节点标签,也无法更改任何子索引条目。拆分节点后,将再次使用替换内部元组调用choose函数。如果spgSplitTuple操作没有创建合适的节点,该调用可能会返回spgAddNode结果。最终choose必须返回spgMatchNode以允许插入下降到下一级别。
选择拆分决定如何在一组叶元组上创建新的内部元组。
该SQL函数声明必须如下所示
CREATE FUNCTION my_picksplit(internal, internal) RETURNS void ...
第一个参数是指向spgPickSplitIn C 结构体的指针,其中包含函数的输入数据。第二个参数是指向spgPickSplitOut C 结构体的指针,函数必须用结果数据填充该结构体。
typedef struct spgPickSplitIn
{
int nTuples; /* number of leaf tuples */
Datum *datums; /* their datums (array of length nTuples) */
int level; /* current level (counting from zero) */
} spgPickSplitIn;
typedef struct spgPickSplitOut
{
bool hasPrefix; /* new inner tuple should have a prefix? */
Datum prefixDatum; /* if so, its value */
int nNodes; /* number of nodes for new inner tuple */
Datum *nodeLabels; /* their labels (or NULL for no labels) */
int *mapTuplesToNodes; /* node index for each leaf tuple */
Datum *leafTupleDatums; /* datum to store in each new leaf tuple */
} spgPickSplitOut;
nTuples是提供的叶元组的数量。datums是其spgConfigOut.leafType类型的数据值数组。level是所有叶元组共享的当前级别,这将成为新内部元组的级别。
设置hasPrefix以指示新内部元组是否应具有前缀,如果是,则将prefixDatum设置为前缀值。设置nNodes以指示新内部元组将包含的节点数量,并将nodeLabels设置为其标签值的数组,如果不需要节点标签则设置为 NULL。将mapTuplesToNodes设置为一个数组,该数组给出每个叶元组应分配到的节点的索引(从零开始)。将leafTupleDatums设置为要存储在新叶元组中的值数组(如果操作符类不修改从一个级别到下一个级别的数据,则这些值将与输入datums相同)。请注意,picksplit函数负责分配nodeLabels、mapTuplesToNodes和leafTupleDatums数组的内存。
如果提供了多个叶元组,则期望picksplit函数将它们分类为多个节点;否则,无法将叶元组拆分到多个页面中,这是此操作的最终目的。因此,如果picksplit函数最终将所有叶元组放置在同一个节点中,SP-GiST 核心代码将覆盖该决定并生成一个内部元组,其中叶元组被随机分配给几个具有相同标签的节点。此类元组被标记为allTheSame,以表示这种情况已经发生。choose和inner_consistent函数必须妥善处理此类内部元组。有关更多信息,请参阅第 65.3.4.3 节。
仅在config函数将longValuesOK设置为 true 且提供了大于页面的输入值的情况下,picksplit才能应用于单个叶元组。在这种情况下,操作的目的是剥离前缀并生成一个新的、更短的叶数据值。该调用将重复,直到生成足够短以适应页面的叶数据值。有关更多信息,请参阅第 65.3.4.1 节。
inner_consistent返回在树搜索期间要遵循的节点(分支)集。
该SQL函数声明必须如下所示
CREATE FUNCTION my_inner_consistent(internal, internal) RETURNS void ...
第一个参数是指向spgInnerConsistentIn C 结构体的指针,其中包含函数的输入数据。第二个参数是指向spgInnerConsistentOut C 结构体的指针,函数必须用结果数据填充该结构体。
typedef struct spgInnerConsistentIn
{
ScanKey scankeys; /* array of operators and comparison values */
ScanKey orderbys; /* array of ordering operators and comparison
* values */
int nkeys; /* length of scankeys array */
int norderbys; /* length of orderbys array */
Datum reconstructedValue; /* value reconstructed at parent */
void *traversalValue; /* opclass-specific traverse value */
MemoryContext traversalMemoryContext; /* put new traverse values here */
int level; /* current level (counting from zero) */
bool returnData; /* original data must be returned? */
/* Data from current inner tuple */
bool allTheSame; /* tuple is marked all-the-same? */
bool hasPrefix; /* tuple has a prefix? */
Datum prefixDatum; /* if so, the prefix value */
int nNodes; /* number of nodes in the inner tuple */
Datum *nodeLabels; /* node label values (NULL if none) */
} spgInnerConsistentIn;
typedef struct spgInnerConsistentOut
{
int nNodes; /* number of child nodes to be visited */
int *nodeNumbers; /* their indexes in the node array */
int *levelAdds; /* increment level by this much for each */
Datum *reconstructedValues; /* associated reconstructed values */
void **traversalValues; /* opclass-specific traverse values */
double **distances; /* associated distances */
} spgInnerConsistentOut;
长度为nkeys的数组scankeys描述了索引搜索条件。这些条件与 AND 组合——只有满足所有这些条件的索引条目才是有趣的。(请注意,nkeys = 0 意味着所有索引条目都满足查询。)通常,一致性函数只关心每个数组条目的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时才会如此。allTheSame如果当前内部元组被标记为“全部相同”,则为 true;在这种情况下,所有节点都具有相同的标签(如果有),因此它们要么全部匹配查询,要么都不匹配查询(请参阅第 65.3.4.3 节)。hasPrefix如果当前内部元组包含前缀,则为 true;如果是,则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数组的内存。但是,traversalValues数组指向的任何输出遍历值都应在traversalMemoryContext中分配。每个遍历值必须是单个 palloc'd 块。
leaf_consistent如果叶元组满足查询,则返回 true。
该SQL函数声明必须如下所示
CREATE FUNCTION my_leaf_consistent(internal, internal) RETURNS bool ...
第一个参数是指向spgLeafConsistentIn C 结构体的指针,其中包含函数的输入数据。第二个参数是指向spgLeafConsistentOut C 结构体的指针,函数必须用结果数据填充该结构体。
typedef struct spgLeafConsistentIn
{
ScanKey scankeys; /* array of operators and comparison values */
ScanKey orderbys; /* array of ordering operators and comparison
* values */
int nkeys; /* length of scankeys array */
int norderbys; /* length of orderbys array */
Datum reconstructedValue; /* value reconstructed at parent */
void *traversalValue; /* opclass-specific traverse value */
int level; /* current level (counting from zero) */
bool returnData; /* original data must be returned? */
Datum leafDatum; /* datum in leaf tuple */
} spgLeafConsistentIn;
typedef struct spgLeafConsistentOut
{
Datum leafValue; /* reconstructed original data, if any */
bool recheck; /* set true if operator must be rechecked */
bool recheckDistances; /* set true if distances must be rechecked */
double *distances; /* associated distances */
} spgLeafConsistentOut;
长度为nkeys的数组scankeys描述了索引搜索条件。这些条件与 AND 组合——只有满足所有这些条件的索引条目才满足查询。(请注意,nkeys = 0 意味着所有索引条目都满足查询。)通常,一致性函数只关心每个数组条目的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将被重置。因此,不必太担心释放所有 palloc 的内存。(config方法是一个例外:它应该尽量避免内存泄漏。但通常config方法除了将常量分配到传递的参数结构中之外,不需要做任何事情。)
如果索引列是可排序数据类型,则索引排序规则将使用标准的PG_GET_COLLATION()机制传递给所有支持方法。
本节涵盖对SP-GiST操作符类的实现者有用的实现细节和其他技巧。
单个叶元组和内部元组必须适合单个索引页(默认为 8kB)。因此,在索引变长数据类型的值时,长值只能通过基数树等方法支持,其中树的每个级别都包含一个足够短以适合页面的前缀,并且最终的叶级别包含一个同样足够短以适合页面的后缀。操作符类应仅在准备好为此进行安排时将longValuesOK设置为 true。否则,SP-GiST核心代码将拒绝任何索引大于索引页大小的值的请求。
同样,操作符类有责任确保内部元组不会过大而无法容纳在一个索引页上;这限制了在一个内部元组中可以使用的子节点数量,以及前缀值的最大大小。
另一个限制是,当一个内部元组的节点指向一组叶元组时,这些元组必须全部位于同一个索引页中。(这是一个设计决定,旨在减少查找和节省链接这些元组的空间。)如果叶元组集增长过大以至于无法容纳在一个页面中,则执行拆分并插入一个中间内部元组。为了解决这个问题,新的内部元组必须将叶值集划分为多个节点组。如果操作符类的picksplit函数未能做到这一点,SP-GiST核心代码将采取第 65.3.4.3 节中描述的特殊措施。
当longValuesOK为 true 时,预期SP-GiST树的连续级别将吸收越来越多的信息到内部元组的前缀和节点标签中,使所需的叶数据变得越来越小,最终它将适合一个页面。为了防止操作符类中的错误导致无限插入循环,如果叶数据在choose方法调用十个周期内没有变小,SP-GiST核心代码将引发错误。
一些树算法为每个内部元组使用一组固定节点;例如,在四叉树中,围绕内部元组质心点的四个象限总是恰好有四个节点。在这种情况下,代码通常通过编号来处理节点,不需要显式节点标签。要抑制节点标签(从而节省一些空间),picksplit函数可以为nodeLabels数组返回 NULL,同样,choose函数可以在spgSplitTuple操作期间为prefixNodeLabels数组返回 NULL。这反过来将导致在后续调用choose和inner_consistent期间nodeLabels为 NULL。原则上,节点标签可以用于某些内部元组,而对于同一索引中的其他内部元组则省略。
当处理具有无标签节点的内部元组时,choose返回spgAddNode是错误的,因为在这种情况下节点集应该是固定的。
该SP-GiST核心代码可以在操作符类的picksplit函数未能将提供的叶值划分为至少两个节点类别时,覆盖picksplit函数的结果。当这种情况发生时,将创建一个新的内部元组,其中包含多个节点,每个节点都具有picksplit用于它所使用的唯一节点的相同标签(如果有),并且叶值在这些等效节点之间随机分配。allTheSame标志在内部元组上设置,以警告choose和inner_consistent函数,该元组不具有它们可能期望的节点集。
当处理allTheSame元组时,spgMatchNode的choose结果被解释为新值可以分配给任何等效节点;核心代码将忽略提供的nodeN值并随机下降到一个节点中(以便保持树的平衡)。choose返回spgAddNode是错误的,因为那会使节点不再等效;如果要插入的值与现有节点不匹配,则必须使用spgSplitTuple操作。
当处理allTheSame元组时,inner_consistent函数应返回所有或不返回任何节点作为继续索引搜索的目标,因为它们都是等效的。这可能需要或可能不需要任何特殊情况代码,具体取决于inner_consistent函数通常对节点含义的假设程度。