GIN代表通用倒排索引。GIN专为处理以下情况而设计:要索引的项目是复合值,要由索引处理的查询需要搜索出现在复合项目中的元素值。例如,这些项目可以是文档,查询可以是搜索包含特定单词的文档。
我们使用术语 项目 来指代要索引的复合值,使用术语 键 来指代元素值。GIN始终存储和搜索键,而不是项目值本身。
一个GIN索引存储一组(键,发布列表)对,其中 发布列表 是出现该键的行 ID 集合。同一个行 ID 可以出现在多个发布列表中,因为一个项目可以包含多个键。每个键值只存储一次,因此GIN索引对于同一键出现多次的情况非常紧凑。
GIN的通用性在于GIN访问方法代码不需要知道它加速的特定操作。相反,它使用为特定数据类型定义的自定义策略。该策略定义如何从索引项目和查询条件中提取键,以及如何确定包含查询中某些键值的行是否实际满足查询。
的优点之一是GIN允许开发具有适当访问方法的自定义数据类型,由数据类型领域专家完成,而不是数据库专家。这与使用GiST.
的优点非常相似。GIN在 PostgreSQL 中的实现主要由 Teodor Sigaev 和 Oleg Bartunov 维护。有关GIN的更多信息,请访问他们的 网站。
核心 PostgreSQL 发行版包括GIN操作符类,如 表 64.3 所示。(附录 F 中描述的某些可选模块提供了附加的GIN操作符类。)
表 64.3. 内置GIN操作符类
名称 | 可索引操作符 |
---|---|
array_ops |
&& (anyarray,anyarray) |
@> (anyarray,anyarray) |
|
<@ (anyarray,anyarray) |
|
= (anyarray,anyarray) |
|
jsonb_ops |
@> (jsonb,jsonb) |
@? (jsonb,jsonpath) |
|
@@ (jsonb,jsonpath) |
|
? (jsonb,text) |
|
?| (jsonb,text[]) |
|
?& (jsonb,text[]) |
|
jsonb_path_ops |
@> (jsonb,jsonb) |
@? (jsonb,jsonpath) |
|
@@ (jsonb,jsonpath) |
|
tsvector_ops |
@@ (tsvector,tsquery) |
对于类型 jsonb
的两个操作符类,jsonb_ops
是默认的。 jsonb_path_ops
支持的操作符较少,但对于这些操作符,它提供了更好的性能。有关详细信息,请参见 第 8.14.4 节。
的优点非常相似。GIN接口具有很高的抽象级别,仅要求访问方法实现者实现被访问的数据类型的语义。这个GIN层本身负责并发、日志记录和搜索树结构。
要使GIN访问方法正常工作,只需要实现一些用户定义的方法,这些方法定义了树中键的行为以及键、索引项目和可索引查询之间的关系。简而言之,GIN将可扩展性与通用性、代码重用和简洁的接口结合在一起。
对于GIN,操作符类必须提供两种方法
Datum *extractValue(Datum itemValue, int32 *nkeys, bool **nullFlags)
返回一个使用 palloc 分配的键数组,给定要索引的项目。返回的键数必须存储到 *nkeys
中。如果任何键可以为 null,还可以使用 palloc 分配一个包含 *nkeys
个 bool
字段的数组,将它的地址存储到 *nullFlags
中,并根据需要设置这些 null 标志。如果所有键都为非 null,则可以将 *nullFlags
留在 NULL
(它的初始值)处。如果项目不包含任何键,则返回值可以为 NULL
。
Datum *extractQuery(Datum query, int32 *nkeys, StrategyNumber n, bool **pmatch, Pointer **extra_data, bool **nullFlags, int32 *searchMode)
返回一个使用 palloc 分配的键数组,给定要查询的值;也就是说,query
是可索引操作符右侧的值,该操作符的左侧是索引列。 n
是操作符在操作符类中的策略编号(请参见 第 36.16.2 节)。通常,extractQuery
需要咨询 n
以确定 query
的数据类型以及它应该使用的方法来提取键值。返回的键数必须存储到 *nkeys
中。如果任何键可以为 null,还可以使用 palloc 分配一个包含 *nkeys
个 bool
字段的数组,将它的地址存储到 *nullFlags
中,并根据需要设置这些 null 标志。如果所有键都为非 null,则可以将 *nullFlags
留在 NULL
(它的初始值)处。如果 query
不包含任何键,则返回值可以为 NULL
。
searchMode
是一个输出参数,它允许 extractQuery
指定有关如何进行搜索的详细信息。如果 *searchMode
设置为 GIN_SEARCH_MODE_DEFAULT
(这是它在调用之前初始化的值),则只有至少匹配一个返回键的项目才会被视为候选匹配项。如果 *searchMode
设置为 GIN_SEARCH_MODE_INCLUDE_EMPTY
,则除了包含至少一个匹配键的项目外,不包含任何键的项目也将被视为候选匹配项。(此模式对于实现子集操作符非常有用,例如。)如果 *searchMode
设置为 GIN_SEARCH_MODE_ALL
,则索引中的所有非 null 项目都将被视为候选匹配项,无论它们是否匹配任何返回的键。(此模式比其他两种选择慢得多,因为它需要扫描整个索引,但这对于正确实现特殊情况可能很有必要。如果大多数情况下需要此模式的操作符可能不是 GIN 操作符类的合适选择。)用于设置此模式的符号在 access/gin.h
中定义。
pmatch
是一个输出参数,用于支持部分匹配。要使用它,extractQuery
必须分配一个包含 *nkeys
个 bool
的数组,并将它的地址存储到 *pmatch
中,然后将它想要存储的内容存储到各个指针中。该变量在调用之前初始化为 NULL
,因此此参数可以被不支持部分匹配的操作符类忽略。如果设置了 *pmatch
,则整个数组将传递给 consistent
方法,并将适当的元素传递给 comparePartial
方法。
extra_data
是一个输出参数,它允许 extractQuery
将附加数据传递给 consistent
和 comparePartial
方法。要使用它,extractQuery
必须分配一个包含 *nkeys
个指针的数组,并将它的地址存储到 *extra_data
中,然后将它想要存储的内容存储到各个指针中。该变量在调用之前初始化为 NULL
,因此此参数可以被不需要额外数据的操作符类忽略。如果设置了 *extra_data
,则整个数组将传递给 consistent
方法,并将适当的元素传递给 comparePartial
方法。
操作符类还必须提供一个函数来检查索引项目是否匹配查询。它有两种形式:布尔 consistent
函数和三元 triConsistent
函数。 triConsistent
涵盖了这两个函数的功能,因此仅提供 triConsistent
就足够了。但是,如果布尔变体的计算成本明显更低,则提供这两个函数会更有优势。如果只提供布尔变体,则一些依赖于在获取所有键之前否定索引项目的优化将被禁用。
bool consistent(bool check[], StrategyNumber n, Datum query, int32 nkeys, Pointer extra_data[], bool *recheck, Datum queryKeys[], bool nullFlags[])
如果索引项目满足策略编号为 n
的查询操作符,则返回 true(或者可能满足它,如果返回了重新检查指示)。此函数没有直接访问索引项目的 value,因为GIN不显式存储项目。相反,可用的知识是关于从查询中提取的哪些键值出现在给定索引项目中。check
数组的长度为 nkeys
,它与之前由 extractQuery
为此 query
数据返回的键数量相同。如果索引项包含相应的查询键,则 check
数组的每个元素都为真,即如果 (check[i] == true),则 extractQuery
结果数组的第 i 个键存在于索引项中。原始 query
数据在 consistent
方法需要查询它时传入,queryKeys[]
和 nullFlags[]
数组也是之前由 extractQuery
返回的。extra_data
是 extractQuery
返回的额外数据数组,如果没有则为 NULL
。
当 extractQuery
在 queryKeys[]
中返回一个空键时,如果索引项包含空键,则相应的 check[]
元素为真;也就是说,check[]
的语义类似于 IS NOT DISTINCT FROM
。如果 consistent
函数需要区分常规值匹配和空匹配,则可以检查相应的 nullFlags[]
元素。
成功时,如果堆元组需要针对查询操作符重新检查,则应将 *recheck
设置为 true,如果索引测试是精确的,则应将 *recheck
设置为 false。也就是说,一个 false 返回值保证堆元组与查询不匹配;一个 true 返回值,其中 *recheck
设置为 false,保证堆元组与查询匹配;一个 true 返回值,其中 *recheck
设置为 true,意味着堆元组可能与查询匹配,因此需要通过直接针对原始索引项评估查询操作符来获取并重新检查它。
GinTernaryValue triConsistent(GinTernaryValue check[], StrategyNumber n, Datum query, int32 nkeys, Pointer extra_data[], Datum queryKeys[], bool nullFlags[])
triConsistent
与 consistent
类似,但 check
向量中的布尔值不是,每个键有三个可能的值:GIN_TRUE
、GIN_FALSE
和 GIN_MAYBE
。GIN_FALSE
和 GIN_TRUE
与常规布尔值具有相同的含义,而 GIN_MAYBE
意味着该键的存在是未知的。当存在 GIN_MAYBE
值时,函数仅当项目肯定匹配(无论索引项是否包含相应的查询键)时才应返回 GIN_TRUE
。同样,该函数必须仅当项目肯定不匹配(无论它是否包含 GIN_MAYBE
键)时才返回 GIN_FALSE
。如果结果依赖于 GIN_MAYBE
条目,即匹配不能根据已知查询键确认或反驳,则该函数必须返回 GIN_MAYBE
。
当 check
向量中没有 GIN_MAYBE
值时,GIN_MAYBE
返回值等效于在布尔 consistent
函数中设置 recheck
标志。
此外,GIN 必须有一种方法来对存储在索引中的键值进行排序。操作符类可以通过指定一个比较方法来定义排序顺序
int compare(Datum a, Datum b)
比较两个键(不是索引项!),并返回小于零、零或大于零的整数,表示第一个键是否小于、等于或大于第二个键。空键永远不会传递给此函数。
或者,如果操作符类没有提供 compare
方法,则 GIN 会查找索引键数据类型的默认 btree 操作符类,并使用它的比较函数。建议在专为单一数据类型设计的 GIN 操作符类中指定比较函数,因为查找 btree 操作符类的成本略高。但是,多态 GIN 操作符类(如 array_ops
)通常无法指定单个比较函数。
操作符类用于GIN可以选择提供以下方法
int comparePartial(Datum partial_key, Datum key, StrategyNumber n, Pointer extra_data)
将部分匹配查询键与索引键进行比较。返回一个整数,其符号表示结果:小于零表示索引键与查询不匹配,但索引扫描应继续;零表示索引键与查询匹配;大于零表示索引扫描应停止,因为不再可能匹配。提供生成部分匹配查询的操作符的策略编号 n
,以防其语义需要确定何时结束扫描。此外,extra_data
是由 extractQuery
创建的额外数据数组的相应元素,如果没有则为 NULL
。空键永远不会传递给此函数。
void options(local_relopts *relopts)
定义一组控制操作符类行为的用户可见参数。
options
函数传递一个指向 local_relopts
结构的指针,该指针需要填充一组特定于操作符类的选项。可以使用 PG_HAS_OPCLASS_OPTIONS()
和 PG_GET_OPCLASS_OPTIONS()
宏从其他支持函数访问这些选项。
由于索引值的键提取和键在GIN中的表示都是灵活的,因此它们可能取决于用户指定的参数。
为了支持 “部分匹配” 查询,操作符类必须提供 comparePartial
方法,并且它的 extractQuery
方法必须在遇到部分匹配查询时设置 pmatch
参数。有关详细信息,请参阅 第 64.4.4.2 节。
上面提到的各种 Datum
值的实际数据类型因操作符类而异。传递给 extractValue
的项目值始终是操作符类的输入类型,所有键值必须是类的 STORAGE
类型。传递给 extractQuery
、consistent
和 triConsistent
的 query
参数的类型是策略编号识别的类成员操作符的正确右输入类型。只要可以从其中提取正确类型的键值,它就不必与索引类型相同。但是,建议这三个支持函数的 SQL 声明对 query
参数使用操作符类的索引数据类型,即使实际类型可能因操作符而异。
在内部,一个GIN索引包含一个在键上构建的 B 树索引,其中每个键都是一个或多个索引项的元素(例如,数组的成员),并且叶页面中的每个元组都包含指向堆指针的 B 树的指针(一个 “发布树”),或者当列表足够小以适合单个索引元组和键值时,包含一个简单的堆指针列表(一个 “发布列表”)。图 64.1 说明了 GIN 索引的这些组成部分。
从 PostgreSQL 9.1 开始,空键值可以包含在索引中。此外,对于根据 extractValue
为空或不包含任何键的索引项,占位符空值也会包含在索引中。这允许应该查找空项目的搜索这样做。
多列GIN索引是通过在复合值(列号、键值)上构建单个 B 树来实现的。不同列的键值可以是不同类型。
图 64.1. GIN 内部结构
更新一个GIN索引往往很慢,因为反向索引的本质:插入或更新一个堆行会导致对索引进行多次插入(为从索引项中提取的每个键插入一次)。GIN能够通过将新元组插入一个临时的、未排序的待处理条目列表来推迟大部分工作。当表被清理或自动分析时,或者当 gin_clean_pending_list
函数被调用时,或者如果待处理列表的大小超过 gin_pending_list_limit,则这些条目将使用在初始索引创建期间使用的相同批量插入技术移动到主GIN数据结构中。这极大地提高了GIN索引更新速度,即使考虑到额外的清理开销。此外,开销工作可以由后台进程完成,而不是在前景查询处理中完成。
这种方法的主要缺点是,搜索必须扫描待处理条目的列表,以及搜索常规索引,因此大量的待处理条目列表会显着降低搜索速度。另一个缺点是,虽然大多数更新都很快,但会导致待处理列表变得 “太大” 的更新会立即发生清理周期,因此比其他更新慢得多。正确使用自动清理可以最大程度地减少这两个问题。
如果一致的响应时间比更新速度更重要,则可以通过关闭 fastupdate
存储参数来禁用待处理条目的使用GIN索引。有关详细信息,请参阅 CREATE INDEX。
GIN 可以支持 “部分匹配” 查询,在这种查询中,查询没有确定一个或多个键的精确匹配,但可能的匹配落在键值范围(由 compare
支持方法确定的键排序顺序)的合理范围内。extractQuery
方法没有返回要精确匹配的键值,而是返回要搜索的范围的下限,并将 pmatch
标志设置为 true。然后使用 comparePartial
方法扫描键范围。comparePartial
必须对匹配的索引键返回零,对仍在要搜索的范围内的非匹配项返回小于零,或者如果索引键超过可能匹配的范围,则返回大于零。
插入到一个GIN索引中可能会很慢,因为每个项目可能会插入许多键。因此,对于对表的批量插入,建议先删除 GIN 索引,并在完成批量插入后重新创建它。
当 fastupdate
为GIN启用时(有关详细信息,请参阅 第 64.4.4.1 节),惩罚比未启用时少。但是,对于非常大的更新,最好还是删除并重新创建索引。
构建一个GIN索引的时间对 maintenance_work_mem
设置非常敏感;在索引创建期间,不要吝啬工作内存。
在一系列插入现有GIN对于启用了 fastupdate
的索引,系统会在待处理条目列表的大小超过 gin_pending_list_limit
时清理该列表。为了避免观察到的响应时间出现波动,最好在后台(例如,通过 autovacuum)清理待处理条目列表。可以通过增加 gin_pending_list_limit
或使 autovacuum 更加积极来避免前台清理操作。但是,扩大清理操作的阈值意味着,如果发生前台清理,则清理操作将花费更长时间。
可以通过更改存储参数来覆盖单个 GIN 索引的 gin_pending_list_limit
,这允许每个 GIN 索引拥有自己的清理阈值。例如,可以只为更新频繁的 GIN 索引增加阈值,而为其他索引降低阈值。
开发GIN索引的主要目标是在 PostgreSQL 中创建对高度可扩展的全文本搜索的支持,并且通常情况下,全文本搜索会返回非常大的结果集。此外,这通常发生在查询包含非常频繁的单词时,因此大的结果集甚至没有用。由于从磁盘读取许多元组并对它们进行排序可能需要很长时间,因此这对于生产来说是不可接受的。(请注意,索引搜索本身非常快。)
为了便于控制此类查询的执行,GIN有一个可配置的返回行数的软上限:gin_fuzzy_search_limit
配置参数。默认情况下,它设置为 0(表示没有限制)。如果设置了非零限制,则返回的集合是整个结果集的子集,随机选择。
“软” 表示实际返回的结果数量可能与指定的限制略有不同,具体取决于查询和系统随机数生成器的质量。
根据经验,数千级的值(例如,5000 - 20000)效果很好。
GIN假设可索引运算符是严格的。这意味着 extractValue
不会在空项目值上调用(而是会自动创建占位符索引条目),并且 extractQuery
也不会在空查询值上调用(而是假定查询不满足)。但是请注意,非空复合项目或查询值中包含的空键值是支持的。
如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符,或者需要进一步澄清,请使用 此表格 报告文档问题。