选择 打开 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

受支持版本: 16 / 15 / 14
不受支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0
历史版本PostgreSQL 9.1 已于 2016 年 10 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本手册首页

53.3. 实现 #

一个GiST索引操作符类必须提供七个方法,另外还有一个可选方法。通过正确实现sameconsistentunion方法可以保证索引的正确性,而索引的效率(大小与速度)则取决于penaltypicksplit方法。另外两个基本方法是compressdecompress,它们允许索引的内部树数据使用与其所索引数据不同的类型。叶子必须是被索引数据类型,而其他树节点可以是任意 C 结构体(但这里仍必须遵守PostgreSQL的数据类型规则,关于变长数据可参见varlena)。如果树的内部数据类型在 SQL 层存在,可以使用CREATE OPERATOR CLASS命令的STORAGE选项。可选的第八个方法是distance,若操作符类希望支持有序扫描(最近邻搜索),则需要它。

consistent

给定一个索引项p和一个查询值q,该函数判断该索引项是否与该查询一致;也就是说,该索引项所代表的某一行是否可能使谓词indexed_column indexable_operator q为真。对于叶子索引项,这等同于测试该可索引条件;而对于内部树节点,这决定是否有必要扫描该树节点所表示的索引子树。当结果为true时,还必须返回一个recheck标志。它表示该谓词是确定为真,还是仅可能为真。如果recheck = false,则该索引已经精确测试了谓词条件;如果recheck = true,则该行只是候选匹配。在这种情况下,系统会自动针对实际行值计算indexable_operator,以判断它是否真的匹配。这种约定使GiST能够同时支持无损和有损的索引结构。

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

CREATE OR REPLACE FUNCTION my_consistent(internal, data_type, smallint, oid, internal)
RETURNS bool
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;

而 C 模块中的对应代码则可以遵循如下框架:

Datum       my_consistent(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(my_consistent);

Datum
my_consistent(PG_FUNCTION_ARGS)
{
    GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
    data_type  *query = PG_GETARG_DATA_TYPE_P(1);
    StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
    /* Oid subtype = PG_GETARG_OID(3); */
    bool       *recheck = (bool *) PG_GETARG_POINTER(4);
    data_type  *key = DatumGetDataType(entry->key);
    bool        retval;

    /*
     * 根据 strategy、key 和 query 确定返回值。
     *
     * 使用 GIST_LEAF(entry) 判断当前调用位于索引树的哪个位置。
     * 例如,支持 = 操作符时这很有用(可以在非叶节点检查
     * union() 是否非空,在叶节点检查是否相等)。
     */

    *recheck = true;        /* 如果检查是精确的,则为 false */

    PG_RETURN_BOOL(retval);
}

这里,key是索引中的一个元素,而query是在该索引中查找的值。StrategyNumber参数指示应用的是操作符类中的哪个操作符,它对应于CREATE OPERATOR CLASS命令中的某个操作符编号。取决于你在该类中包含了哪些操作符,query的数据类型可能会随操作符而变化,但上面的框架假设它不会变化。

union

该方法用于汇总树中的信息。给定一组项,该函数生成一个新的索引项,用来表示所有给定项。

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

CREATE OR REPLACE FUNCTION my_union(internal, internal)
RETURNS internal
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;

而 C 模块中的对应代码则可以遵循如下框架:

Datum       my_union(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(my_union);

Datum
my_union(PG_FUNCTION_ARGS)
{
    GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
    GISTENTRY  *ent = entryvec->vector;
    data_type  *out,
               *tmp,
               *old;
    int         numranges,
                i = 0;

    numranges = entryvec->n;
    tmp = DatumGetDataType(ent[0].key);
    out = tmp;

    if (numranges == 1)
    {
        out = data_type_deep_copy(tmp);

        PG_RETURN_DATA_TYPE_P(out);
    }

    for (i = 1; i < numranges; i++)
    {
        old = out;
        tmp = DatumGetDataType(ent[i].key);
        out = my_union_implementation(out, tmp);
    }

    PG_RETURN_DATA_TYPE_P(out);
}

如你所见,在这个框架里,我们处理的是一种满足union(X, Y, Z) = union(union(X, Y), Z)的数据类型。对于不满足这一性质的数据类型,只需在这个GiST支持方法中实现正确的 union 算法即可。

union实现函数应返回一个指向新近通过palloc()分配的内存的指针。不能原样返回输入值。

compress

将一个数据项转换成适合在索引页中物理存储的格式。

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

CREATE OR REPLACE FUNCTION my_compress(internal)
RETURNS internal
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;

而 C 模块中的对应代码则可以遵循如下框架:

Datum       my_compress(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(my_compress);

Datum
my_compress(PG_FUNCTION_ARGS)
{
    GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
    GISTENTRY  *retval;

    if (entry->leafkey)
    {
        /* 将 entry->key 替换为压缩后的形式 */
        compressed_data_type *compressed_data = palloc(sizeof(compressed_data_type));

        /* 根据 entry->key 填充 *compressed_data ... */

        retval = palloc(sizeof(GISTENTRY));
        gistentryinit(*retval, PointerGetDatum(compressed_data),
                      entry->rel, entry->page, entry->offset, FALSE);
    }
    else
    {
        /* 通常无需对非叶项做任何处理 */
        retval = entry;
    }

    PG_RETURN_POINTER(retval);
}

当然,为了压缩叶子节点,你必须把compressed_data_type改成要转换成的具体类型。

根据你的需要,你可能还需要注意在其中压缩NULL值,例如像gist_circle_compress那样存储(Datum) 0

decompress

compress方法的逆操作。将数据项的索引表示转换成数据库能够操作的格式。

SQL声明必须如下所示:

CREATE OR REPLACE FUNCTION my_decompress(internal)
RETURNS internal
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;

C 模块中相应的代码可以采用以下框架:

Datum       my_decompress(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(my_decompress);

Datum
my_decompress(PG_FUNCTION_ARGS)
{
    PG_RETURN_POINTER(PG_GETARG_POINTER(0));
}

上述框架适用于不需要解压的情况。

penalty

返回一个值,指示把新项插入树中特定分支的代价。项会沿着树中penalty最小的路径插入。penalty返回的值应为非负;如果返回负值,它将被按零处理。

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

CREATE OR REPLACE FUNCTION my_penalty(internal, internal, internal)
RETURNS internal
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;  -- 某些情况下 penalty 函数不必是严格函数

而 C 模块中的对应代码则可以遵循如下框架:

Datum       my_penalty(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(my_penalty);

Datum
my_penalty(PG_FUNCTION_ARGS)
{
    GISTENTRY  *origentry = (GISTENTRY *) PG_GETARG_POINTER(0);
    GISTENTRY  *newentry = (GISTENTRY *) PG_GETARG_POINTER(1);
    float      *penalty = (float *) PG_GETARG_POINTER(2);
    data_type  *orig = DatumGetDataType(origentry->key);
    data_type  *new = DatumGetDataType(newentry->key);

    *penalty = my_penalty_implementation(orig, new);
    PG_RETURN_POINTER(penalty);
}

penalty函数对于索引的良好性能至关重要。它会在插入时用于决定在树中应沿着哪个分支向下,以便选择把新项加到哪里。在查询时,索引越平衡,查找就越快。

picksplit

当索引页必须分裂时,该函数决定页面上的哪些项留在旧页中,哪些移到新页中。

SQL 声明必须如下所示:

CREATE OR REPLACE FUNCTION my_picksplit(internal, internal)
RETURNS internal
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;

C 模块中相应的代码可以采用以下框架:

Datum       my_picksplit(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(my_picksplit);

Datum
my_picksplit(PG_FUNCTION_ARGS)
{
    GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
    OffsetNumber maxoff = entryvec->n - 1;
    GISTENTRY  *ent = entryvec->vector;
    GIST_SPLITVEC *v = (GIST_SPLITVEC *) PG_GETARG_POINTER(1);
    int         i,
                nbytes;
    OffsetNumber *left,
               *right;
    data_type  *tmp_union;
    data_type  *unionL;
    data_type  *unionR;
    GISTENTRY **raw_entryvec;

    maxoff = entryvec->n - 1;
    nbytes = (maxoff + 1) * sizeof(OffsetNumber);

    v->spl_left = (OffsetNumber *) palloc(nbytes);
    left = v->spl_left;
    v->spl_nleft = 0;

    v->spl_right = (OffsetNumber *) palloc(nbytes);
    right = v->spl_right;
    v->spl_nright = 0;

    unionL = NULL;
    unionR = NULL;

    /* Initialize the raw entry vector. */
    raw_entryvec = (GISTENTRY **) malloc(entryvec->n * sizeof(void *));
    for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i))
        raw_entryvec[i] = &(entryvec->vector[i]);

    for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i))
    {
        int         real_index = raw_entryvec[i] - entryvec->vector;

        tmp_union = DatumGetDataType(entryvec->vector[real_index].key);
        Assert(tmp_union != NULL);

        /*
         * Choose where to put the index entries and update unionL and unionR
         * accordingly. Append the entries to either v_spl_left or
         * v_spl_right, and care about the counters.
         */

        if (my_choice_is_left(unionL, curl, unionR, curr))
        {
            if (unionL == NULL)
                unionL = tmp_union;
            else
                unionL = my_union_implementation(unionL, tmp_union);

            *left = real_index;
            ++left;
            ++(v->spl_nleft);
        }
        else
        {
            /*
             * Same on the right
             */
        }
    }

    v->spl_ldatum = DataTypeGetDatum(unionL);
    v->spl_rdatum = DataTypeGetDatum(unionR);
    PG_RETURN_POINTER(v);
}

penalty一样,picksplit函数对于索引的良好性能至关重要。设计合适的penaltypicksplit实现,正是实现高性能GiST索引的难点所在。

same

如果两个索引项相同则返回真,否则返回假。

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

CREATE OR REPLACE FUNCTION my_same(internal, internal, internal)
RETURNS internal
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;

而 C 模块中的对应代码则可以遵循如下框架:

Datum       my_same(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(my_same);

Datum
my_same(PG_FUNCTION_ARGS)
{
    prefix_range *v1 = PG_GETARG_PREFIX_RANGE_P(0);
    prefix_range *v2 = PG_GETARG_PREFIX_RANGE_P(1);
    bool       *result = (bool *) PG_GETARG_POINTER(2);

    *result = my_eq(v1, v2);
    PG_RETURN_POINTER(result);
}

出于历史原因,same函数并不是直接返回一个布尔结果;相反,它必须把该标志存储到第三个参数指示的位置。

distance

给定一个索引项p和一个查询值q,该函数确定索引项与查询值之间的距离。如果操作符类包含任何排序操作符,就必须提供此函数。使用排序操作符的查询会优先返回距离值最小的索引项,因此结果必须与该操作符的语义一致。对于叶子索引项,结果仅表示到该索引项的距离;对于内部树节点,结果必须是其任意子项可能具有的最小距离。

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

CREATE OR REPLACE FUNCTION my_distance(internal, data_type, smallint, oid)
RETURNS float8
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;

而 C 模块中的对应代码则可以遵循如下框架:

Datum       my_distance(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(my_distance);

Datum
my_distance(PG_FUNCTION_ARGS)
{
    GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
    data_type  *query = PG_GETARG_DATA_TYPE_P(1);
    StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
    /* Oid subtype = PG_GETARG_OID(3); */
    data_type  *key = DatumGetDataType(entry->key);
    double      retval;

    /*
     * 根据 strategy、key 和 query 确定返回值。
     */

    PG_RETURN_FLOAT8(retval);
}

distance函数的参数与consistent函数的参数相同,只是不使用 recheck 标志。到叶子索引项的距离必须始终精确确定,因为元组一旦返回就无法再重新排序。在确定到内部树节点的距离时允许有一定近似,只要结果永不大于任一子节点的实际距离即可。因此,例如在几何应用中,到包围盒的距离通常就足够了。结果值可以是任意有限的float8值。(无穷大和负无穷在内部用于处理空值等情况,因此不建议distance函数返回这些值。)

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。