PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

65.3. SP-GiST 索引 #

65.3.1. 简介
65.3.2. 内置操作符类
65.3.3. 可扩展性
65.3.4. 实现
65.3.5. 示例

65.3.1. 简介 #

SP-GiST 是 space-partitioned GiST (空间分区 GiST)的缩写。SP-GiST 支持分区搜索树, 这有助于开发各种不同的非平衡数据结构,例如四叉树、k-d 树和基数树 (trie)。这些结构的共同特点是:它们反复地把搜索空间划分成大小不必 相同的分区。与分区规则匹配良好的搜索可以非常快。

这些流行的数据结构最初是为内存中的使用而开发的。在内存中,它们通常 被设计为由指针链接的一组动态分配的节点。这不适合直接存储在磁盘上, 因为这样的指针链可能相当长,会导致过多的磁盘访问。相比之下,基于 磁盘的数据结构应当具有较高的扇出,以最小化 I/O。SP-GiST 所应对的挑战是:把搜索树节点映射到磁盘页面上,使得一次搜索即使遍历 很多节点,也只需访问少数几个磁盘页面。

与 GiST 一样,SP-GiST 的目的是 允许由该数据类型领域的专家(而非数据库专家)来开发带有相应访问方法的 自定义数据类型。

这里的一些信息源自普渡大学的 SP-GiST Indexing Project 网站。 PostgreSQL 中的 SP-GiST 实现主要由 Teodor Sigaev 和 Oleg Bartunov 维护,更多信息见他们的 网站。

65.3.2. 内置操作符类 #

PostgreSQL 核心发行版包含 Table 65.2 所示的 SP-GiST 操作符类。

Table 65.2. 内置 SP-GiST 操作符类

NameIndexable OperatorsOrdering Operators
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_ops、kd_point_ops 和 poly_ops 操作符类支持 <-> 排序操作符,它使得可以对被索引的点或多边形数据集进行 k 近邻 (k-NN)搜索。

65.3.3. 可扩展性 #

SP-GiST 提供了一个抽象层次很高的接口,只要求访问 方法的开发者实现特定于给定数据类型的方法。SP-GiST 核心负责高效的磁盘映射和树结构的搜索,同时也处理并发与日志方面的 考量。

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

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

内层元组更为复杂,因为它们是搜索树中的分支点。每个内层元组包含一组 一个或多个节点,这些节点代表相似叶值的分组。 一个节点包含一个下行链接,它指向另一个更低级别的内层元组,或指向 位于同一索引页面上的一小列叶元组。每个节点通常有一个描述它的 标签;例如,在基数树中,节点标签可以是字符串 值的下一个字符。(另外,如果操作符类对所有内层元组都使用固定的节点 集合,它可以省略节点标签;见 Section 65.3.4.2。) 内层元组还可以选择性地拥有一个描述其所有成员的 前缀值。在基数树中,它可以是所表示字符串的 公共前缀。前缀值不一定是真正的前缀,而可以是操作符类所需的任何数据; 例如,在四叉树中,它可以存储四个象限所参照的中心点。四叉树的内层 元组随后还会包含对应于此中心点周围四个象限的四个节点。

一些树算法需要知道当前元组的层级(或深度),因此 SP-GiST 核心为操作符类提供了在下降树时管理层级 计数的能力。此外还有对增量重建所表示值的支持(在需要时),以及在树 下降过程中向下传递附加数据(称为遍历值)的 支持。

Note

SP-GiST 核心代码负责处理空条目。 虽然 SP-GiST 索引确实会为被索引列中的空值存储 条目,但这对索引操作符类代码是透明的:不会有空的索引条目或搜索条件 被传给操作符类的方法。(这里假定 SP-GiST 操作符是严格的,因此对空值不可能成功。)因此这里不再进一步讨论空 值。

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。 只有当 attType 是变长的、且操作符类能够 通过反复加后缀的方式分割长值时,才应把 longValuesOK 设为 true(见 Section 65.3.4.1)。

leafType 应与操作符类的 opckeytype 目录条目所定义的索引存储类型 一致。 (注意 opckeytype 可以为零,这意味着 存储类型与操作符类的输入类型相同,这是最常见的情况。) 出于向后兼容的原因,config 方法可以把 leafType 设为其他值,该值 也会被使用;但由于这样做会使目录中的索引内容标识不正确,因此已被 弃用。此外,允许不初始化 leafType (即保持为零);这会被解释为使用从 opckeytype 推导出的索引存储类型。

当 attType 与 leafType 不同时,必须提供可选的 compress 方法。 compress 方法负责把待索引的 datum 从 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 的原始 datum。 leafDatum 是类型为 spgConfigOut.leafType 的值;如果提供了 compress 方法,它起初是对 datum 应用 compress 的结果,否则与 datum 相同。 如果 choose 或 picksplit 方法修改了 leafDatum,它在树的较低层级 会发生变化。当插入搜索到达叶页面时,leafDatum 的当前值就是要存储在新创建的叶元组中的值。 level 是当前内层元组的层级,根级别从 零开始。 如果当前内层元组被标记为包含多个等价节点,则 allTheSame 为 true(见 Section 65.3.4.3)。 如果当前内层元组包含前缀,则 hasPrefix 为 true;如果是,prefixDatum 就是其值。 nNodes 是内层元组中包含的子节点数量, nodeLabels 是它们标签值的数组(没有 标签时为 NULL)。

choose 函数可以判定:新值匹配某个现有的子 节点;或者必须添加一个新的子节点;或者新值与元组前缀不一致, 因此必须拆分内层元组以创建一个限制更宽松的前缀。

如果新值匹配某个现有的子节点,把 resultType 设为 spgMatchNode。 把 nodeN 设为该节点在节点数组中的 索引(从零开始)。 把 levelAdd 设为通过该节点下降导致的 level 增量;如果操作符类不使用层级, 保持为零。 如果操作符类不在层级之间修改 datum,把 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 是它们的 datum 值数组,类型为 spgConfigOut.leafType。 level 是所有叶元组共享的当前层级, 它将成为新内层元组的层级。

设置 hasPrefix 以指示新的内层元组是否 应有前缀,如果有,把 prefixDatum 设为 前缀值。 设置 nNodes 以指示新内层元组将包含的 节点数量,并把 nodeLabels 设为它们 标签值的数组(不需要节点标签时为 NULL)。 把 mapTuplesToNodes 设为一个数组,给出 每个叶元组应被分配到的节点的索引(从零开始)。 把 leafTupleDatums 设为要存储在新叶 元组中的值的数组(如果操作符类不在层级之间修改 datum,这些值与 输入的 datums 相同)。 注意,picksplit 函数负责 palloc nodeLabels、 mapTuplesToNodes 和 leafTupleDatums 数组。

如果提供了多于一个叶元组,预期 picksplit 函数会把它们分类到多于一个节点中;否则就不可能把叶元组拆分到 多个页面上,而这正是这个操作的根本目的。因此,如果 picksplit 函数最终把所有叶元组都放在了同一个 节点中,核心 SP-GiST 代码会推翻这个决定,并生成一个把这些叶元组 随机分配到若干个具有相同标签的节点的内层元组。这样的元组会被标记 为 allTheSame,以表示发生了这种情况。 choose 和 inner_consistent 函数必须对这样的内层元组采取适当的处理。 更多信息见 Section 65.3.4.3。

只有当 config 函数把 longValuesOK 设为 true 且提供了一个 超过一页大小的输入值时,picksplit 才可能被 应用于单个叶元组。在这种情况下,该操作的目的是剥离一个前缀, 产生一个新的、更短的叶 datum 值。这个调用会被重复执行,直到产生 一个短到能放进一页的叶 datum。 更多信息见 Section 65.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 意味着所有索引条目都 满足查询。)通常,consistent 函数只关心每个数组条目的 sk_strategy 和 sk_argument 字段,它们分别给出可索引 操作符和比较值。特别地,不需要检查 sk_flags 来判断比较值是否为 NULL,因为 SP-GiST 核心代码会过滤掉这样的 条件。 长度为 norderbys 的数组 orderbys 以相同的方式描述排序操作符 (如果有)。 reconstructedValue 是为父元组重建的值; 在根级别,或当 inner_consistent 函数在父级别 未提供值时,它为 (Datum) 0。 traversalValue 是指向任何遍历数据的 指针,该数据由上一次对父索引元组调用 inner_consistent 时向下传递;在根级别为 NULL。 traversalMemoryContext 是用于存储输出 遍历值的内存上下文(见下文)。 level 是当前内层元组的层级,根级别从 零开始。 如果该查询需要重建数据,returnData 为 true;只有当 config 函数断言了 canReturnData 时才会如此。 如果当前内层元组被标记为“all-the-same”, 则 allTheSame 为 true;此时所有节点 具有相同的标签(如果有),因此它们要么全部匹配查询,要么全部 不匹配(见 Section 65.3.4.3)。 如果当前内层元组包含前缀,则 hasPrefix 为 true;如果是,prefixDatum 就是其值。 nNodes 是内层元组中包含的子节点数量, nodeLabels 是它们标签值的数组(节点 没有标签时为 NULL)。

nNodes 必须被设为搜索需要访问的子节点 数量,nodeNumbers 必须被设为它们索引的 数组。 如果操作符类跟踪层级,把 levelAdds 设 为下降到每个待访问节点所需的层级增量数组。(通常这些增量对所有 节点都相同,但并非必然如此,因此使用数组。) 如果需要值重建,把 reconstructedValues 设为为每个待访问子节点重建的值的数组;否则把 reconstructedValues 保持为 NULL。 假定重建值的类型为 spgConfigOut.leafType。 (不过,由于核心系统除了可能复制它们之外不会对它们做任何事,所以 只要它们具有与 leafType 相同的 typlen 和 typbyval 属性即可。) 如果执行的是有序搜索,把 distances 设 为按照 orderbys 数组得到的距离值数组 (距离最小的节点将被优先处理)。否则保持为 NULL。 如果希望向树搜索的更低层级传递额外的带外信息 (“遍历值”),把 traversalValues 设为相应遍历值的数组, 每个待访问子节点一个;否则把 traversalValues 保持为 NULL。 注意,inner_consistent 函数负责在当前内存 上下文中 palloc nodeNumbers、levelAdds、 distances、 reconstructedValues 和 traversalValues 数组。 不过,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 意味着所有索引条目都 满足查询。)通常,consistent 函数只关心每个数组条目的 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。 如果执行的是有序搜索,把 distances 设 为按照 orderbys 数组得到的距离值数组。 否则保持为 NULL。如果返回的距离中至少有一个不是精确的,把 recheckDistances 设为 true。 在这种情况下,执行器会在从堆中取回元组之后计算精确的距离,并在 需要时对元组重新排序。

可选的用户定义方法是:

Datum compress(Datum in)

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

注意:compress 方法只应用于待存储的值。 consistent 方法接收的查询 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 会被 重置。因此不必太担心对 palloc 出来的东西进行 pfree。 (config 方法是个例外:它应尽量避免泄漏内存。 不过通常 config 方法只需向传入的参数结构赋以 常量即可。)

如果被索引的列是可排序(collatable)的数据类型,索引的排序规则会通过 标准的 PG_GET_COLLATION() 机制传给所有支持 方法。

65.3.4. 实现 #

本节介绍 SP-GiST 操作符类的实现者应当了解的实现 细节和其他技巧。

65.3.4.1. SP-GiST 的限制 #

单个叶元组和内层元组都必须能放入单个索引页面(默认 8kB)。因此, 在为变长数据类型的值建立索引时,只有借助基数树之类的方法才能支持 长值:树的每一级都包含一个短到能放进一页的前缀,而最终的叶级别包含 一个同样短到能放进一页的后缀。只有当操作符类准备好安排这一切时, 才应把 longValuesOK 设为 true。否则, SP-GiST 核心会拒绝任何对大到无法放入索引页面的值 建立索引的请求。

同样,保证内层元组不会增长到无法放入索引页面也是操作符类的责任;这 限制了单个内层元组中可以使用的子节点数量,以及前缀值的最大大小。

另一个限制是:当内层元组的节点指向一组叶元组时,这些元组必须都位于 同一个索引页面上。(这是一项设计决策,目的是减少寻道并节省把这些 元组串在一起的链接所占的空间。)如果这组叶元组增长到一页放不下, 就会执行一次拆分并插入一个中间内层元组。要使拆分解决问题,新的内层 元组必须把叶值集合划分到多于一个节点组中。 如果操作符类的 picksplit 函数做不到这一点, SP-GiST 核心就会采取 Section 65.3.4.3 中描述的特殊措施。

当 longValuesOK 为 true 时,预期 SP-GiST 树的连续各层会把越来越多的信息吸收进 内层元组的前缀和节点标签中,使所需的叶 datum 越来越小,最终能放进 一页。为了防止操作符类的缺陷导致无限的插入循环,如果叶 datum 在 十轮 choose 方法调用内没有任何变小, SP-GiST 核心将抛出错误。

65.3.4.2. 无节点标签的 SP-GiST #

一些树算法对每个内层元组使用固定的节点集合;例如,在四叉树中总是 恰好有四个节点,对应于内层元组中心点周围的四个象限。在这种情况 下,代码通常按编号处理节点,无需显式的节点标签。要取消节点标签 (从而节省一些空间),picksplit 函数可以为 nodeLabels 数组返回 NULL,同样, 在 spgSplitTuple 操作期间 choose 函数可以为 prefixNodeLabels 数组返回 NULL。 这反过来会使后续调用 choose 和 inner_consistent 时 nodeLabels 为 NULL。 原则上,同一个索引中可以某些内层元组使用节点标签而另一些省略。

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

65.3.4.3. “All-the-Same” 内层元组 #

当操作符类的 picksplit 函数未能把提供的叶值 划分成至少两个节点类别时,SP-GiST 核心可以推翻 该函数的结果。发生这种情况时,新内层元组会以多个节点创建,每个节点 都具有 picksplit 给它实际使用的那个节点的标签 (如果有),叶值则在这些等价节点之间随机分配。 内层元组上会设置 allTheSame 标志,以提醒 choose 和 inner_consistent 函数:该元组并没有它们原本可能预期的节点集合。

处理 allTheSame 元组时, choose 返回的 spgMatchNode 结果被解释为:新值可以被分配给任何一个等价节点;核心代码会忽略所 提供的 nodeN 值,随机下降到其中一个节点 (以保持树的平衡)。choose 返回 spgAddNode 是错误的,因为那样会使节点不再全部 等价;如果待插入的值与现有节点都不匹配,必须使用 spgSplitTuple 操作。

处理 allTheSame 元组时, inner_consistent 函数应把全部节点或全部不选 作为继续索引搜索的目标,因为它们全部等价。这是否需要特殊处理取决于 inner_consistent 函数平时对节点含义做了多少 假设。

65.3.5. 示例 #

PostgreSQL 源码发行版包含若干 SP-GiST 索引操作符类的示例,如 Table 65.2 所述。 可以查看 src/backend/access/spgist/ 和 src/backend/utils/adt/ 中的代码。