Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

64.3. SP-GiST 索引 #

64.3.1. 简介
64.3.2. 内置算符类
64.3.3. 扩展性
64.3.4. 实现
64.3.5. 示例

64.3.1. 简介 #

SP-GiST是空间分区串缩词GiST. SP-GiST支持分区搜索树,有助于开发各种不同的非平衡数据结构,如四叉树、k-d 树和前缀树(尝试)。这些结构的共同特点是它们会重复将搜索空间划分成大小不必相等的分区。与分区规则匹配良好的搜索速度可能非常快。

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

比如GiST, SP-GiST旨在允许数据类型的专家(而非数据库专家)开发具有适当访问方法的自定义数据类型。

这里的一些信息源自普渡大学的 SP-GiST 索引项目 网站SP-GiSTPostgreSQL 中的

实现主要由 Teodor Sigaev 和 Oleg Bartunov 维护,他们 网站 上还有更多信息。

64.3.2. 内置运算符类 #SP-GiST核心 PostgreSQL 发行版包括

运算符类,如 表 64.2 所示。SP-GiST表 64.2. 内置

运算符类 名称 可索引运算符
排序运算符 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)
|&> (多边形、多边形)
四分点运算 kd_point_ops |>> (point,point)
<-> (point,point)
<< (point,point)
>> (point,point)
<<| (point,point)
~= (point,point)
范围运算 = (任意范围、任意范围)  
&& (任意范围、任意范围)
@> (任意范围、任意元素)
@> (任意范围、任意范围)
<@ (任意范围、任意范围)
<< (任意范围、任意范围)
>> (任意范围、任意范围)
&< (任意范围、任意范围)
&> (任意范围、任意范围)
-|- (任意范围、任意范围)
文本运算 = (文本、文本)  
< (文本、文本)
<= (文本、文本)
> (文本、文本)
>= (文本、文本)
~<~ (文本、文本)
~<=~ (文本、文本)
~>=~ (文本、文本)
~>~ (文本、文本)
^@ (文本、文本)

对于类型 的两个运算符类,四分点运算 是默认值。kd 点运算 支持相同的运算符,但使用不同的索引数据结构,这在某些应用程序中可能提供更好的性能。

四分点运算kd 点运算多边形运算 运算符类支持 <-> 排序运算符,此运算符支持对索引点或多边形数据集执行 k 近邻 (k-NN) 搜索。

64.3.3. 可扩展性 #

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

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

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

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

某些树算法需要有关当前元组的级别(或深度)的知识,因此SP-GiST内核为运算符类提供了在沿着树向下移动时管理级别计数的可能性。对于在需要时增量重构所表示的值和在树向下移动过程中传递附加数据(称为遍历值)也存在支持。

注意

内核SP-GiST代码负责处理空条目。虽然SP-GiST索引确实针对已编入索引列中的空值存储条目,但这对索引运算符类代码是隐藏的:永远不会将空索引条目或搜索条件传递给运算符类方法。(假设SP-GiST运算符是严格的,因此无法对空值成功。)因此,本文将不再讨论空值。

对于针对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 ...

第一个参数是指向包含函数输入数据的 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是可变长度且运算符类能够通过重复后缀对长值进行分割(见64.3.4.1 节)时,应仅将longValuesOK设置为 true。

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 方法,则一开始是应用于 datumcompress 方法的结果,否则就是和 datum 相同的值。如果 choose 或者 picksplit 方法改变了 leafDatum,它可以在树的较低层级发生改变。当插入搜索到达叶页面,leafDatum 当前的值将被存储在新建的叶元组中。 level 是当前内元组的层级,从根层级的零开始。 allTheSame 在当前内元组被标记为包含多个等效节点时为真(参见 第 64.3.4.3 节)。 hasPrefix 在当前内元组包含一个前缀时为真;如果是,则 prefixDatum 是其值。 nNodes 是内元组包含的子节点的数量,nodeLabels 是一个包含其标签值或者在没有标签时为 NULL 的数组。

choose 函数可以判断新值是否匹配某个现有的子节点,或者必须添加一个新的子节点,或者新值与元组前缀不一致,因此内元组必须被切分为一个限制性较小的前缀。

如果新值匹配某个现有的子节点,请将 resultType 设置为 spgMatchNode。将 nodeN 设置为节点数组中该节点的索引(从零开始)。将 levelAdd 设置为下降到该节点导致的 level 的增量,或者(如果操作符类不使用层级)保留为零。如果操作符类不修改从一个层级到下一个层级的 data,将 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;        /* 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 相同)。请注意,负责 palloc 的 picksplit 函数数组 nodeLabels, mapTuplesToNodesleafTupleDatums

如果提供了多个叶元组,则期望 picksplit 函数会将它们分类到多个节点中;否则,不能将叶元组拆分到多个页面中,这是此操作的最终目的。因此,如果 picksplit 函数最终将所有叶元组放在同一节点中,核心 SP-GiST 代码将替代该决策,并生成内部元组,其中叶元组被随机分配到几个标签相同的节点中。这种元组标记为 allTheSame 以表明发生了这种情况。 chooseinner_consistent 函数必须对这种内部元组采取适当的注意。有关更多信息,请参见 第 64.3.4.3 节

picksplit 只能对单个叶元组应用,在 config 函数将 longValuesOK 设置为 true 并且已经提供了大于页的大于页输入值的情况下。在这种情况下,操作的目的是剥离前缀,并生成新的更短的叶数据值。该调用将重复,直到生成足够短的叶数据来适应页面为止。有关更多信息,请参见 第 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,描述了索引搜索条件。这些条件是和操作结合在一起的,仅同时满足所有条件的索引条目才具有价值(注意 nkeys = 0 表示所有索引条目都满足查询)。通常,一致性函数只关心每个数组条目的 sk_strategysk_argument 域,它们分别给出可索引运算符和比较值。特别地,无需查看 sk_flags 来判断比较值是否为 NULL,因为 SP-GiST 内核代码会过滤掉此类条件。长度为 norderbys 的数组 orderbys,以相同方式描述排序运算符(如果有)。reconstructedValue 是为父 tuple 重建的值;在根级别或如果 inner_consistent 函数未在父级别提供值,则该值为 (Datum) 0traversalValue 是指向从 inner_consistent 在父索引 tuple 上前次回调传递下来的任何遍历数据的指针,或在根级别为 NULL。traversalMemoryContext 是用来存储输出遍历值(见下文)的内存环境。level 是当前内部 tuple 的级别,根级别的起始值为零。returnData 在本查询需要重构数据时为 true;当且仅当 config 函数断言 canReturnData 时才成立。allTheSame 在当前内部 tuple 被标记为 all-the-same 时为 true;在此情况下,所有节点都有相同的标签(如果有),因此所有节点都匹配查询,或者没有节点匹配查询(见 第 64.3.4.3 节)。hasPrefix 在当前内部 tuple 包含前缀时为 true;如果包含,prefixDatum 是其值。nNodes 是内部 tuple 中包含的子节点数,nodeLabels 是其标签值的数组,如果节点没有标签,则该数组为 NULL。

nNodes 必须设置为需要被搜索访问的子节点数,nodeNumbers 必须设置为一个数组,用于存储它们的索引。如果运算符类追踪级别,将 levelAdds 设置为当降级到每个待访问节点时所需的级别增量数组。(通常这些增量对于所有节点而言都是相同的,但并非必然如此,因此使用了一个数组。)如果需要值重构,设置 reconstructedValues 为一个待访问的每个子节点重构的值数组;如果不,让 reconstructedValues 保持为 NULL。重构的值被认为是 spgConfigOut.leafType 类型。(然而,因为核心系统除了可能复制它们之外对它们不做任何操作,因此只需要它们拥有与 leafType 相同的 typlentypbyval 属性即可。)如果执行有序搜索,根据 orderbys 数组将 distances 设置为一个距离值数组(具有最小距离的节点将被首先处理)。否则让它为 NULL。如果想要将附加的带外信息(遍历值)向下传递到树搜索的较低层级,将 traversalValues 设置为适当遍历值数组,为每个待访问的子节点提供一个遍历值;如果不,让 traversalValues 保持为 NULL。注意,inner_consistent 函数负责在当前内存上下文中分配 nodeNumberslevelAddsdistancesreconstructedValuestraversalValues 数组。然而,traversalValues 数组指向的任何输出遍历值都应该在 traversalMemoryContext 中分配。每个遍历值都必须是一个单独 alloc 的块。

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 是为父元组重建的值;在根级别它为 (Datum) 0,或者若 inner_consistent 函数没有在父级提供某个值时也是如此。 traversalValue 是指向从之前对父索引元组执行 inner_consistent 时的遍历数据的任何指针,或根级别的 NULL。 level 是当前叶元组的级别,从根级别的零开始。 returnData 在查询需要使用重建数据时为 true;仅在 config 函数断言 canReturnData 时才为真。 leafDatumspgConfigOut 的键值。leafType 存储在当前叶元组中。

若叶元组满足查询,则函数必须返回 true,若叶元组不满足查询则返回 falsetrue 情况下,如果 returnDatatrue,则 leafValue 必须设置为值(类型为 spgConfigIn.attType),最初提供该值来为叶元组创建索引。此外,如果匹配不确定,可以将 recheck 设置为 true,因此必须重新将运算符应用到实际堆元组以验证匹配。如果执行有序搜索,请根据 orderbys 数组将 distances 设置为距离值数组。否则,将其保留为 NULL。如果至少一个已返回距离不是精确的,则将 recheckDistances 设置为 true。在这种情况下,执行器将在从堆中提取元组后计算精确的距离,并在需要时重新排序元组。

可选的用户定义方法为

Datum compress(Datum in)

将数据项转换为适合索引叶元组物理存储的格式。它接受类型为 spgConfigIn.attType 的值,并返回类型为 spgConfigOut.leafType 的值。输出值不得包含游离 TOAST 指针。

注意:compress 方法仅适用于要存储的值。一致方法接收查询 scankeys 且未更改,未使用 compress 进行变换。

选项

定义一组用户可见的参数,它们用于控制算子类行为。

内核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树的连续层级将把越来越多的信息吸收进内部元组的前缀和节点标签中,从而使必需的叶基准数据越来越小,最终可以适合一个页面。为防止操作符类中的错误导致无穷插入循环,SP-GiST如果在choose方法调用十个周期内叶基准数据没有变小,核心会引发错误。

64.3.4.2. 缺少节点标签的 SP-GiST #

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

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

64.3.4.3. 同类 内部元组 #

内核SP-GiST当码元类别的picksplit函数无法将所供应的叶值至少划分为两个节点类别时,core 可以覆盖picksplit函数的结果。当发生这种情况时,新的内部二元组将创建多个节点,每个节点都具有picksplit赋予其使用的节点(如果有的话)相同的标签(如果有),并且叶值在这些同等节点之间随机分配。设置内部二元组上的allTheSame标记以警告chooseinner_consistent函数,该二元组不具有它们可能预期的节点集。

在处理allTheSame二元组时,choose结果spgMatchNode的意思是可以将新值分配给任何一个同等节点;core 代码将忽略所供应的nodeN值,并随机进入节点之一(以便保持树的平衡)。choose返回spgAddNode是一个错误,因为那样会让节点不再同等;如果要插入的值与现有节点不匹配,则必须使用spgSplitTuple操作。

在处理allTheSame二元组时,inner_consistent函数应返回所有节点或没有节点作为继续索引搜索的目标,因为它们是同等的。这是否需要任何特殊情况代码取决于inner_consistent函数通常对节点的含义有多少假设。

64.3.5. 示例 #

PostgreSQL 源分发包含针对表格 64.2中描述的多个索引运算符类别的示例。SP-GiST查看src/backend/access/spgist/src/backend/utils/adt/以查看该代码。