Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16

59.3. 可扩展性

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

这种可扩展性不应该与其他标准搜索树对于它们所处理的数据上的可扩展性混淆。 例如,PostgreSQL支持可扩展的 B 树和哈希索引。 也就是说你可以用PostgreSQL 在任何你想要的数据类型上构建一个 B 树或哈希。但是 B 树只支持范围谓词 (<=>), 而哈希索引支持等值查询。

这样如果你用一个PostgreSQL的 B 树索引一个图像集合, 你只能发出例如"imagex 等于 imagey 吗""imagex 小于 imagey 吗" 以及"imagex 大于 imagey 吗"的查询。取决于你如何在这种上下文中定义 "等于""小于""大于",这可能会有用。 但是,通过使用一个基于GiST的索引,你可以创建提问领域相关问题的方法, 可能是"找所有马的图片"或者"找所有曝光过度的图片"

建立一个GiST访问方法并让其运行的所有工作是实现几个用户定义的方法, 它们定义了树中键的行为。当然这些方法必须相当特别来支持特别的查询, 但是对于所有标准查询(B 树、R 树等)它们相对直接。简而言之, GiST在可扩展性之上结合了通用型、代码重用和一个干净的接口。

一个用于GiST的索引操作符类必须提供七个方法, 并且还有两个是可选的。索引的正确性由正确实现的sameconsistentunion方法保证,而索引的效率 (尺寸和速度)将依赖于penaltypicksplit方法。 剩下的两个基本方法是compressdecompress, 它们允许一个索引能对内部树数据使用一种不同于被其索引的数据的类型。 叶子是被索引的数据类型,而其他树结点可以是任何 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;

    /*
     * determine return value as a function of strategy, key and query.
     *
     * Use GIST_LEAF(entry) to know where you're called in the index tree,
     * which comes handy when supporting the = operator for example (you could
     * check for non empty union() in non-leaf nodes and equality in leaf
     * nodes).
     */

    *recheck = true;        /* or false if check is exact */

    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 模块中匹配的代码则应该遵循这样的框架:

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实现函数应该返回一个新palloc() 的内存的指针。你不能只是返回输入的内容。

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)
    {
        /* replace entry->key with a compressed version */
        compressed_data_type *compressed_data = palloc(sizeof(compressed_data_type));

        /* fill *compressed_data from entry->key ... */

        retval = palloc(sizeof(GISTENTRY));
        gistentryinit(*retval, PointerGetDatum(compressed_data),
                      entry->rel, entry->page, entry->offset, FALSE);
    }
    else
    {
        /* typically we needn't do anything with non-leaf entries */
        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;  -- in some cases penalty functions need not be strict

在 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函数对于索引的好性能是至关重要的。在插入时, 当要选择在树中的哪个位置加入新项时,这个函数有助于决定应该顺着哪个分支进行。 在查询时,索引越平衡,查找越快速。

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);
    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 模块中匹配的代码则应该遵循这样的框架:

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 模块中匹配的代码则应该遵循这样的框架:

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;

    /*
     * determine return value as a function of strategy, key and query.
     */

    PG_RETURN_FLOAT8(retval);
}

distance函数的参数和consistent函数的相同。

在决定距离时,允许有一些近似,只要结果绝不会超过项的实际距离。因此, 例如在几何应用中到一个边界框的距离通常是足够的。对于内部树节点, 返回的值必须大于到任意子节点的距离。如果返回的距离不准确, 则必须设置*recheck为真。(内部树节点无需这样做; 对于他们,计算总是假定不精确。)在这种情况下, 执行器将在从堆中提取元组之后计算准确的距离,并在必要时重新排序元组。

如果对于任何叶节点,距离函数返回*recheck = true, 则原始排序运算符的返回类型必须是float8float4, 并且距离函数的结果值必须与原始排序算子的结果值可比较, 因为执行器将使用距离函数结果和重新计算的排序-运算符结果进行排序。 否则,距离函数的结果值可以是任何有限的float8值, 只要结果值的相对顺序与排序运算符返回的顺序相匹配即可。 (无穷大和负无穷大在内部用于处理诸如null的情况, 因此不推荐distance函数返回这些值。)

fetch

将数据项的压缩索引表示转换为原始数据类型,用于仅进行索引的扫描。 返回的数据必须是原始索引值的精确,无损副本。

该函数的SQL声明必须看起来像这样:

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

参数是指向GISTENTRY结构的指针。在条目上, 其'key'字段包含其压缩形式的非空叶数据。返回值是另一个 GISTENTRY结构,其'key'字段包含原始未压缩形式中的相同数据。 如果操作符类的压缩函数对叶子项没有任何作用,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 = DatumGetP(entry->key);
    fetched_data_type *fetched_data;
    GISTENTRY  *retval;

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

    /*
     * Convert 'fetched_data' into the a Datum of the original datatype.
     */

    /* fill *retval from fetch_data. */
    gistentryinit(*retval, PointerGetDatum(converted_datum),
                  entry->rel, entry->page, entry->offset, FALSE);

    PG_RETURN_POINTER(retval);
}

如果压缩方法对于叶子项是有损的,则操作符类不能支持仅索引的扫描, 并且不能定义'fetch'函数。

所有的 GiST 支持方法通常都在一个短暂存在的内存上下文中被调用;就是说, 每个元组被处理之后CurrentMemoryContext将被重置。 因此没有必要操心释放你 palloc 的所有东西。但是,在某些情况下, 一个支持方法在重复调用之间缓存数据是有用的。要这样做, 将这些长期生存的数据分配在fcinfo->flinfo->fn_mcxt中, 并且在fcinfo->flinfo->fn_extra中保持一个到它的指针。 这种数据将在索引操作期间都存在(例如一次 GiST 索引扫描、 索引构建或索引元组插入)。注意当替换一个fn_extra 值时要释放之前的值,否则在操作期间该泄露会累积。