↑↓ 选择↵ 打开⌫ 切换范围完整搜索

PG.CENTER 连接 PostgreSQL 文档、百科与生态知识。由 Pigsty 维护。

支持中的版本: 16 / 15 / 14
已结束支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1
历史版本。 PostgreSQL 10 已结束支持。 2022-11-10. 请参阅 当前版本手册.

62.3. 可扩展性 #

传统上,实现一种新的索引访问方法意味着大量艰难的工作。必须理解数据库的内部机制,例如锁管理器和预写式日志。GiST 接口具有很高的抽象层次,只要求访问方法实现者实现被访问数据类型的语义。GiST 层本身会处理并发、日志记录以及树结构的搜索。

这种可扩展性不应与其他标准搜索树在可处理数据方面的可扩展性相混淆。例如,PostgreSQL 支持可扩展的 B-树和 hash 索引。这意味着你可以用 PostgreSQL 在任意数据类型上构建 B-树或 hash 索引。但 B-树只支持范围谓词(<、=、>),而 hash 索引只支持等值查询。

因此,如果你用 PostgreSQL 的 B-树为一个图像集合建立索引,你只能发出诸如“imagex 是否等于 imagey”、“imagex 是否小于 imagey”以及“imagex 是否大于 imagey”之类的查询。取决于你如何在这种上下文中定义“等于”、“小于”和“大于”,这可能仍然有用。不过,使用基于 GiST 的索引,你就可以构造出能够提出特定领域问题的查询方式,例如“找出所有马的图片”或者“找出所有曝光过度的图片”。

要让一个 GiST 访问方法运行起来,只需实现几个用户定义的方法,这些方法定义了树中键的行为。当然,要支持复杂查询,这些方法本身也必须足够巧妙;但对于所有标准查询(B-树、R 树等),它们都相对直接。简而言之,GiST 把可扩展性与通用性、代码复用以及清晰的接口结合了起来。

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

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 模块中的对应代码则可以遵循如下框架:

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 的数据类型可能会随操作符而变化,因为它将是操作符右侧的类型,而这可能不同于左侧出现的被索引数据类型。(上面的代码框架假定只可能有一种类型;如果不是这样,获取 query 参数值的方式就必须依赖于具体的操作符。)建议在 consistent 函数的 SQL 声明中,对 query 参数使用该操作符类的被索引数据类型,即使实际类型可能因为操作符不同而有所不同。

union

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

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

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

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

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 函数的结果必须是索引存储类型的值,不管该类型是什么(它可能与被索引列的类型相同,也可能不同)。union 函数应返回一个指向新近通过 palloc() 分配的内存的指针。即使没有类型变化,也不能原样返回输入值。

如上所示,union 函数的第一个 internal 参数实际上是一个 GistEntryVector 指针。第二个参数是一个指向整数变量的指针,可以忽略。(过去要求 union 函数把结果值的大小存入该变量,但现在已经不再需要。)

compress

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

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

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

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

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 改成要转换成的具体类型。

decompress

compress 方法的逆操作。将数据项的索引表示转换成操作符类中其他 GiST 方法能够操作的格式。

该 SQL 声明必须如下所示:

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

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

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 模块中的对应代码则可以遵循如下框架:

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 函数并不是直接返回一个 float 结果;相反,它必须把该值存储到第三个参数指示的位置。返回值本身会被忽略,不过通常会返回该参数所指向的地址。

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

picksplit

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

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

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

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

PG_FUNCTION_INFO_V1(my_picksplit);

Datum
my_picksplit(PG_FUNCTION_ARGS)
{
    GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
    GIST_SPLITVEC *v = (GIST_SPLITVEC *) PG_GETARG_POINTER(1);
    OffsetNumber maxoff = entryvec->n - 1;
    GISTENTRY  *ent = entryvec->vector;
    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;

    /* 初始化原始项向量。 */
    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);

        /*
         * 选择索引项的存放位置,并相应更新 unionL 和 unionR。
         * 将项追加到 v->spl_left 或 v->spl_right,
         * 同时更新计数器。
         */

        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
        {
            /*
             * 对右侧执行相同操作
             */
        }
    }

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

注意,picksplit 函数的结果是通过修改传入的 v 结构体来传递的。返回值本身会被忽略,不过通常会返回 v 的地址。

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

same

如果两个索引项相同则返回真,否则返回假。(“索引项”是索引存储类型的值,不一定是原始被索引列的类型。)

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

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

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

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, internal)
RETURNS float8
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;

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

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); */
    /* bool *recheck = (bool *) PG_GETARG_POINTER(4); */
    data_type  *key = DatumGetDataType(entry->key);
    double      retval;

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

    PG_RETURN_FLOAT8(retval);
}

distance 函数的参数与 consistent 函数的参数完全相同。

在确定距离时允许有一定近似,只要结果永不大于该项的实际距离即可。因此,例如在几何应用中,到包围盒的距离通常就足够了。对于内部树节点,返回的距离不能大于其任一子节点的距离。如果返回的距离不精确,函数必须将*recheck 设为 true。(对内部树节点则不必这样做;对它们总是假定计算结果不精确。)在这种情况下,执行器会在从堆中取出元组后计算准确距离,并在必要时重新排序这些元组。

如果距离函数对任意一个叶节点返回*recheck = true,原始排序操作符的返回类型必须是 float8 或 float4,且距离函数的结果值必须能与原始排序操作符的结果进行比较,因为执行器会同时使用距离函数结果和重新计算得到的排序操作符结果进行排序。否则,距离函数的结果值可以是任意有限的 float8 值,只要这些结果值的相对顺序与排序操作符返回的顺序一致即可。(无穷大和负无穷在内部用于处理空值等情况,因此不建议 distance 函数返回这些值。)

fetch

为了支持仅索引扫描,将数据项的压缩索引表示转换为原始数据类型。返回的数据必须是最初被索引值的精确、无损副本。

该 SQL 声明必须如下所示:

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

参数是一个指向 GISTENTRY 结构体的指针。进入该函数时,它的 key 字段包含一个压缩形式的非 NULL 叶子 datum。返回值是另一个 GISTENTRY 结构体,其中的 key 字段以原始、未压缩形式包含同一个 datum。如果该操作符类的 compress 函数对叶子项不做任何处理,fetch 方法可以原样返回该参数。

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

PG_FUNCTION_INFO_V1(my_fetch);

Datum
my_fetch(PG_FUNCTION_ARGS)
{
    GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
    input_data_type *in = DatumGetPointer(entry->key);
    fetched_data_type *fetched_data;
    GISTENTRY  *retval;

    retval = palloc(sizeof(GISTENTRY));
    fetched_data = palloc(sizeof(fetched_data_type));

    /*
     * 将 'fetched_data' 转换为原始数据类型的 Datum。
     */

    /* 根据 fetched_data 填充 *retval。 */
    gistentryinit(*retval, PointerGetDatum(converted_datum),
                  entry->rel, entry->page, entry->offset, FALSE);

    PG_RETURN_POINTER(retval);
}

如果 compress 方法对叶子项是有损的,该操作符类就不能支持仅索引扫描,并且不得定义 fetch 函数。

所有 GiST 支持方法通常都在短生命周期的内存上下文中被调用;也就是说,每处理完一个元组,CurrentMemoryContext 都会被重置。因此通常无需过分担心释放所有通过 palloc 分配的内容。不过,在某些情况下,让支持方法在重复调用之间缓存数据是有用的。要做到这一点,可将寿命更长的数据分配在 fcinfo->flinfo->fn_mcxt 中,并在 fcinfo->flinfo->fn_extra 中保存指向它的指针。这类数据会在一次索引操作期间存活(例如一次 GiST 索引扫描、索引构建或索引元组插入)。在替换 fn_extra 值时要注意 pfree 旧值,否则泄漏会在整个操作期间不断累积。

报告文档问题

阅读 上游文档. 反馈更正前请先核对 当前版本手册.