GIN代表通用反向索引。GIN旨在处理要编制的索引是复合值的情况,而索引要处理的查询需要搜索出现在复合项目中的元素值。例如,项目可能是文档,而查询可能是搜索包含特定单词的文档。
我们用单词 item 指代要编制索引的复合值,用单词 key 指代元素值。GIN始终存储和搜索键,而非项目值本身。
AGIN索引存储一组 (键、张贴列表) 对,其中 张贴列表 是一个键出现的行 ID 组。同一行 ID 可以出现在多个张贴列表中,因为一个项目可以包含多个键。键只存储一次,因此GIN索引针对键出现多次的情况非常紧凑。
GIN从广义上讲,GIN访问方法代码无需知道其加速的特定操作。相反,它使用为特定数据类型定义的自定义策略。策略定义如何从编制索引的项目和查询条件中提取键,以及如何确定包含某个查询中部分键值的行是否实际上满足查询。
GIN的一项优势是,它允许数据类型专家(而非数据库专家)使用适当的访问方法开发自定义数据类型。这与使用GiST.
类似GINPostgreSQL 中的GIN实现主要由 Teodor Sigaev 和 Oleg Bartunov 负责维护。关于
64.4.2. 内置运算符类 #GIN核心 PostgreSQL 发行版包括GIN运算符类,如 表 64.3 所示。(附录 F 中描述的某些可选模块提供了其他
运算符类。)GIN表 64.3. 内置
运算符类 | 名称 |
---|---|
可编制索引的运算符 |
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_ops |
@> (jsonb,jsonb) |
|
@? (jsonb,jsonpath) |
|
jsonb_path_ops |
tsvector_ops |
对于类型 jsonb
的两个操作符类,jsonb_ops
是默认值。jsonb_path_ops
支持较少操作符,但对这些操作符提供了更好的性能。有关详细信息,请参阅第 8.14.4 节。
类似GIN接口具有高度抽象性,要求访问方法实现者仅实现正在访问的数据类型的语义。该GIN层本身负责并发性、日志记录和搜索树结构。
使访问GIN方法工作的全部工作是实现一些用户定义的方法,这些方法定义树中键的行为以及键、索引项和可索引查询之间的关系。简而言之,GIN将可扩展性与普遍性、代码重用和清晰的接口相结合。
对于GIN必须提供
Datum *extractValue(Datum itemValue, int32 *nkeys, bool **nullFlags)
给定要索引的项目,返回一个 palloc 键数组。返回的键数必须存储到 *nkeys
中。如果任何键可以为空,还要 palloc 一个 *nkeys
bool
字段的数组,将其地址存储在 *nullFlags
中,并根据需要设置这些空标志。如果所有键都是非空的,则可以将 *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
中。如果任何键可以为空,还要 palloc 一个 *nkeys
bool
字段的数组,将其地址存储在 *nullFlags
中,并根据需要设置这些空标志。如果所有键都是非空的,则可以将 *nullFlags
保留为 NULL
(其初始值)。如果 query
不包含任何键,则返回值可以为 NULL
。
searchMode
是一个输出参数,允许 extractQuery
指定有关如何进行搜索的详细信息。如果 *searchMode
被设为 GIN_SEARCH_MODE_DEFAULT
(这是在调用前被初始化的值),则仅将至少匹配一个返回键的项目视为候选匹配项。如果 *searchMode
被设为 GIN_SEARCH_MODE_INCLUDE_EMPTY
,则除了包含至少一个匹配键的项目之外,不包含任何键的项目也被视为候选匹配项。(例如,此模式对于实现子集运算符非常有用。)如果 *searchMode
被设为 GIN_SEARCH_MODE_ALL
,则索引中所有非空项目都被视为候选匹配项,无论它们是否匹配任何返回键。(此模式比其他两种选择慢得多,因为它需要扫描整个索引,但可能需要正确实现特殊情况。在大多数情况下需要此模式的运算符可能不适合 GIN 运算符类。)用于设置此模式的符号在 access/gin.h
中定义。
pmatch
是在支持部分匹配时使用的输出参数。要使用它,extractQuery
必须分配一个包含 *nkeys
bool
的数组,并将它的地址存储在 *pmatch
中。如果相应的键需要部分匹配,则数组的每个元素应设为 true,否则设为 false。如果 *pmatch
被设为 NULL
,则 GIN 会认为不需要部分匹配。该变量在调用前被初始化为 NULL
,因此那些不支持部分匹配的运算符类可以简单地忽略此参数。
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。此函数无法直接访问索引项的值,因为GIN不会明确地存储项。相反,可以获取有关从查询中提取的键值出现在给定索引项中的知识。check
数组的长度为 nkeys
,与该 query
数据之前由 extractQuery
返回的键数相同。check
数组的每个元素都为 true,如果索引项包含对应的查询键,即,如果 (check[i] == true),则 extractQuery
结果数组的第 i 个键存在于索引项中。如果 consistent
方法需要咨询,则会传入原始 query
数据,queryKeys[]
和 nullFlags[]
数组也以前由 extractQuery
返回。extra_data
是由 extractQuery
返回的额外数据数组,或者在没有数据时为 NULL
。
当 extractQuery
在 queryKeys[]
中返回 null 键时,如果索引项包含一个 null 键,则对应的 check[]
元素为 true;也就是说,check[]
的语义类似于 IS NOT DISTINCT FROM
。consistent
函数可以检查对应的 nullFlags[]
元素,如果它需要区分常规值匹配和 null 匹配。
执行成功时,如果堆元组需要根据查询运算符重新进行检查,则应该将 *recheck
设为 true,或者如果索引测试准确,则应将其设为 false。即,返回 false 值可确保堆元组与查询不匹配;*recheck
设为 false 时,返回 true 值可确保堆元组与查询匹配;*recheck
设为 true 时,返回 true 值意味着堆元组可能与该查询匹配,因此,需要通过直接针对最初索引项评估查询运算符来获取该堆元组并重新检查它。
GinTernaryValue triConsistent(GinTernaryValue check[], StrategyNumber n, Datum query, int32 nkeys, Pointer extra_data[], Datum queryKeys[], bool nullFlags[])
triConsistent
与 consistent
相似,但并非在 check
向量中使用布尔值,而是针对每个 key 设置三个可能的值:GIN_TRUE
、GIN_FALSE
和 GIN_MAYBE
。 GIN_FALSE
和 GIN_TRUE
具有与常规布尔值相同的含义,而 GIN_MAYBE
意味着,存在该 key 的情况未知。在出现 GIN_MAYBE
值时,仅当该项肯定匹配(无论索引项是否包含对应的查询 key)时,函数才会返回 GIN_TRUE
。同样地,该函数仅在该项肯定不匹配(无论是否包含 GIN_MAYBE
key)时才会返回 GIN_FALSE
。如果结果取决于 GIN_MAYBE
条目,即,根据所知的查询 key 无法确认或反驳该匹配,则该函数必须返回 GIN_MAYBE
。
当 check
向量中没有 GIN_MAYBE
值时,GIN_MAYBE
返回值相当于在布尔 consistent
函数中设置 recheck
标志。
此外,GIN 必须有方法对存储在索引中的 key 值进行排序。运算符类可以通过指定比较方法来定义排序顺序
int compare(Datum a, Datum b)
比较两个 key(而非索引项!),并返回小于零、零或大于零的整数,以指示第一个 key 是否小于、等于或大于第二个 key。永远不会将空 key 传递给此函数。
另外,如果操作符类未提供 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
生成的 extra-data 数组的相应元素,如果没有相应的元素,则为 NULL
。空键绝不会传递给此函数。
void options(local_relopts *relopts)
定义一组用户可见参数来控制操作符类行为。
将一个 local_relopts
结构的指针传递到 options
函数,该指针需要填充一组特定于操作符类的选项。可以使用 PG_HAS_OPCLASS_OPTIONS()
和 PG_GET_OPCLASS_OPTIONS()
宏从其他支持函数访问这些选项。
由于索引值的键提取和GIN中的键表示都非常灵活,因此它们可能依赖于用户指定的参数。
为了支持 “部分匹配” 查询,操作符类必须提供 comparePartial
方法,且其 extractQuery
方法必须在遇到部分匹配查询时设置 pmatch
参数。请参阅 第 64.4.4.2 节 了解更多详情。
上述不同的 Datum
值的实际数据类型根据操作符类而异。传递给 extractValue
的项目值始终是操作符类的输入类型,所有键值必须是类的 STORAGE
类型。传递给 extractQuery
、consistent
和 triConsistent
的 query
参数的类型是策略编号标识的类成员操作符的正确的右手输入类型。只要可以从这种类型中提取正确类型的键值,它不必与索引类型相同。但是,建议这些三个支持函数的 SQL 声明使用 opclass 的索引数据类型作为 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
时对待处理条目列表进行清理。为避免观测到的响应时间出现波动,最好在后台执行待处理列表清理(即通过自动清理)。可通过增加 gin_pending_list_limit
或加大自动清理力度来避免前台清理操作。但是,扩大清理操作的阈值意味着,如果发生前台清理,所需时间会更长。
可以通过更改存储参数覆盖各个 GIN 索引的 gin_pending_list_limit
,这使得每个 GIN 索引都有自己的清理阈值。例如,可以只针对可能频繁更新的 GIN 索引增加阈值,而对其他索引降低阈值。
开发GIN索引的主要目标是为 PostgreSQL 中高度可伸缩全文搜索创建支持,并且经常会出现全文搜索返回非常大的结果集的情况。此外,当查询包含高频词汇时经常会发生这种情况,使得大的结果集根本无用。由于从磁盘中读取许多元组并进行排序需要花费大量时间,因此对于产品而言,这是不可接受的。(请注意,索引搜索本身非常快。)
为了方便此类查询的可控执行,GIN配置了返回的行数的上限软限制:gin_fuzzy_search_limit
配置参数。默认情况下将其设置为 0(表示没有限制)。如果设置非零限制,则返回的集合是整个结果集的一个子集,随机选择。
“软”指实际返回的结果数可能与指定限制略有不同,具体取决于查询和系统随机数生成器的质量。
根据经验,数千(例如,5000 — 20000)的值效果很好。
GIN假设可索引操作符是严格的。这意味着根本不会对 null 项值调用 extractValue
(相反,会自动创建一个占位符索引条目),并且也不对 null 查询值调用 extractQuery
(相反,查询被假定为不可满足)。但是请注意,支持包含在非 null 复合项或查询值中的 null 键值。