支持的版本:当前 (17) / 16 / 15 / 14 / 13
开发版本:devel
不支持的版本:12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2

64.3. SP-GiST 索引 #

64.3.1. 简介 #

SP-GiST是空间分区GiST. SP-GiST的缩写,支持分区搜索树,这有助于开发各种不同的非平衡数据结构,例如四叉树、k-d 树和基数树(trie)。这些结构的共同特征是它们重复地将搜索空间划分为不必大小相等的多个分区。与分区规则非常匹配的搜索可能非常快。

这些流行的数据结构最初是为内存中使用而开发的。在主内存中,它们通常设计为一组由指针链接的动态分配的节点。这不适合直接存储在磁盘上,因为这些指针链可能相当长,这将需要太多的磁盘访问。相比之下,基于磁盘的数据结构应该具有高扇出以最大限度地减少 I/O。解决的挑战是SP-GiST以这样一种方式将搜索树节点映射到磁盘页面,即使它遍历许多节点,搜索也只需要访问几个磁盘页面。

GiST, SP-GiST旨在允许由数据类型领域的专家而不是数据库专家开发具有适当访问方法的自定义数据类型。

此处的一些信息来自普渡大学的 SP-GiST 索引项目 网站。在 PostgreSQL 中的SP-GiST实现主要由 Teodor Sigaev 和 Oleg Bartunov 维护,并且他们的 网站上有更多信息。

64.3.2. 内置操作符类 #

核心 PostgreSQL 发行版包含表 64.2 中所示的SP-GiST操作符类。

表 64.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_opskd_point_opspoly_ops 操作符类支持 <-> 排序操作符,该操作符支持在索引点或多边形数据集上进行 k 最近邻 (k-NN) 搜索。

64.3.3. 可扩展性 #

SP-GiST提供了一个高度抽象的接口,要求访问方法开发人员仅实现特定于给定数据类型的方法。SP-GiST核心负责高效的磁盘映射和搜索树结构。它还负责并发和日志记录方面的考虑。

一个SP-GiST树的叶子元组通常包含与索引列相同的数据类型的值,尽管它们也可能包含索引列的有损表示。存储在根级别的叶子元组将直接表示原始索引数据值,但较低级别的叶子元组可能仅包含部分值,例如后缀。在这种情况下,操作符类支持函数必须能够使用从传递到以到达叶子级别的内部元组累积的信息来重建原始值。

当使用 INCLUDE 列创建SP-GiST索引时,这些列的值也存储在叶子元组中。INCLUDE 列与SP-GiST操作符类无关,因此此处不再进一步讨论它们。

内部元组更复杂,因为它们是搜索树中的分支点。每个内部元组都包含一组或多个节点,它们表示一组相似的叶子值。一个节点包含一个下行链路,该链路要么通向另一个较低级别的内部元组,要么通向位于同一索引页上的短叶子元组列表。每个节点通常都有一个标签来描述它;例如,在基数树中,节点标签可以是字符串值的下一个字符。(或者,如果操作符类对所有内部元组使用一组固定的节点,则操作符类可以省略节点标签;请参阅第 64.3.4.2 节。)可选地,内部元组可以有一个前缀值来描述其所有成员。在基数树中,这可以是表示的字符串的公共前缀。前缀值不一定是真正的前缀,但可以是操作符类需要的任何数据;例如,在四叉树中,它可以存储用于测量四个象限的中心点。然后,四叉树内部元组还将包含与此中心点周围的象限相对应的四个节点。

某些树算法需要知道当前元组的级别(或深度),因此SP-GiST核心为操作符类提供了在树下降时管理级别计数的能力。在需要时,还支持增量重建表示的值,以及在树下降期间传递其他数据(称为遍历值)。

注意

SP-GiST核心代码会处理空条目。虽然SP-GiST索引确实为索引列中的 null 存储了条目,但这对于索引操作符类代码是隐藏的:永远不会将空索引条目或搜索条件传递给操作符类方法。(假设SP-GiST操作符是严格的,因此不能对 null 值成功。)因此,此处不再进一步讨论 null 值。

对于SP-GiST,索引操作符类必须提供五个用户定义的方法,并且其中两个是可选的。所有五个强制方法都遵循接受两个 internal 参数的约定,第一个参数是指向包含支持方法的输入值的 C 结构体的指针,而第二个参数是指向必须放置输出值的 C 结构体的指针。四个强制方法只返回 void,因为它们的所有结果都出现在输出结构体中;但是 leaf_consistent 返回一个 boolean 结果。这些方法不得修改其输入结构体的任何字段。在所有情况下,在调用用户定义的方法之前,输出结构体会初始化为零。可选的第六种方法 compress 接受一个 datum 作为唯一参数进行索引,并返回适合在叶子元组中进行物理存储的值。可选的第七种方法 options 接受一个 internal 指向 C 结构体的指针,其中应放置 opclass 特定的参数,并返回 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。 仅当 attType 的类型是可变长度,并且操作符类能够通过重复添加后缀来分割长值时,才应将 longValuesOK 设置为 true(请参阅 第 64.3.4.1 节)。

leafType 应该与操作符类的 opckeytype 目录条目定义的索引存储类型匹配。(请注意,opckeytype 可以为零,表示存储类型与操作符类的输入类型相同,这是最常见的情况。)出于向后兼容的原因,config 方法可以将 leafType 设置为其他值,并且将使用该值;但这已被弃用,因为索引内容在目录中会被错误地识别。此外,允许将 leafType 保持未初始化(零);这被解释为表示索引存储类型从 opckeytype 派生而来。

attTypeleafType 不同时,必须提供可选方法 compress。方法 compress 负责将要索引的数据从 attType 转换为 leafType

choose

选择一种将新值插入到内部元组的方法。

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;

datumspgConfigIn.attType 类型的原始数据,该数据将被插入到索引中。leafDatumspgConfigOut.leafType 类型的值,如果提供了方法 compress,则该值最初是应用 compress 方法对 datum 的结果,否则与 datum 的值相同。如果 choosepicksplit 方法更改了 leafDatum,则树的较低级别可能会更改 leafDatum。当插入搜索到达叶子页时,leafDatum 的当前值将存储在新建的叶子元组中。level 是当前内部元组的级别,根级别的起始值为零。如果当前内部元组被标记为包含多个等效节点,则 allTheSame 为 true(请参阅 第 64.3.4.3 节)。如果当前内部元组包含前缀,则 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,以允许插入下降到下一级别。

picksplit

决定如何在一组叶子元组上创建新的内部元组。

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 函数负责 palloc'ing nodeLabelsmapTuplesToNodesleafTupleDatums 数组。

如果提供了多个叶子元组,则期望 picksplit 函数将它们分类为多个节点;否则,不可能跨多个页面分割叶子元组,这是此操作的最终目的。因此,如果 picksplit 函数最终将所有叶子元组放置在同一个节点中,则核心 SP-GiST 代码将覆盖该决定,并生成一个内部元组,其中叶子元组被随机分配给几个标签相同的节点。这样的元组被标记为 allTheSame,表示这种情况已经发生。 chooseinner_consistent 函数必须适当处理此类内部元组。有关更多信息,请参阅 第 64.3.4.3 节

仅当 config 函数将 longValuesOK 设置为 true 并且提供了大于页面的输入值时,才能将 picksplit 应用于单个叶子元组。在这种情况下,操作的目的是剥离前缀并生成新的、较短的叶子数据值。将重复调用该操作,直到生成足够短以适合页面的叶子数据。有关更多信息,请参阅 第 64.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_strategysk_argument 字段,它们分别给出了可索引的操作符和比较值。特别是,没有必要检查 sk_flags 来查看比较值是否为 NULL,因为 SP-GiST 核心代码会过滤掉此类条件。长度为 norderbys 的数组 orderbys 以相同的方式描述了排序操作符(如果有)。reconstructedValue 是为父元组重建的值;在根级别或当 inner_consistent 函数未在父级别提供值时,其值为 (Datum) 0traversalValue 是一个指向从上一次对父索引元组调用 inner_consistent 传递下来的任何遍历数据的指针,如果在根级别则为 NULL。traversalMemoryContext 是存储输出遍历值的内存上下文(见下文)。level 是当前内部元组的级别,根级别从零开始。returnDatatrue,如果此查询需要重建的数据;仅当 config 函数断言 canReturnData 时,才会如此。allTheSame 为 true,如果当前内部元组被标记为 全部相同;在这种情况下,所有节点都具有相同的标签(如果有),因此它们要么全部匹配查询,要么全部不匹配查询(请参阅 第 64.3.4.3 节)。hasPrefix 为 true,如果当前内部元组包含前缀;如果是这样,则 prefixDatum 是其值。nNodes 是内部元组中包含的子节点数,而 nodeLabels 是其标签值的数组,如果节点没有标签则为 NULL。

nNodes 必须设置为搜索需要访问的子节点数,nodeNumbers 必须设置为其索引的数组。如果操作符类跟踪级别,则将 levelAdds 设置为下降到要访问的每个节点时所需的级别增量的数组。(通常这些增量对于所有节点都是相同的,但并非必须如此,因此使用数组。)如果需要值重建,则将 reconstructedValues 设置为为每个要访问的子节点重建的值的数组;否则,将 reconstructedValues 保留为 NULL。假定重建值的类型为 spgConfigOut.leafType。(但是,由于核心系统除了可能复制它们外不会对它们执行任何操作,因此只要它们具有与 leafType 相同的 typlentypbyval 属性就足够了。)如果执行有序搜索,则根据 orderbys 数组将 distances 设置为距离值的数组(将首先处理距离最小的节点)。否则,将其保留为 NULL。如果希望将额外的带外信息(遍历值)传递到树搜索的较低级别,请将 traversalValues 设置为相应遍历值的数组,每个要访问的子节点一个;否则,将 traversalValues 保留为 NULL。请注意,inner_consistent 函数负责在当前内存上下文中 palloc nodeNumberslevelAddsdistancesreconstructedValuestraversalValues 数组。但是,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_strategysk_argument 字段,它们分别给出了可索引的操作符和比较值。特别是,没有必要检查 sk_flags 来查看比较值是否为 NULL,因为 SP-GiST 核心代码会过滤掉此类条件。长度为 norderbys 的数组 orderbys 以相同的方式描述了排序操作符。reconstructedValue 是为父元组重建的值;在根级别或当 inner_consistent 函数未在父级别提供值时,其值为 (Datum) 0traversalValue 是一个指向从上一次对父索引元组调用 inner_consistent 传递下来的任何遍历数据的指针,如果在根级别则为 NULL。level 是当前叶元组的级别,根级别从零开始。returnDatatrue,如果此查询需要重建的数据;仅当 config 函数断言 canReturnData 时,才会如此。leafDatum 是存储在当前叶元组中的 spgConfigOut.leafType 的键值。

如果叶元组与查询匹配,则函数必须返回 true,否则返回 false。在 true 的情况下,如果 returnDatatrue,则 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() 机制传递给所有支持方法。

64.3.4. 实现 #

本节介绍了实现细节以及对SP-GiST操作符类的实现者有用的其他技巧。

64.3.4.1. SP-GiST 限制 #

单个叶元组和内部元组必须适合单个索引页(默认为 8kB)。因此,当索引可变长度数据类型的值时,只能通过诸如基数树之类的方法来支持长值,其中树的每一层都包括一个足够短以适合一页的前缀,并且最终叶级别也包括一个足够短以适合一页的后缀。只有在准备安排此情况发生时,操作符类才应将 longValuesOK 设置为 true。否则,SP-GiST核心将拒绝任何索引过大而无法容纳在索引页上的值的请求。

同样,操作符类有责任确保内部元组不会增长太大而无法容纳在索引页上;这限制了一个内部元组中可以使用的子节点数,以及前缀值的最大大小。

另一个限制是,当内部元组的节点指向一组叶元组时,这些元组必须都在同一索引页中。(这是一个设计决策,目的是减少查找并节省将此类元组链接在一起的链接中的空间。)如果叶元组集太大而无法容纳在页面上,则会执行拆分并插入一个中间内部元组。为了解决此问题,新的内部元组必须将叶值集划分为多个节点组。如果操作符类的 picksplit 函数未能做到这一点,则SP-GiST核心会采取 第 64.3.4.3 节 中描述的特殊措施。

longValuesOK 为 true 时,预期SP-GiST树的连续级别将吸收更多信息到内部元组的前缀和节点标签中,从而使所需的叶数据越来越小,最终使其适合页面。为了防止操作符类中的错误导致无限插入循环,如果叶数据在十个 choose 方法调用周期内没有变小,则SP-GiST核心将引发错误。

64.3.4.2. 不带节点标签的 SP-GiST #

一些树算法为每个内部元组使用一组固定的节点;例如,在四叉树中,总是恰好有四个节点对应于内部元组的质心点周围的四个象限。在这种情况下,代码通常按数字处理节点,而不需要显式的节点标签。为了抑制节点标签(从而节省一些空间),picksplit 函数可以为 nodeLabels 数组返回 NULL,同样,在 spgSplitTuple 操作期间,choose 函数可以为 prefixNodeLabels 数组返回 NULL。这将反过来导致在后续调用 chooseinner_consistentnodeLabels 为 NULL。原则上,节点标签可以用于某些内部元组,而对于同一索引中的其他内部元组则可以省略。

当处理具有未标记节点的内部元组时,choose 返回 spgAddNode 是错误的,因为在这种情况下,节点集应该是固定的。

64.3.4.3. 全部相同 内部元组 #

SP-GiSTpicksplit 函数未能将提供的叶值划分为至少两个节点类别时,核心代码可以覆盖操作符类的 picksplit 函数的结果。当发生这种情况时,将创建一个新的内部元组,其中多个节点都具有相同的标签(如果有),该标签是 picksplit 函数赋予它使用的那个节点的,并且叶值在这些等效节点之间随机分配。内部元组上设置了 allTheSame 标志,以警告 chooseinner_consistent 函数,该元组不具有它们可能期望的节点集。

当处理 allTheSame 元组时,choosespgMatchNode 结果被解释为意味着新值可以分配给任何等效节点;核心代码将忽略提供的 nodeN 值,并随机下降到其中一个节点(以便保持树的平衡)。choose 返回 spgAddNode 是错误的,因为这将使节点不再全部等效;如果要插入的值与现有节点不匹配,则必须使用 spgSplitTuple 操作。

当处理 allTheSame 元组时,inner_consistent 函数应该返回所有节点或不返回任何节点作为继续索引搜索的目标,因为它们都是等效的。这可能需要或可能不需要任何特殊情况的代码,具体取决于 inner_consistent 函数通常对节点的含义假设了多少。

64.3.5. 示例 #

PostgreSQL 源代码分发包包含几个用于的索引操作符类的示例,SP-GiST表 64.2中所述。查看 src/backend/access/spgist/src/backend/utils/adt/ 以查看代码。

提交更正

如果您在文档中发现任何不正确、与您使用特定功能的经验不符或需要进一步澄清的内容,请使用此表单报告文档问题。