Postgresql 中文操作指南
68.3. Extensibility #
从传统意义上说,实现一种新的索引访问方法意味着大量的困难工作。必须了解数据库的内部工作原理,例如锁管理器和预写日志。GiST 接口具有较高的抽象级别,只需要访问方法的实现者实现正在访问的数据类型的语义。GiST 层本身负责并发、记录以及 tree 结构的搜索。
此扩展性不应与其它标准搜索树在可处理数据方面的扩展性相混淆。例如,PostgreSQL 支持可扩展的 B-Tree 和哈希索引。这意味着你可以使用 PostgreSQL 在任何想要的数据类型上构建 B-Tree 或哈希。但 B-Tree 仅支持范围谓词(<、=、>),而哈希索引仅支持相等性查询。
因此,如果你使用 PostgreSQL B-Tree 编制索引(比如图像集合),你仅能够发布诸如“图像 x 是否等于图像 y”、“图像 x 是否小于图像 y”和“图像 x 是否大于图像 y”之类的查询。根据你在此上下文中如何定义“等于”、“小于”和“大于”,这可能有用。但通过使用基于 GiST 的索引,你可以创建提出特定领域问题的方法,比如“找到所有马的图像”或“找到所有曝光过度图像”。
让 GiST 访问方法正常运行所需要的一切就是实现几个用户定义的方法,这些方法定义了树中键的行为。当然,这些方法必须非常巧妙才能支持巧妙的查询,但对于所有标准查询(B-Tree、R-Tree 等),它们相对简单。总之,GiST 将扩展性与通用性、代码重用和清晰的界面相结合。
GiST 的索引操作符类必须提供五种方法,可以选择提供六种方法。通过正确实现 same、consistent 和 union 方法来确保索引的正确性,而索引的效率(大小和速度)将取决于 penalty 和 picksplit 方法。两种可选方法是 compress 和 decompress,它们允许索引具有与其索引数据类型不同的内部 tree 数据。叶子应为已编制索引的数据类型,而其它树节点可以是任何 C 结构(但你仍然必须在此遵循 PostgreSQL 数据类型规则,有关大小可变的数据,请参阅 varlena)。如果 tree 的内部数据类型存在于 SQL 层,则可以使用 CREATE OPERATOR CLASS 命令的 STORAGE 选项。如果操作符类希望支持已排序扫描(最近邻搜索),则可选的第八种方法是 distance。如果操作符类希望支持仅索引扫描(除非省略 compress 方法除外),则需要可选的第九种方法 fetch。如果操作符类有用户指定的参数,则需要可选的第十种方法 options。可选的第十一种方法 sortsupport 用于加快构建 GiST 索引。
-
consistent
-
给定索引条目 p 和查询值 q,此函数确定索引条目是否与查询“一致”,即谓词“indexed_column indexable_operator q”对于任何由索引条目表示的行是否为 true。对于叶索引条目,这等效于测试可索引条件,而对于内部 tree 节点,这决定是否有必要扫描由该 tree 节点表示的索引的子树。当结果为 true 时,还必须返回 recheck 标志。这指示谓词肯定为 true 还是可能为 true。如果 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 的数据类型可能因运算符而异,因为它将是运算符右边的任何类型,这可能不同于左侧的已编制索引数据类型。(以上代码框架假定只有一种类型是可行的;如果不是,则获取 query 参数值将取决于运算符。)建议 SQL 对 consistent 函数的声明使用 opclass 的已编制索引数据类型用于 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 函数应返回指向新_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)
{
/* 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
-
-
将数据项的存储表示转换为操作符类的其他 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; -- 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 函数不仅返回 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;
/* 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);
}
-
请注意, picksplit 函数的结果通过修改传入的 v 结构来传递。忽略返回值本身,尽管将 v 的地址传递回来很常见。
-
与 penalty 类似,picksplit 函数对于索引的良好性能至关重要。设计合适的 penalty 和 picksplit 实现方法是实现性能良好的 GiST 索引时的挑战所在。
-
same
-
-
如果两个索引条目相同,则返回 true;否则返回 false。(“索引条目”是索引存储类型的值,不一定是原始索引列的类型。)
-
函数的 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;
/*
* determine return value as a function of strategy, key and 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 字段包含压缩形式的非空叶数据。返回值是另一个 GISTENTRY 结构,其 key 字段包含相同数据,但采用其原始未压缩格式。如果操作类别的压缩功能对叶条目不执行任何操作,则 fetch 方法可以按原样返回参数。或者,如果操作类别没有压缩功能, 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));
/*
* Convert 'fetched_data' into the a Datum of the original datatype.
*/
/* fill *retval from fetched_data. */
gistentryinit(*retval, PointerGetDatum(converted_datum),
entry->rel, entry->page, entry->offset, FALSE);
PG_RETURN_POINTER(retval);
}
-
如果压缩方法对于叶条目来说有损,则操作类将无法支持仅索引扫描,并且不得定义 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 header (do not touch directly!) */
int int_param; /* integer parameter */
double real_param; /* real parameter */
MyEnumType enum_param; /* enum parameter */
int str_param; /* string parameter */
} MyOptionsStruct;
/* String representation of enum values */
static relopt_enum_elt_def myEnumValues[] =
{
{"on", MY_ENUM_ON},
{"off", MY_ENUM_OFF},
{"auto", MY_ENUM_AUTO},
{(const char *) NULL} /* list terminator */
};
static char *str_param_default = "default";
/*
* Sample validator: checks that string is not longer than 8 bytes.
*/
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")));
}
/*
* Sample filler: switches characters to lower case.
*/
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;
/*
* Normally, when opclass contains 'options' method, then options are always
* passed to support functions. However, if you add 'options' method to
* existing opclass, previously defined indexes have no options, so the
* check is required.
*/
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);
}
/* the rest implementation of support function */
}
-
由于 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 结构的一个指针。函数至少必须填充其比较器字段。该比较器采用三个参数:两个要比较的数据,和一个指向 SortSupport 结构的指针。这些数据是以索引中存储的格式存在的两个索引值;即,以 compress 方法返回的格式存在。完整 API 在 src/include/utils/sortsupport.h 中进行了定义。
-
C 模块中的匹配代码可以遵循以下框架:
PG_FUNCTION_INFO_V1(my_sortsupport);
static int
my_fastcmp(Datum x, Datum y, SortSupport ssup)
{
/* establish order between x and y by computing some sorting value z */
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 将在处理每个元组后重置。因此,不必非常担心使用 pfree 释放 palloc 过的所有内容。但在某些情况下,支持方法跨重复调用缓存数据会很有用。要做到这一点,请在 fcinfo→flinfo→fn_mcxt 中分配更长生命的,并在 fcinfo→flinfo→fn_extra 中保留对其的指针。此类数据将在索引操作的整个生命周期内都存在(例如,一个 GiST 索引扫描、索引构建或索引元组插入)。替换 fn_extra 值时务必使用 pfree 释放先前的值,否则内存泄漏将会在整个操作期间持续累积。