69.3. 可扩展性#
SP-GiST提供了一个具有高抽象级别的接口,要求访问方法开发者仅实现特定于给定数据类型的方法。SP-GiST内核负责高效的磁盘映射和搜索树结构。它还负责并发性和日志记录注意事项。
SP-GiST 树的叶元组通常包含与索引列相同数据类型的数值,尽管它们也可能包含索引列的损耗表示。存储在根级别的叶元组将直接表示原始索引数据值,但较低级别的叶元组可能只包含部分值,例如后缀。在这种情况下,操作符类支持函数必须能够使用从传递到达到叶级别的内部元组中累积的信息来重建原始值。
当使用INCLUDE
列创建 SP-GiST 索引时,这些列的值也存储在叶元组中。INCLUDE
列与 SP-GiST 操作符类无关,因此此处不再讨论它们。
内部元组更复杂,因为它们是搜索树中的分支点。每个内部元组包含一组一个或多个节点,它们表示相似叶值组。节点包含一个下行链接,该链接指向另一个较低级别的内部元组或指向同一索引页上的所有叶元组的短列表。每个节点通常有一个标签来描述它;例如,在基数树中,节点标签可以是字符串值的下一个字符。(或者,如果操作符类对所有内部元组使用一组固定节点,则可以省略节点标签;请参见第 69.4.2 节。)内部元组可以选择具有前缀值来描述其所有成员。在基数树中,这可以是所表示字符串的公共前缀。前缀值不一定真的是前缀,但可以是操作符类需要的任何数据;例如,在四叉树中,它可以存储四个象限相对于其测量的中心点。然后,四叉树内部元组还将包含四个与该中心点周围的象限相对应的节点。
一些树算法需要知道当前元组的级别(或深度),因此SP-GiST核心为操作符类提供了在下降树时管理级别计数的可能性。在需要时,还支持增量重建表示的值,以及在树下降期间传递附加数据(称为遍历值)。
注意
SP-GiST核心代码负责处理空条目。虽然SP-GiST索引确实存储了索引列中的空值的条目,但这对索引操作符类代码是隐藏的:永远不会将空索引条目或搜索条件传递给操作符类方法。(假设SP-GiST运算符是严格的,因此不能对空值成功。)因此,此处不再进一步讨论空值。
对于SP-GiST的索引操作符类,有五个用户定义的方法必须提供,还有两个可选的方法。所有五个强制性方法都遵循接受两个internal
参数的约定,第一个参数是指向包含支持方法输入值的 C 结构的指针,而第二个参数是指向必须放置输出值的 C 结构的指针。四个强制性方法只返回void
,因为它们的所有结果都出现在输出结构中;但leaf_consistent
返回boolean
结果。这些方法不得修改其输入结构的任何字段。在所有情况下,在调用用户定义的方法之前,输出结构都会初始化为零。可选的第六个方法compress
接受一个datum
作为唯一参数进行索引,并返回适合在叶元组中物理存储的值。可选的第七个方法options
接受一个internal
指针,指向 C 结构,其中应放置特定于操作类的参数,并返回void
。
五种强制用户定义的方法是
config
返回有关索引实现的静态信息,包括前缀和节点标签数据类型的数据类型 OID。
函数声明必须如下所示
CREATE FUNCTION my_config(internal, internal) RETURNS void ...
第一个参数是指向 C 结构
spgConfigIn
的指针,其中包含函数的输入数据。第二个参数是指向 C 结构spgConfigOut
的指针,函数必须用结果数据填充该结构。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。仅当attType
为可变长度并且操作符类能够通过重复后缀对长值进行分段时,才应将longValuesOK
设置为 true(请参阅 第 69.4.1 节)。leafType
应与操作符类的opckeytype
目录项定义的索引存储类型相匹配。(请注意,opckeytype
可以为零,这意味着存储类型与操作符类的输入类型相同,这是最常见的情况。)出于向后兼容性的原因,config
方法可以将leafType
设置为其他值,并且将使用该值;但这已弃用,因为索引内容随后在目录中被错误识别。此外,允许将leafType
保留为未初始化(零);这被解释为意味着从opckeytype
派生的索引存储类型。当
attType
和leafType
不同时,必须提供可选方法compress
。方法compress
负责将要编入索引的数据从attType
转换为leafType
。choose
选择一种将新值插入内部元组的方法。
函数声明必须如下所示
CREATE FUNCTION my_choose(internal, internal) RETURNS void ...
第一个参数是指向 C 结构
spgChooseIn
的指针,其中包含函数的输入数据。第二个参数是指向 C 结构spgChooseOut
的指针,函数必须用结果数据填充该结构。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
为真,如果当前内部元组被标记为包含多个等效节点(请参见 第 69.4.3 节)。hasPrefix
为真,如果当前内部元组包含前缀;如果是,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 数组,或在不需要节点标签时将其设置为 NULL。请注意,新上层元组的总大小不得大于它所替换的元组的总大小;这会限制新前缀和新标签的长度。将childNodeN
设置为将下行链接到新较低级别内部元组的节点的索引(从零开始)。设置postfixHasPrefix
以指示新较低级别内部元组是否应具有前缀,如果应具有前缀,则将postfixPrefixDatum
设置为前缀值。这两个前缀和下行链接节点的标签(如果存在)的组合必须与原始前缀具有相同的含义,因为没有机会更改移动到新较低级别元组的节点标签,也没有机会更改任何子索引条目。拆分节点后,将再次使用替换内部元组调用choose
函数。如果spgSplitTuple
操作未创建合适的节点,则该调用可能会返回spgAddNode
结果。最终,choose
必须返回spgMatchNode
以允许插入降至下一级。picksplit
决定如何针对一组叶元组创建新的内部元组。
函数声明必须如下所示
CREATE FUNCTION my_picksplit(internal, internal) RETURNS void ...
第一个参数是指向 C 结构
spgPickSplitIn
的指针,其中包含函数的输入数据。第二个参数是指向 C 结构spgPickSplitOut
的指针,函数必须用结果数据填充该结构。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
函数负责 palloc'ingnodeLabels
、mapTuplesToNodes
和leafTupleDatums
数组。如果提供了多个叶元组,则
picksplit
函数应将它们分类到多个节点中;否则无法将叶元组拆分到多个页面中,而这是此操作的最终目的。因此,如果picksplit
函数最终将所有叶元组都放在同一节点中,则核心 SP-GiST 代码将覆盖该决策并生成一个内部元组,其中叶元组被随机分配到几个具有相同标签的节点中。这样的元组标记为allTheSame
以表示已发生这种情况。choose
和inner_consistent
函数必须对这些内部元组进行适当处理。有关详细信息,请参见 第 69.4.3 节。仅当
config
函数将longValuesOK
设置为 true 并且已提供大于页面的输入值时,才能将picksplit
应用于单个叶元组。在这种情况下,操作的目的是剥离前缀并生成一个新的、更短的叶数据值。将重复调用,直到生成一个足够短以适合页面的叶数据为止。有关详细信息,请参见 第 69.4.1 节。inner_consistent
返回在树搜索期间要遵循的节点(分支)集。
函数声明必须如下所示
CREATE FUNCTION my_inner_consistent(internal, internal) RETURNS void ...
第一个参数是指向 C 结构
spgInnerConsistentIn
的指针,其中包含函数的输入数据。第二个参数是指向 C 结构spgInnerConsistentOut
的指针,函数必须用结果数据填充该结构。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;
The array
scankeys
, of lengthnkeys
, describes the index search condition(s). These conditions are combined with AND — only index entries that satisfy all of them are interesting. (Note thatnkeys
= 0 implies that all index entries satisfy the query.) Usually the consistent function only cares about thesk_strategy
andsk_argument
fields of each array entry, which respectively give the indexable operator and comparison value. In particular it is not necessary to checksk_flags
to see if the comparison value is NULL, because the SP-GiST core code will filter out such conditions. The arrayorderbys
, of lengthnorderbys
, describes ordering operators (if any) in the same manner.reconstructedValue
is the value reconstructed for the parent tuple; it is(Datum) 0
at the root level or if theinner_consistent
function did not provide a value at the parent level.traversalValue
is a pointer to any traverse data passed down from the previous call ofinner_consistent
on the parent index tuple, or NULL at the root level.traversalMemoryContext
is the memory context in which to store output traverse values (see below).level
is the current inner tuple's level, starting at zero for the root level.returnData
istrue
if reconstructed data is required for this query; this will only be so if theconfig
function assertedcanReturnData
.allTheSame
is true if the current inner tuple is marked “all-the-same”; in this case all the nodes have the same label (if any) and so either all or none of them match the query (see Section 69.4.3).hasPrefix
is true if the current inner tuple contains a prefix; if so,prefixDatum
is its value.nNodes
is the number of child nodes contained in the inner tuple, andnodeLabels
is an array of their label values, or NULL if the nodes do not have labels.nNodes
必须设置为搜索需要访问的子节点数,nodeNumbers
必须设置为其索引的数组。如果运算符类跟踪级别,将levelAdds
设置为在下降到要访问的每个节点时所需的级别增量数组。(通常这些增量对于所有节点都是相同的,但并非必然如此,因此使用数组。)如果需要值重构,将reconstructedValues
设置为要访问的每个子节点重构的值的数组;否则,将reconstructedValues
保留为 NULL。假定重构的值为spgConfigOut
.leafType
类型。(但是,由于核心系统除了可能复制它们之外不会对它们执行任何操作,因此它们具有与leafType
相同的typlen
和typbyval
属性就足够了。)如果执行有序搜索,将distances
设置为根据orderbys
数组的距离值数组(距离最小的节点将首先处理)。否则,将其保留为 NULL。如果希望将附加带外信息(“遍历值”)传递到树搜索的较低级别,将traversalValues
设置为适当遍历值的数组,每个要访问的子节点一个;否则,将traversalValues
保留为 NULL。请注意,inner_consistent
函数负责在当前内存上下文中 pallocnodeNumbers
、levelAdds
、distances
、reconstructedValues
和traversalValues
数组。但是,traversalValues
数组指向的任何输出遍历值都应在traversalMemoryContext
中分配。每个遍历值必须是一个单独的 palloc 块。leaf_consistent
如果叶元组满足查询,则返回 true。
函数声明必须如下所示
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
定义一组用户可见的参数,用于控制运算符类的行为。
函数声明必须如下所示
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 支持方法通常在短期内存上下文中调用;也就是说,在处理每个元组后,CurrentMemoryContext
将被重置。因此,不必非常担心 pfree 所有 palloc 的内容。(config
方法是一个例外:它应该尝试避免内存泄漏。但通常config
方法只需将常量分配到传递的参数结构中即可。)
如果索引列是可整理数据类型,则将使用标准PG_GET_COLLATION()
机制将索引整理规则传递给所有支持方法。