pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
SP-GiST 提供了一个高度抽象的接口,访问方法开发者只 需实现特定数据类型所需的方法。SP-GiST 核心负责将树 结构高效映射到磁盘并执行搜索,同时也处理并发与日志记录方面的问题。
SP-GiST 树的叶子元组包含与被索引列相同数据类型的值。位于根层的叶子元组始终包含原始的被索引数据值,而位于较低层的叶子元组可能只包含压缩表示,例如一个后缀。在这种情况下,操作符类支持函数必须能够利用为到达叶子层而经过的内部元组中累积的信息,重建出原始值。
内部元组更为复杂,因为它们是搜索树中的分支点。每个内部元组都包含一个或 多个结点,代表相似叶子值的分组。一个结点包含一 个向下链接,它要么指向更低层级的另一个内部元组,要么指向一小组位于 同一索引页上的叶子元组。每个结点都有一个描述它的标签; 例如在后缀树中,结点标签可以是字符串值的下一个字符。内部元组还可以选择带有一个描 述其所有成员的前缀值。在后缀树中,这可以是所 表示字符串的公共前缀。前缀值不一定真的是前缀,它也可以是操作符类需要的 任何数据;例如在四叉树中,它可以存储划分四个象限所依据的中心点。这样, 四叉树的内部元组还会包含四个结点,对应于该中心点周围的四个象限。
某些树算法需要知道当前元组所在的层级(或深度),因此 SP-GiST 核心允许操作符类在沿树向下遍历时管理层级计 数。它还支持在需要时增量重建所表示的值。
SP-GiST 核心代码负责处理值为 null 的索引项。虽然 SP-GiST 索引会为被索引列中的 null 值存储索引项, 但索引操作符类代码看不到这些项:值为 null 的索引项或搜索条件绝不会传给 操作符类方法。(这里假定 SP-GiST 操作符是严格的, 因此对 null 值不可能返回真。)所以这里不再讨论 null 值。
SP-GiST 的索引操作符类必须提供五个用户定义方法。五个必需方法都遵循这样的约定:接受两个 internal 参数,第一个参数是指向某个 C 结构体的指针,其中包 含该支持方法的输入值;第二个参数也是指向某个 C 结构体的指针,方法必须将输 出值写入其中。四个必需方法只返回 void,因为它们的全部结果 都体现在输出结构体中;但 leaf_consistent 还返回一个 boolean 结果。这些方法不得修改其输入结构体中的任何字段。在 所有情况下,调用用户定义方法之前,输出结构体都会先被清零。
五个用户定义方法是:
config返回索引实现的静态信息,包括前缀和结点标签数据类型的 OID。
该SQL声明必须如下所示:
CREATE FUNCTION my_config(internal, internal) RETURNS void ...
第一个参数是一个指向spgConfigInC 结构体的指针,其中包含该函数的输入数据。第二个参数是一个指向spgConfigOutC 结构体的指针,函数必须将结果数据填入其中。
typedef struct spgConfigIn
{
Oid attType; /* 要被索引的数据类型 */
} spgConfigIn;
typedef struct spgConfigOut
{
Oid prefixType; /* 内部元组前缀的数据类型 */
Oid labelType; /* 内部元组结点标签的数据类型 */
bool canReturnData; /* 操作符类能重建原始数据 */
bool longValuesOK; /* 操作符类能处理大小 > 1 页的值 */
} spgConfigOut;
attType的传入是为了支持多态索引操作符类;对于普通的固定数据类型操作符类,它始终具有相同的值,因此可以忽略。
对于不使用前缀的操作符类,可以将 prefixType 设为 VOIDOID。同样,对于不使用结点标签的操作符类, 可以将 labelType 设为 VOIDOID。如果操作符类能够重建最初提供的索引值, 则应将 canReturnData 设为真。只有在 attType 是变长类型,并且该操作符类能够通 过反复取后缀来切分长值时,才应将 longValuesOK 设为真(参见 第 54.3.1 节)。
choose为向内部元组插入新值选择一种方法。
该SQL声明必须如下所示:
CREATE FUNCTION my_choose(internal, internal) RETURNS void ...
第一个参数是一个指向spgChooseInC 结构体的指针,其中包含该函数的输入数据。第二个参数是一个指向spgChooseOutC 结构体的指针,函数必须将结果数据填入其中。
typedef struct spgChooseIn
{
Datum datum; /* 要被索引的原始 datum */
Datum leafDatum; /* 当前要存储在叶子中的 datum */
int level; /* 当前层级(从零开始计) */
/* 来自当前内部元组的数据 */
bool allTheSame; /* 元组被标记为全部相同? */
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; /* 如果有,前缀值 */
Datum nodeLabel; /* 结点的标签 */
/* 构造包含所有旧结点的新下层内部元组所需的信息 */
bool postfixHasPrefix; /* 元组应有前缀? */
Datum postfixPrefixDatum; /* 如果有,前缀值 */
} splitTuple;
} result;
} spgChooseOut;
datum是将要插入索引的原始 datum。leafDatum最初与datum相同,但在树的较低层可能发生变化,如果choose或picksplit方法对它进行了修改。当插入搜索到达叶页时,leafDatum的当前值将存储到新创建的叶子元组中。level是当前内部元组的层级,根层为零。allTheSame为真,表示当前内部元组被标记为包含多个等价结点(参见第 54.3.3 节)。 hasPrefix为真时,表示当前内部元组包含前缀;若是如此,prefixDatum就是该前缀值。nNodes是内部元组中包含的子结点数量,而nodeLabels是它们的标签值数组;如果没有标签,则为 NULL。
choose 函数可以判定:新值要么匹配某个现有子结 点,要么必须添加一个新子结点,要么与该元组的前缀不一致,因此必须拆分 该内部元组以创建限制性更弱的前缀。
如果新值匹配某个现有子结点,则将 resultType 设为 spgMatchNode。将 nodeN 设为该结点在结点数组中的索引(从零开始)。将 levelAdd 设为通过该结点向下下降所导致的 level 增量;如果操作符类不使用层级,则保 持为零。若操作符类不会在层级之间修改 datum,则将 restDatum 设为与 datum 相等;否则,将它设为下一层要用 作 leafDatum 的修改后值。
如果必须添加一个新子结点,则将 resultType 设为 spgAddNode。将 nodeLabel 设为新结点要使用的标签,并将 nodeN 设为 该结点应插入到结点数组中的位置索引(从零开始)。添加该结点后, choose 函数会使用修改后的内部元组再次被调用; 这次调用应返回 spgMatchNode。
如果新值与该元组的前缀不一致,则将 resultType 设为 spgSplitTuple。这个动作会把所有现有结点移动到一 个新的较低层内部元组中,并用一个仅含单个结点、链接到该新下层内部元 组的元组替换现有内部元组。将 prefixHasPrefix 设为指示新的上层元组是 否应有前缀;若应有,则将 prefixPrefixDatum 设为该前缀值。这个新前 缀值必须比原来的限制性更弱,以便能够接受将要索引的新值,并且它不应 比原来的前缀更长。将 nodeLabel 设为指向新的较低层内部元组的 那个结点所使用的标签。将 postfixHasPrefix 设为指示新的较低层内部 元组是否应有前缀;若应有,则将 postfixPrefixDatum 设为该前缀值。这两个前 缀与附加标签的组合,必须与原始前缀具有相同的含义,因为没有机会修改 被移动到新下层元组中的结点标签,也不能更改任何子索引项。结点拆分完 成后,choose 函数会用替换后的内部元组再次被调 用。这次调用通常会得到 spgAddNode 结果,因为拆 分步骤中加入的结点标签多半不会匹配新值;因此之后还会有第三次调用, 它最终返回 spgMatchNode,让插入下降到叶子层。
picksplit决定如何在一组叶子元组上创建一个新的内部元组。
该SQL声明必须如下所示:
CREATE FUNCTION my_picksplit(internal, internal) RETURNS void ...
第一个参数是一个指向spgPickSplitInC 结构体的指针,其中包含该函数的输入数据。第二个参数是一个指向spgPickSplitOutC 结构体的指针,函数必须将结果数据填入其中。
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 值数组。level是所有这些叶子元组当前共同的层级,它将成为新内部元组的层级。
将 hasPrefix 设为指示新的内部元组是否应 有前缀;若应有,则将 prefixDatum 设为该 前缀值。将 nNodes 设为新内部元组将包含 的结点数,并将 nodeLabels 设为这些结点 的标签值数组。(如果结点不需要标签,则将 nodeLabels 设为 NULL;详情参见第 54.3.2 节。)将 mapTuplesToNodes 设为一个数组,其中给出 每个叶子元组应分配到的结点索引(从零开始)。将 leafTupleDatums 设为要存储在新叶子元组 中的值数组(如果操作符类不会在层级之间修改 datum,这些值就与输入的 datums 相同)。注意, picksplit 函数负责为 nodeLabels、 mapTuplesToNodes 和 leafTupleDatums 数组执行 palloc。
如果提供了多于一个叶子元组,则期望 picksplit 函数把它们划分到多于一个结点中;否 则就无法把叶子元组拆分到多个页上,而这正是此操作的最终目的。因此, 如果 picksplit 最终把所有叶子元组都放进同一个 结点,SP-GiST 核心代码会覆盖这一决定,生成一个内部元组,并将叶子元 组随机分配到多个标签相同的结点上。这样的元组会被标记为 allTheSame,以表明发生了这种情况。 choose 和 inner_consistent 函数必须对这种内部元组做出恰当处理。更多信息见 第 54.3.3 节。
只有在 config 函数将 longValuesOK 设为真,并且提供了一个大于一 页的输入值时,picksplit 才会应用到单个叶子元 组。在这种情况下,这个操作的目的是剥离一个前缀,并产生一个新的、更短 的叶子 datum 值。该调用会重复进行,直到生成足够短、能够放入一页的叶 子 datum。更多信息见 第 54.3.1 节。
inner_consistent在树搜索期间返回需要继续跟随的一组结点(分支)。
该SQL声明必须如下所示:
CREATE FUNCTION my_inner_consistent(internal, internal) RETURNS void ...
第一个参数是一个指向spgInnerConsistentInC 结构体的指针,其中包含该函数的输入数据。第二个参数是一个指向spgInnerConsistentOutC 结构体的指针,函数必须将结果数据填入其中。
typedef struct spgInnerConsistentIn
{
ScanKey scankeys; /* 操作符和比较值的数组 */
int nkeys; /* 数组长度 */
Datum reconstructedValue; /* 在父元组处重建的值 */
int level; /* 当前层级(从零开始计) */
bool returnData; /* 必须返回原始数据? */
/* 来自当前内部元组的数据 */
bool allTheSame; /* 元组被标记为全部相同? */
bool hasPrefix; /* 元组有前缀? */
Datum prefixDatum; /* 如果有,前缀值 */
int nNodes; /* 内部元组中的结点数 */
Datum *nodeLabels; /* 结点标签值(如果没有则为 NULL) */
} spgInnerConsistentIn;
typedef struct spgInnerConsistentOut
{
int nNodes; /* 需要访问的子结点数 */
int *nodeNumbers; /* 它们在结点数组中的索引 */
int *levelAdds; /* 对每个结点层级增加这么多 */
Datum *reconstructedValues; /* 关联的重建值 */
} spgInnerConsistentOut;
数组scankeys的长度为nkeys,它描述索引搜索条件。这些条件用 AND 组合 — 只有满足全部条件的索引项才是我们关心的。(注意,nkeys= 0 表示所有索引项都满足该查询。)通常一致性检查函数只关心每个数组元素的sk_strategy和sk_argument字段,它们分别给出可索引操作符和比较值。特别地,无需检查sk_flags以判断比较值是否为 NULL,因为 SP-GiST 核心代码会过滤掉此类条件。reconstructedValue是为父元组重建的值;以下情况下它为(Datum) 0:位于根层,或者inner_consistent函数没有在父层提供该值。level是当前内部元组的层级,根层为零。returnData为true表示本查询需要重建数据;这要求config函数将canReturnData设为真。 allTheSame为真,表示当前内部元组被标记为“全部相同”;在这种情况下,所有结点都具有相同的标签(如果有),因此要么全部匹配该查询,要么全部不匹配(参见第 54.3.3 节)。 hasPrefix为真时,表示当前内部元组包含前缀;若是如此,prefixDatum就是该前缀值。nNodes是内部元组中包含的子结点数量,而nodeLabels是它们的标签值数组;如果结点没有标签,则为 NULL。
nNodes 必须设为搜索需要访问的子结点数 量,并且 nodeNumbers 必须设为这些结点 索引的数组。如果操作符类跟踪层级,则将 levelAdds 设为一个数组,其中给出下降到 每个待访问结点时所需增加的层数。(这些增量常常对所有结点都相同,但并 非必然如此,所以这里使用数组。)如果需要值重建,则将 reconstructedValues 设为一个数组,其中包 含为每个待访问子结点重建的值;否则,将 reconstructedValues 保持为 NULL。注意, inner_consistent 函数负责在当前内存上下文中为 nodeNumbers、 levelAdds 和 reconstructedValues 数组执行 palloc。
leaf_consistent如果叶子元组满足查询,则返回 true。
该SQL声明必须如下所示:
CREATE FUNCTION my_leaf_consistent(internal, internal) RETURNS bool ...
第一个参数是一个指向spgLeafConsistentInC 结构体的指针,其中包含该函数的输入数据。第二个参数是一个指向spgLeafConsistentOutC 结构体的指针,函数必须将结果数据填入其中。
typedef struct spgLeafConsistentIn
{
ScanKey scankeys; /* 操作符和比较值的数组 */
int nkeys; /* 数组长度 */
Datum reconstructedValue; /* 在父元组处重建的值 */
int level; /* 当前层级(从零开始计) */
bool returnData; /* 必须返回原始数据? */
Datum leafDatum; /* 叶子元组中的 datum */
} spgLeafConsistentIn;
typedef struct spgLeafConsistentOut
{
Datum leafValue; /* 重建出的原始数据(如果有) */
bool recheck; /* 如果必须重新检查操作符则设为真 */
} spgLeafConsistentOut;
数组scankeys的长度为nkeys,它描述索引搜索条件。这些条件用 AND 组合 — 只有满足全部条件的索引项才满足该查询。(注意,nkeys= 0 表示所有索引项都满足该查询。)通常一致性检查函数只关心每个数组元素的sk_strategy和sk_argument字段,它们分别给出可索引操作符和比较值。特别地,无需检查sk_flags以判断比较值是否为 NULL,因为 SP-GiST 核心代码会过滤掉此类条件。reconstructedValue是为父元组重建的值;以下情况下它为(Datum) 0:位于根层,或者inner_consistent函数没有在父层提供该值。level是当前叶子元组的层级,根层为零。returnData为true表示本查询需要重建数据;这要求config函数将canReturnData设为真。 leafDatum是当前叶子元组中存储的键值。
如果叶子元组匹配查询,则该函数必须返回 true, 否则返回 false。在返回 true 的情况下,如果 returnData 为 true, 则必须将 leafValue 设为最初为该叶子元组 提供并建立索引的值。 此外,如果匹配结果不确定,因而必须将操作符重新应用到实际的堆元组上以 验证匹配,则可以将 recheck 设为 true。
所有 SP-GiST 支持方法通常都在一个短生命周期的内存上下文中调用;也就是 说,处理完每个元组后,CurrentMemoryContext 都会被 重置。因此,通常不必太担心是否 pfree 了你用 palloc 分配的所有内容。 (config 方法是个例外:它应尽量避免内存泄漏。不 过通常 config 方法只需把常量赋入传入的参数结构体即 可。)
如果被索引列属于支持排序规则的数据类型,则索引排序规则会通过标准的 PG_GET_COLLATION() 机制传递给所有支持方法。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。