PostgreSQL 9.3.4 文档 | ||||
---|---|---|---|---|
Prev | Up | Chapter 55. GiST 索引 | Next |
在传统上,实现一种新的索引访问方法意味着很多困难的工作。开发者必须要理解数据库的内部工作,例如锁管理器和预写式日志。GiST接口有一个高层的抽象,要求访问方法实现者只实现要被访问的数据类型的语义。GiST层本身会处理并发、日志和对树结构的搜索。
这种可扩展性不应该与其他标准搜索树对于它们所处理的数据上的可扩展性混淆。例如,PostgreSQL支持可扩展的 B 树和哈希索引。也就是说你可以用PostgreSQL在任何你想要的数据类型上构建一个 B 树或哈希。但是 B 树只支持范围谓词(<、=、>),而哈希索引支持等值查询。
这样如果你用一个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
,如果操作符类希望支持有序扫描(最近邻搜索)就需要它。
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; /* * 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 模块中匹配的代码则应该遵循这样的框架:
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
实现函数应该返回一个新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) { /* 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改编成你正在转换到的指定类型。
取决于你的需要,你可能还需要在这里处理对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; -- in some cases penalty functions need not be strict
在 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
函数对于索引的好性能至关重要。设计合适的penalty
和picksplit
是实现一个好的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; /* * determine return value as a function of strategy, key and query. */ PG_RETURN_FLOAT8(retval); }
distance
函数的参数和consistent
函数的相同,不过不使用复核标志。到一个页索引项的距离必须总是被准确地判断,因为一旦元组被返回就没有办法对它们重排序。在决定到一个内部树结点距离时,允许有一些近似,只要结果绝不会超过任何子女的实际距离。因此,例如在几何应用中到一个边界框的距离通常是足够的。结果值可以是任意有限float8值(无限和负无限在内部被用来处理空值等情况,因此我们不推荐distance
函数返回这些值)。
所有的 GiST 支持方法通常都在一个短暂存在的内存上下文中被调用;就是说,每个元组被处理之后CurrentMemoryContext将被重置。因此没有必要操心释放你 palloc 的所有东西。但是,在某些情况下,一个支持方法在重复调用之间缓存数据是有用的。要这样做,将这些长期生存的数据分配在fcinfo->flinfo->fn_mcxt中,并且在fcinfo->flinfo->fn_extra中保持一个到它的指针。这种数据将在索引操作期间都存在(例如一次 GiST 索引扫描、索引构建或索引元组插入)。注意当替换一个fn_extra值时要释放之前的值,否则在操作期间该泄露会累积。