64.2. GiST 索引 #
64.2.1. 简介 #
GiST 是 Generalized Search Tree(通用搜索树)的缩写。它是一种平衡的树形访问方法,可作为实现任意索引方案的基本模板。B-树、R 树以及许多其他索引方案都可以在 GiST 中实现。
GiST 的一个优点是,它使数据类型所属领域的专家而不是数据库专家,能够开发带有适当访问方法的自定义数据类型。
这里的一些内容来自加州大学伯克利分校的 GiST 索引项目网站以及 Marcel Kornacker 的论文 Access Methods for Next-Generation Database Systems。PostgreSQL 中的 GiST 实现主要由 Teodor Sigaev 和 Oleg Bartunov 维护,他们的网站上还有更多信息。
64.2.2. 内置操作符类 #
PostgreSQL 核心发行版包含表 64.1 中所示的 GiST 操作符类。(附录 F 中描述的一些可选模块还提供了额外的 GiST 操作符类。)
表 64.1. 内置 GiST 操作符类
| 名称 | 可索引操作符 | 排序操作符 |
|---|---|---|
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) | ||
circle_ops | << (circle, circle) | <-> (circle, point) |
&< (circle, circle) | ||
&> (circle, circle) | ||
>> (circle, circle) | ||
<@ (circle, circle) | ||
@> (circle, circle) | ||
~= (circle, circle) | ||
&& (circle, circle) | ||
|>> (circle, circle) | ||
<<| (circle, circle) | ||
&<| (circle, circle) | ||
|&> (circle, circle) | ||
inet_ops | << (inet, inet) | |
<<= (inet, inet) | ||
>> (inet, inet) | ||
>>= (inet, inet) | ||
= (inet, inet) | ||
<> (inet, inet) | ||
< (inet, inet) | ||
<= (inet, inet) | ||
> (inet, inet) | ||
>= (inet, inet) | ||
&& (inet, inet) | ||
multirange_ops | = (anymultirange, anymultirange) | |
&& (anymultirange, anymultirange) | ||
&& (anymultirange, anyrange) | ||
@> (anymultirange, anyelement) | ||
@> (anymultirange, anymultirange) | ||
@> (anymultirange, anyrange) | ||
<@ (anymultirange, anymultirange) | ||
<@ (anymultirange, anyrange) | ||
<< (anymultirange, anymultirange) | ||
<< (anymultirange, anyrange) | ||
>> (anymultirange, anymultirange) | ||
>> (anymultirange, anyrange) | ||
&< (anymultirange, anymultirange) | ||
&< (anymultirange, anyrange) | ||
&> (anymultirange, anymultirange) | ||
&> (anymultirange, anyrange) | ||
-|- (anymultirange, anymultirange) | ||
-|- (anymultirange, anyrange) | ||
point_ops | |>> (point, point) | <-> (point, point) |
<< (point, point) | ||
>> (point, point) | ||
<<| (point, point) | ||
~= (point, point) | ||
<@ (point, box) | ||
<@ (point, polygon) | ||
<@ (point, circle) | ||
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) | ||
range_ops | = (anyrange, anyrange) | |
&& (anyrange, anyrange) | ||
&& (anyrange, anymultirange) | ||
@> (anyrange, anyelement) | ||
@> (anyrange, anyrange) | ||
@> (anyrange, anymultirange) | ||
<@ (anyrange, anyrange) | ||
<@ (anyrange, anymultirange) | ||
<< (anyrange, anyrange) | ||
<< (anyrange, anymultirange) | ||
>> (anyrange, anyrange) | ||
>> (anyrange, anymultirange) | ||
&< (anyrange, anyrange) | ||
&< (anyrange, anymultirange) | ||
&> (anyrange, anyrange) | ||
&> (anyrange, anymultirange) | ||
-|- (anyrange, anyrange) | ||
-|- (anyrange, anymultirange) | ||
tsquery_ops | <@ (tsquery, tsquery) | |
@> (tsquery, tsquery) | ||
tsvector_ops | @@ (tsvector, tsquery) |
出于历史原因,inet_ops 操作符类不是类型 inet 和 cidr 的默认类。要使用它,请在 CREATE INDEX 中写出该类名,例如
CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
64.2.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 在操作符类希望支持仅索引扫描时需要,除非省略了 compress 方法。可选的第十个方法 options 在操作符类具有用户指定参数时需要。可选的第十一个方法 sortsupport 用于加速构建 GiST 索引。
consistent#给定一个索引项
p和一个查询值q,该函数判断该索引项是否与该查询“一致”;也就是说,该索引项所代表的某一行是否可能使谓词“indexed_columnindexable_operatorq”为真。对于叶子索引项,这等同于测试该可索引条件;而对于内部树节点,这决定是否有必要扫描该树节点所表示的索引子树。当结果为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#将一个数据项转换成适合在索引页中物理存储的格式。如果省略
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#将数据项的存储表示转换成操作符类中其他 GiST 方法能够操作的格式。如果省略
decompress方法,就假定其他 GiST 方法可以直接处理存储的数据格式。(decompress不一定是compress方法的逆操作;特别是如果compress是有损的,那么decompress就不可能精确重建原始数据。decompress也不一定等同于fetch,因为其他 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方法可以原样返回该参数。或者,如果该操作符类没有 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函数。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()宏来访问这些选项。下面给出了 my_options() 的一个实现示例,以及其他支持函数如何使用这些参数:
typedef enum MyEnumType { MY_ENUM_ON, MY_ENUM_OFF, MY_ENUM_AUTO } MyEnumType; typedef struct { int32 vl_len_; /* varlena 首部(不要直接操作!) */ int int_param; /* 整数参数 */ double real_param; /* 实数参数 */ MyEnumType enum_param; /* 枚举参数 */ int str_param; /* 字符串参数 */ } MyOptionsStruct; /* 枚举值的字符串表示 */ static relopt_enum_elt_def myEnumValues[] = { {"on", MY_ENUM_ON}, {"off", MY_ENUM_OFF}, {"auto", MY_ENUM_AUTO}, {(const char *) NULL} /* 列表终止项 */ }; static char *str_param_default = "default"; /* * 验证器示例:检查字符串长度不超过 8 字节。 */ static void validate_my_string_relopt(const char *value) { if (strlen(value) > 8) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("str_param must be at most 8 bytes"))); } /* * 填充函数示例:将字符转换为小写。 */ static Size fill_my_string_relopt(const char *value, void *ptr) { char *tmp = str_tolower(value, strlen(value), DEFAULT_COLLATION_OID); int len = strlen(tmp); if (ptr) strcpy((char *) ptr, tmp); pfree(tmp); return len + 1; } PG_FUNCTION_INFO_V1(my_options); Datum my_options(PG_FUNCTION_ARGS) { local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0); init_local_reloptions(relopts, sizeof(MyOptionsStruct)); add_local_int_reloption(relopts, "int_param", "integer parameter", 100, 0, 1000000, offsetof(MyOptionsStruct, int_param)); add_local_real_reloption(relopts, "real_param", "real parameter", 1.0, 0.0, 1000000.0, offsetof(MyOptionsStruct, real_param)); add_local_enum_reloption(relopts, "enum_param", "enum parameter", myEnumValues, MY_ENUM_ON, "Valid values are: \"on\", \"off\" and \"auto\".", offsetof(MyOptionsStruct, enum_param)); add_local_string_reloption(relopts, "str_param", "string parameter", str_param_default, &validate_my_string_relopt, &fill_my_string_relopt, offsetof(MyOptionsStruct, str_param)); PG_RETURN_VOID(); } PG_FUNCTION_INFO_V1(my_compress); Datum my_compress(PG_FUNCTION_ARGS) { int int_param = 100; double real_param = 1.0; MyEnumType enum_param = MY_ENUM_ON; char *str_param = str_param_default; /* * 通常,操作符类包含 'options' 方法时,总会把选项传给支持函数。 * 但是,如果把 'options' 方法添加到已有操作符类中, * 先前定义的索引没有选项,因此需要进行检查。 */ if (PG_HAS_OPCLASS_OPTIONS()) { MyOptionsStruct *options = (MyOptionsStruct *) PG_GET_OPCLASS_OPTIONS(); int_param = options->int_param; real_param = options->real_param; enum_param = options->enum_param; str_param = GET_STRING_RELOPTION(options, str_param); } /* 支持函数的其余实现 */ }由于 GiST 中键的表示是灵活的,它可能依赖于用户指定的参数。例如,可以指定键签名的长度。参见
gtsvector_options()。sortsupport#返回一个比较器函数,用某种能够保持局部性的方式对数据进行排序。它由
CREATE INDEX和REINDEX命令使用。所创建索引的质量取决于该比较器函数确定的排序顺序在多大程度上保持了输入的局部性。sortsupport方法是可选的。如果未提供,CREATE INDEX会通过使用penalty和picksplit函数将每个元组插入树中来构建索引,这会慢得多。该函数的 SQL 声明必须如下所示:
CREATE OR REPLACE FUNCTION my_sortsupport(internal) RETURNS void AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
参数是一个指向
SortSupport结构体的指针。至少,该函数必须填写其中的comparator字段。比较器接受三个参数:两个要比较的 Datum,以及一个指向SortSupport结构体的指针。这两个 Datum 就是两个被索引的值,其格式与它们在索引中的存储格式相同;也就是说,是compress方法返回的格式。完整的 API 定义在src/include/utils/sortsupport.h中。而 C 模块中的对应代码则可以遵循如下框架:
PG_FUNCTION_INFO_V1(my_sortsupport); static int my_fastcmp(Datum x, Datum y, SortSupport ssup) { /* 通过计算某个排序值 z 来确定 x 和 y 的顺序 */ int z1 = ComputeSpatialCode(x); int z2 = ComputeSpatialCode(y); return z1 == z2 ? 0 : z1 > z2 ? 1 : -1; } Datum my_sortsupport(PG_FUNCTION_ARGS) { SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0); ssup->comparator = my_fastcmp; PG_RETURN_VOID(); }
所有 GiST 支持方法通常都在短生命周期的内存上下文中被调用;也就是说,每处理完一个元组,CurrentMemoryContext 都会被重置。因此通常无需过分担心释放所有通过 palloc 分配的内容。不过,在某些情况下,让支持方法在重复调用之间缓存数据是有用的。要做到这一点,可将寿命更长的数据分配在 fcinfo->flinfo->fn_mcxt 中,并在 fcinfo->flinfo->fn_extra 中保存指向它的指针。这类数据会在一次索引操作期间存活(例如一次 GiST 索引扫描、索引构建或索引元组插入)。在替换 fn_extra 值时要注意 pfree 旧值,否则泄漏会在整个操作期间不断累积。
64.2.4. 实现 #
64.2.4.1. GiST 索引构建方法 #
构建 GiST 索引最简单的方法就是把所有项逐个插入。这对于大型索引往往很慢,因为如果索引元组分散在整个索引中,而索引又大到无法放入缓存,就需要大量随机 I/O。PostgreSQL 支持两种用于 GiST 索引初始构建的替代方法:排序和缓冲模式。
只有当索引使用的每个操作符类都提供了 sortsupport 函数时,排序方法才可用,如第 64.2.3 节所述。如果满足这一条件,这通常是最好的方法,因此默认使用它。
缓冲方法的做法是先不把元组直接插入索引。对于非有序数据集,它可以显著减少所需的随机 I/O 量。对于顺序良好的数据集,收益较小或者根本没有,因为一次只有少量页面会接收新元组,而这些页面即使整个索引放不进缓存,也能够放进缓存。
缓冲方法比简单方法更频繁地调用 penalty 函数,这会消耗一些额外的 CPU 资源。此外,缓冲区需要临时磁盘空间,最多可达最终索引的大小。缓冲还可能正面或负面地影响最终索引的质量。这种影响取决于多种因素,例如输入数据的分布以及操作符类的实现。
如果不能排序,那么默认情况下,当索引大小达到 effective_cache_size 时,GiST 索引构建会切换到缓冲方法。也可以通过 CREATE INDEX 命令的 buffering 参数手工强制启用或禁止缓冲。默认行为在大多数情况下都不错,但如果输入数据是有序的,关闭缓冲模式可能会略微加快构建速度。
64.2.5. 示例 #
PostgreSQL 源代码发行包包含了若干使用 GiST 实现的索引方法示例。核心系统目前提供了文本检索支持(为 tsvector 和 tsquery 建立索引),并为某些内置几何数据类型提供了与 R 树等价的功能(见 src/backend/access/gist/gistproc.c)。下列 contrib 模块中也包含 GiST 操作符类:
报告文档问题
阅读 上游文档. 通过 PostgreSQL 文档反馈表单.