2024 年 9 月 26 日: PostgreSQL 17 发布!
支持的版本:Current (17) / 16 / 15 / 14 / 13 / 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 树和基数树(字典树)。这些结构共同的特点是,它们会不断地将搜索空间分割成不必相等的若干个分区。与分区规则匹配得相当的搜索,通常可以很快得到结果。

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

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

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

64.3.2. 内置运算符类 #

核心 PostgreSQL 发布包含SP-GiST表 64.2 中显示的运算符类。

表 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)

对于 type 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树通常包含与其索引列相同数据类型的值,虽然它们也可能包含索引列的无损耗表示。存储在根级别的叶元组将直接表示原始的索引数据值,但较低级别的叶元组可能只包含部分值,例如后缀。在这种情况下,运算符类支持函数必须能够使用从通过并到达叶级别的内部元组累积的信息重建原始值。

SP-GiST使用 INCLUDE 列创建索引时,这些列的值也将存储在叶元组中。 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接受指向 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为可变长度且操作符类别能够通过重复后缀来对长值进行分段时才应设置为 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;

datum 是要插入到索引中的 spgConfigIn.attType 类型的原始数据。 leafDatumspgConfigOut.leafType 类型的值,当提供方法 compress 时,最初是应用于 datum 的方法 compress 的结果值,否则与 datum 相同。如果 choosepicksplit 方法更改了 leafDatum,则 leafDatum 可以在树的较低级别更改。当插入搜索到达叶页面时,leafDatum 的当前值就是存储在新创建的叶元组中的值。 level 是当前内部元组的级别,从根级别的零开始。如果当前内部元组被标记为包含多个等效节点,则 allTheSame 为真(请参阅 第 64.3.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

决定如何针对一组叶元组创建新的内部元组。

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 函数负责分配 nodeLabelsmapTuplesToNodesleafTupleDatums 数组。P

如果提供了一个以上的叶元组,则预期 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,描述了索引搜索条件。这些条件与 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 为真,如果当前内元组标记为 all-the-same;在这种情况下,所有节点都有相同的标签(如果有),因此所有节点与查询匹配,或者没有一个节点与查询匹配(请参阅 第 64.3.4.3 节)。如果当前内元组包含前缀,则 hasPrefix 为真;如果是,prefixDatum 为它的值。 nNodes 是内元组中包含的子节点数,nodeLabels 是其标签值数组,如果没有标签,则为 NULL。

nNodes 必须设置为搜索需要访问的子节点数,nodeNumbers 必须设置为其索引的数组。如果运算符类跟踪层级,将 levelAdds 设置为下降到要访问的每个节点时所需的层级递增数组。(通常,这些递增对于所有节点来说都是相同的,但不一定如此,因此使用数组。)如果需要值重构,将 reconstructedValues 设置为要访问的每个子节点重构的值的数组;否则,将 reconstructedValues 保留为 NULL。重构的值假设类型为 spgConfigOut.leafType。(但是,由于核心系统除了可能会复制它们之外不会对它们进行任何操作,因此它们具有与 leafType 相同的 typlentypbyval 属性就足够了。)执行有序搜索时,根据 orderbys 数组将 distances 设置为距离值数组(距离最小的节点将首先处理)。否则将其保留为 NULL。如果希望将附加带外信息(遍历值)传递到树搜索的较低层级,将 traversalValues 设置为适当遍历值的数组,每个要访问的子节点一个;否则,将 traversalValues 保留为 NULL。请注意,inner_consistent 函数负责在当前内存上下文中分配 nodeNumberslevelAddsdistancesreconstructedValuestraversalValues 数组。但是,traversalValues 数组指向的任何输出遍历值都应在 traversalMemoryContext 中分配。每个遍历值必须是一个单独分配的块。

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 是当前叶元组的级别,从根级别的零开始。returnData 在此查询需要重建数据时为 true;只有当 config 函数断言 canReturnData 时才会这样。leafDatum 是存储在当前叶元组中的 spgConfigOutleafType 的键值。

如果叶元组匹配查询,则该函数必须返回 true,否则返回 false。如果 returnDatatrue 情况下为 true,则必须将 leafValue 设置为最初提供给要为该叶元组编制索引的值(类型为 spgConfigInattType)。此外,如果匹配不确定,则可以将 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 将在处理每个元组后重置。因此,不必担心释放您分配的所有内容(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为真时,预计SP-GiST树的连续层将越来越多的信息吸收进内部元组的前缀和节点标记,使所需的叶数据越来越小,以便最终存储在页面上。为了防止运算符类中的错误导致无限插入循环,SP-GiST如果在choose方法调用的十个周期内叶数据未变小,内核将引发一个错误。

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

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

使用具有未标记节点的内部元组时,对于choose函数返回spgAddNode是一种错误,因为在这种情况下节点集应该是固定的。

64.3.4.3. 完全相同内部元组 #

SP-GiST当无法将提供的叶值分成至少两个节点类别时,某些内核可能会覆盖运算符类的 picksplit 函数的结果。发生这种情况时,会创建带有多个节点的新内部元组,每个节点都具有 picksplit 给出的用于节点的同一标签(如有),并且叶值随机分配在这些等效节点之间。 allTheSame 标记会在内部元组上设置,以警告 chooseinner_consistent 函数,该元组没有它们可能预期的节点集。

处理 allTheSame 元组时,spgMatchNodechoose 结果会被解释为可以将新值分配给任何等效节点。核心代码会忽略提供的 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/ 查看其代码。

提交更正

如果您在文档中发现不正确的地方,与您对特定功能的体验不符,或需要进一步的澄清,请使用 此表单 报告一个文档问题。