受支持版本: 16 / 15 / 14

64.3. 可扩展性 #

SP-GiST 提供了一个高度抽象的接口,访问方法开发者只 需实现特定数据类型所需的方法。SP-GiST 核心负责将树 结构高效映射到磁盘并执行搜索,同时也处理并发与日志记录方面的问题。

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

当创建 SP-GiST 索引并指定 INCLUDE 列时,这些列的值也会存储在叶子元组中。 INCLUDE 列 与 SP-GiST 操作符类无关,因此此处不再展开讨论。

内部元组更为复杂,因为它们是搜索树中的分支点。每个内部元组都包含一个或 多个结点,代表相似叶子值的分组。一个结点包含一 个向下链接,它要么指向更低层级的另一个内部元组,要么指向一小组位于 同一索引页上的叶子元组。每个结点通常都有一个描述它的标签; 例如在基数树中,结点标签可以是字符串值的下一个字符。(或者,如果某个 操作符类对所有内部元组都使用固定的一组结点,也可以省略结点标签;参见 Section 64.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;        /* 要被索引的数据类型 */
} spgConfigIn;

typedef struct spgConfigOut
{
    Oid         prefixType;     /* 内部元组前缀的数据类型 */
    Oid         labelType;      /* 内部元组结点标签的数据类型 */
    Oid         leafType;       /* 叶子元组值的数据类型 */
    bool        canReturnData;  /* 操作符类能重构原始数据 */
    bool        longValuesOK;   /* 操作符类能处理值 > 1 页 */
} spgConfigOut;

传入 attType 是为了支持多态索引操作符类; 对于普通的固定数据类型操作符类,它始终具有相同的值,因此可以忽略。

对于不使用前缀的操作符类,可以将 prefixType 设为 VOIDOID。同样,对于不使用结点标签的操作符类, 可以将 labelType 设为 VOIDOID。如果操作符类能够重建最初提供的索引值, 则应将 canReturnData 设为真。只有在 attType 是变长类型,并且该操作符类能够通 过反复取后缀来切分长值时,才应将 longValuesOK 设为真(参见 Section 64.4.1)。

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

attTypeleafType 不同时,必须提供可选方法 compresscompress 方法 负责将待索引的 datum 从 attType 转换为 leafType

choose #

为向内部元组插入新值选择一种方法。

该函数的 SQL 声明必须如下所示:

CREATE FUNCTION my_choose(internal, internal) RETURNS void ...

第一个参数是一个指向 spgChooseIn C 结构的 指针,其中包含该函数的输入数据。第二个参数是一个指向 spgChooseOut C 结构的指针,函数必须将结果 数据填入其中。

typedef struct spgChooseIn
{
    Datum       datum;          /* 要被索引的原始 datum */
    Datum       leafDatum;      /* 当前要存储在叶子中的 datum */
    int         level;          /* 当前层级(从零开始计) */

    /* 来自当前内部元组的数据 */
    bool        allTheSame;     /* 元组被标记为 all-the-same? */
    bool        hasPrefix;      /* 元组有前缀? */
    Datum       prefixDatum;    /* 如果有,前缀值 */
    int         nNodes;         /* 内部元组中的结点数 */
    Datum      *nodeLabels;     /* 结点标签值(如果没有则为 NULL) */
} spgChooseIn;

typedef enum spgChooseResultType
{
    spgMatchNode = 1,           /* 下降到现有结点 */
    spgAddNode,                 /* 向内部元组添加一个结点 */
    spgSplitTuple               /* 拆分内部元组(修改其前缀) */
} spgChooseResultType;

typedef struct spgChooseOut
{
    spgChooseResultType resultType;     /* 动作代码,见上文 */
    union
    {
        struct                  /* spgMatchNode 的结果 */
        {
            int         nodeN;      /* 下降到该结点(索引从 0 开始) */
            int         levelAdd;   /* 层级增加这么多 */
            Datum       restDatum;  /* 新的叶子 datum */
        }           matchNode;
        struct                  /* spgAddNode 的结果 */
        {
            Datum       nodeLabel;  /* 新结点的标签 */
            int         nodeN;      /* 在哪里插入它(索引从 0 开始) */
        }           addNode;
        struct                  /* spgSplitTuple 的结果 */
        {
            /* 构造只有一个子元组的新上层内部元组所需的信息 */
            bool        prefixHasPrefix;    /* 元组应有前缀? */
            Datum       prefixPrefixDatum;  /* 如果有,前缀值 */
            int         prefixNNodes;       /* 结点数 */
            Datum      *prefixNodeLabels;   /* 它们的标签(或为 NULL 表示
                                             * 没有标签) */
            int         childNodeN;         /* 哪个结点接收子元组 */

            /* 构造包含所有旧结点的新下层内部元组所需的信息 */
            bool        postfixHasPrefix;   /* 元组应有前缀? */
            Datum       postfixPrefixDatum; /* 如果有,前缀值 */
        }           splitTuple;
    }           result;
} spgChooseOut;

datum 是将要插入索引的、类型为 spgConfigIn.attType 的原始 datum。leafDatum 是一个类型为 spgConfigOut.leafType 的值;如果提供了 compress 方法,它最初是将 datum 交给 compress 的结果,否则与 datum 相同。如果 choosepicksplit 方法修改了它,那么在树的更低层级中 leafDatum 可能会发生变化。当插入搜索到达 叶子页时,leafDatum 的当前值将存储在新创 建的叶子元组中。level 是当前内部元组的层 级,根层为零。若当前内部元组被标记为包含多个等价结点(参见 Section 64.4.3),则 allTheSame 为真。若当前内部元组包含前缀, 则 hasPrefix 为真;若是如此, 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 分配的数组,其中保存这些结点的标签;如果不需要结点标签,则设为 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;        /* 叶子元组的数量 */
    Datum      *datums;         /* 它们的 datum(长度为 nTuples 的数组) */
    int         level;          /* 当前层级(从零开始计) */
} spgPickSplitIn;

typedef struct spgPickSplitOut
{
    bool        hasPrefix;      /* 新内部元组应有前缀? */
    Datum       prefixDatum;    /* 如果有,前缀值 */

    int         nNodes;         /* 新内部元组的结点数 */
    Datum      *nodeLabels;     /* 它们的标签(或为 NULL 表示无标签) */

    int        *mapTuplesToNodes;   /* 每个叶子元组对应的结点索引 */
    Datum      *leafTupleDatums;    /* 每个新叶子元组中存储的 datum */
} spgPickSplitOut;

nTuples 是提供的叶子元组数量。 datums 是由它们的 datum 值组成的数组,类 型为 spgConfigOut.leafTypelevel 是这些叶子元组共享的当前层级,它将 成为新内部元组的层级。

hasPrefix 设为指示新的内部元组是否应 有前缀;若应有,则将 prefixDatum 设为该 前缀值。将 nNodes 设为新内部元组将包含 的结点数,并将 nodeLabels 设为这些结点 的标签值数组;如果不需要结点标签,则设为 NULL。将 mapTuplesToNodes 设为一个数组,其中给出 每个叶子元组应分配到的结点索引(从零开始)。将 leafTupleDatums 设为要存储在新叶子元组 中的值数组(如果操作符类不会在层级之间修改 datum,这些值就与输入的 datums 相同)。注意, picksplit 函数负责为 nodeLabelsmapTuplesToNodesleafTupleDatums 数组执行 palloc。

如果提供了多于一个叶子元组,则期望 picksplit 函数把它们划分到多于一个结点中;否 则就无法把叶子元组拆分到多个页上,而这正是此操作的最终目的。因此, 如果 picksplit 最终把所有叶子元组都放进同一个 结点,SP-GiST 核心代码会覆盖这一决定,生成一个内部元组,并将叶子元 组随机分配到多个标签相同的结点上。这样的元组会被标记为 allTheSame,以表明发生了这种情况。 chooseinner_consistent 函数必须对这种内部元组做出恰当处理。更多信息见 Section 64.4.3

只有在 config 函数将 longValuesOK 设为真,并且提供了一个大于一 页的输入值时,picksplit 才会应用到单个叶子元 组。在这种情况下,这个操作的目的是剥离一个前缀,并产生一个新的、更短 的叶子 datum 值。该调用会重复进行,直到生成足够短、能够放入一页的叶 子 datum。更多信息见 Section 64.4.1

inner_consistent #

在树搜索期间返回需要继续跟随的一组结点(分支)。

该函数的 SQL 声明必须如下所示:

CREATE FUNCTION my_inner_consistent(internal, internal) RETURNS void ...

第一个参数是一个指向 spgInnerConsistentIn C 结构的指针,其中包含该函数的输入数据。第二个参数是一个指向 spgInnerConsistentOut C 结构的指针,函数必 须将结果数据填入其中。

typedef struct spgInnerConsistentIn
{
    ScanKey     scankeys;       /* 操作符和比较值的数组 */
    ScanKey     orderbys;       /* 排序操作符和比较值的数组 */
    int         nkeys;          /* scankeys 数组的长度 */
    int         norderbys;      /* orderbys 数组的长度 */

    Datum       reconstructedValue;     /* 在父元组处重构的值 */
    void       *traversalValue; /* 操作符类特定的遍历值 */
    MemoryContext traversalMemoryContext;   /* 将新的遍历值放在这里 */
    int         level;          /* 当前层级(从零开始计) */
    bool        returnData;     /* 必须返回原始数据? */

    /* 来自当前内部元组的数据 */
    bool        allTheSame;     /* 元组被标记为 all-the-same? */
    bool        hasPrefix;      /* 元组有前缀? */
    Datum       prefixDatum;    /* 如果有,前缀值 */
    int         nNodes;         /* 内部元组中的结点数 */
    Datum      *nodeLabels;     /* 结点标签值(如果没有则为 NULL) */
} spgInnerConsistentIn;

typedef struct spgInnerConsistentOut
{
    int         nNodes;         /* 需要访问的子结点数 */
    int        *nodeNumbers;    /* 它们在结点数组中的索引 */
    int        *levelAdds;      /* 对每个结点层级增加这么多 */
    Datum      *reconstructedValues;    /* 关联的重构值 */
    void      **traversalValues;        /* 操作符类特定的遍历值 */
    double    **distances;              /* 关联距离 */
} spgInnerConsistentOut;

长度为 nkeysscankeys 数组描述索引搜索条件。这些条件 用 AND 组合 — 只有满足全部条件的索引项才是我们关心的。(注 意, nkeys = 0 表示所有索引项都满足该查 询。)通常 consistent 函数只关心每个数组元素的 sk_strategysk_argument 字段,它们分别给出可索引操 作符和比较值。特别地,无需检查 sk_flags 以判断比较值是否为 NULL,因为 SP-GiST 核心代码会过滤掉此类条件。长 度为 norderbysorderbys 数组以相同方式描述排序操作符 (如果有)。reconstructedValue 是为父元 组重建的值;在根层,或者当 inner_consistent 函数没有在父层提供该值时,它为 (Datum) 0traversalValue 是指向任意遍历数据的指针, 这些数据由上一次针对父索引元组调用 inner_consistent 时向下传递;在根层时则为 NULL。traversalMemoryContext 是存放输出 遍历值(见下文)的内存上下文。level 是 当前内部元组的层级,根层为零。如果本查询需要重建数据,则 returnDatatrue; 只有在 config 函数声明了 canReturnData 时才会如此。若当前内部元组 被标记为 all-the-same,则 allTheSame 为真;在这种情况下,所有结点 都具有相同的标签(如果有),因此要么全部匹配该查询,要么全部不匹配 (参见 Section 64.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 数组执行 palloc。不过, traversalValues 数组所指向的任何输出遍历 值都应在 traversalMemoryContext 中分配。 每个遍历值都必须是单独 palloc 的一个块。

leaf_consistent #

如果叶子元组满足查询,则返回 true

该函数的 SQL 声明必须如下所示:

CREATE FUNCTION my_leaf_consistent(internal, internal) RETURNS bool ...

第一个参数是一个指向 spgLeafConsistentIn C 结构的指针,其中包含该函数的输入数据。第二个参数是一个指向 spgLeafConsistentOut C 结构的指针,函数必 须将结果数据填入其中。

typedef struct spgLeafConsistentIn
{
    ScanKey     scankeys;       /* 操作符和比较值的数组 */
    ScanKey     orderbys;       /* 排序操作符和比较值的数组 */
    int         nkeys;          /* scankeys 数组的长度 */
    int         norderbys;      /* orderbys 数组的长度 */

    Datum       reconstructedValue;     /* 在父元组处重构的值 */
    void       *traversalValue; /* 操作符类特定的遍历值 */
    int         level;          /* 当前层级(从零开始计) */
    bool        returnData;     /* 必须返回原始数据? */

    Datum       leafDatum;      /* 叶子元组中的 datum */
} spgLeafConsistentIn;

typedef struct spgLeafConsistentOut
{
    Datum       leafValue;        /* 重构出的原始数据(如果有) */
    bool        recheck;          /* 如果必须重新检查操作符则设为真 */
    bool        recheckDistances; /* 如果必须重新检查距离则设为真 */
    double     *distances;        /* 关联距离 */
} spgLeafConsistentOut;

长度为 nkeysscankeys 数组描述索引搜索条件。这些条件 用 AND 组合在一起 — 只有满足全部条件的索引项才满足该查询。(注 意 nkeys = 0 表示所有索引项都满足该查 询。)通常 consistent 函数只关心每个数组元素的 sk_strategysk_argument 字段,它们分别给出可索引操 作符和比较值。特别地,无需检查 sk_flags 以判断比较值是否为 NULL,因为 SP-GiST 核心代码会过滤掉此类条件。长 度为 norderbysorderbys 数组以相同方式描述排序操作符。 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() 机制传递给所有支持方法。

提交更正

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