GIN 是 Generalized Inverted Index(广义倒排索引)的 缩写。GIN 用于处理这样的场景:被索引的项是复合值, 而索引需要处理的查询要搜索出现在复合项内部的元素值。例如,项可以是 文档,而查询可以是搜索包含特定单词的文档。
我们用 项 一词指代要被索引的复合值,用 键 一词指代元素值。 GIN 总是存储并搜索键,而不是项值本身。
GIN 索引存储一组 (键, posting list) 对,其中 posting list 是出现该键的行 ID 的集合。同一行 ID 可以出现在多个 posting list 中,因为一个项可以包含多个键。每个键值 只存储一次,因此在同一个键出现很多次的场景下,GIN 索引非常紧凑。
GIN 之所以是广义的,是因为 GIN 访问方法代码不需要了解它所加速的具体操作。 它使用为特定数据类型定义的自定义策略。策略定义了如何从被索引的项和 查询条件中提取键,以及如何判断包含查询中某些键值的行是否真正满足 该查询。
GIN 的一个优点是:它允许由该数据类型领域的专家 (而非数据库专家)来开发带有相应访问方法的自定义数据类型。 这与使用 GiST 的优点大体相同。
PostgreSQL 中的 GIN 实现主要由 Teodor Sigaev 和 Oleg Bartunov 维护。关于 GIN 的更多信息见他们的 网站。
PostgreSQL 核心发行版包含 Table 65.3 所示的 GIN 操作符类。 (Appendix F 中描述的一些可选模块提供了额外的 GIN 操作符类。)
Table 65.3. 内置 GIN 操作符类
| Name | Indexable Operators |
|---|---|
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 支持的操作符更少,但对这些
操作符提供了更好的性能。
详情见 Section 8.14.4。
GIN 接口具有很高的抽象层次,只要求访问方法的实现者 实现被访问数据类型的语义。GIN 层本身负责并发、 日志记录以及树结构的搜索。
要让一个 GIN 访问方法工作起来,只需实现少量 用户定义的方法,这些方法定义了树中键的行为,以及键、被索引项和可索引 查询之间的关系。简而言之,GIN 把可扩展性与通用性、 代码复用以及简洁的接口结合在了一起。
GIN 的操作符类必须提供两个方法:
Datum *extractValue(Datum itemValue, int32 *nkeys,
bool **nullFlags)
给定一个待索引的项,返回一个 palloc'd 的键数组。返回的键数量必须
存入 *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'd 的键数组;也就是说,
query 是某个可索引操作符右侧的值,其左侧是被索引
的列。
n 是该操作符在操作符类中的策略号(见
Section 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,那么除了包含
至少一个匹配键的项之外,完全不包含任何键的项也被视为候选匹配项。
(例如,这个模式对实现 is-subset-of 操作符很有用。)
如果 *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 的查询操作符
(或可能满足,如果返回了 recheck 指示),则返回 true。该函数无法
直接访问被索引项的值,因为 GIN 不显式存储项。
可用的信息是:从查询中提取的哪些键值出现在给定的被索引项中。
check 数组的长度为
nkeys,与 extractQuery 之前
为该 query 值返回的键数量相同。
如果被索引项包含对应的查询键,check 数组的
对应元素为 true,即若 (check[i] == true),则
extractQuery 结果数组的第 i 个键存在于被索引
项中。
原始的 query 值会被传入,以备
consistent 方法需要查看它;
extractQuery 之前返回的
queryKeys[] 和 nullFlags[]
数组同样会被传入。
extra_data 是 extractQuery
返回的额外数据数组,若没有则为 NULL。
当 extractQuery 在
queryKeys[] 中返回一个空键时,如果被索引项
包含一个空键,则对应的 check[] 元素为 true;
也就是说,check[] 的语义类似 IS NOT DISTINCT
FROM。如果 consistent 函数需要区分
常规值匹配与空值匹配,它可以查看对应的
nullFlags[] 元素。
成功时,如果堆元组需要针对查询操作符重新检查,应把
*recheck 设为 true;如果索引测试是精确的,则设为
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 参数。
详情见 Section 65.4.4.2。
上述各种 Datum 值的实际数据类型取决于操作符类。
传给 extractValue 的项值始终是操作符类的输入类型,
并且所有键值必须属于该类的 STORAGE 类型。
传给 extractQuery、
consistent 和 triConsistent
的 query 参数的类型,是由策略号所标识的类成员
操作符的右侧输入类型。它不必与被索引类型相同,只要能从中提取出正确
类型的键值即可。不过,建议这三个支持函数的 SQL 声明对
query 参数使用操作符类的被索引数据类型,即使实际
类型可能因操作符而异。
在内部,GIN 索引包含一棵构建在键之上的 B-tree 索引, 其中每个键是一个或多个被索引项的元素(例如数组的成员),并且叶页面中 的每个元组要么包含一个指向堆指针 B-tree 的指针(即 “posting tree”),要么在列表小到可以连同键值一起放入单个 索引元组时,包含一个简单的堆指针列表(即 “posting list”)。Figure 65.1 展示了 GIN 索引 的这些组成部分。
从 PostgreSQL 9.1 开始,空键值也可以被包含
在索引中。此外,对于根据 extractValue 判定为空或
不包含任何键的被索引项,索引中会包含占位空值。这使得应当找到空项的
搜索能够做到这一点。
多列 GIN 索引通过在复合值 (列号, 键值) 之上构建 单棵 B-tree 来实现。不同列的键值可以是不同的类型。
Figure 65.1. GIN 内部结构
更新 GIN 索引往往很慢,这是倒排索引的固有性质
决定的:插入或更新一个堆行可能导致对索引的多次插入(从被索引项中
提取出的每个键一次)。
GIN 能够把这项工作的很大一部分推迟,方法是先把新
元组插入一个临时的、未排序的待处理条目列表。
当表被清理或自动分析时,或者调用 gin_clean_pending_list
函数时,或者待处理列表超过
gin_pending_list_limit 时,这些条目会被移入主
GIN 数据结构,使用与初始索引创建时相同的大量插入
技术。这极大地提高了 GIN 索引的更新速度,即使算上
额外的清理开销也是如此。而且,这些开销工作可以由后台进程完成,而不必
在前台查询处理中进行。
这种方法的主要缺点是:除了搜索常规索引之外,还必须扫描待处理条目 列表,因此大量的待处理条目会显著拖慢搜索。另一个缺点是,虽然大多数 更新都很快,但导致待处理列表变得“过大”的那次更新会引发 一次立即的清理循环,因此比其他更新慢得多。正确使用 autovacuum 可以 把这两个问题都降到最低。
如果稳定的响应时间比更新速度更重要,可以通过关闭
GIN 索引的 fastupdate 存储参数
来禁用待处理条目。
详情见 CREATE INDEX。
GIN 可以支持“部分匹配”查询,即查询无法确定一个或多个键
的精确匹配,但可能的匹配落在一个相当窄的键值范围内(按
compare 支持方法确定的键排序顺序)。
此时 extractQuery 方法不返回要精确匹配的键值,
而是返回一个作为待搜索范围下界的键值,并把 pmatch
标志设为 true。然后使用 comparePartial
方法扫描该键范围。comparePartial 对匹配的索引键
必须返回零,对仍在待搜索范围内的不匹配键返回小于零,对超出可能匹配
范围的索引键返回大于零。
向 GIN 索引插入数据可能很慢,因为每个项很可能 会插入很多键。因此,在向表进行批量插入时,建议先删除 GIN 索引, 待批量插入完成后再重建。
当为 GIN 启用 fastupdate 时
(详情见 Section 65.4.4.1),代价会比未启用时小。
但对于非常大的更新,删除并重建索引可能仍是最佳选择。
GIN 索引的构建时间对
maintenance_work_mem 设置非常敏感;在索引创建
期间节省工作内存是不划算的。
在向启用了 fastupdate 的现有
GIN 索引连续插入数据的过程中,只要待处理条目
列表增长超过 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
(而是假定查询无法满足)。不过要注意,非空复合项或查询值中包含的空
键值是受支持的。
PostgreSQL 核心发行版包含前面
Table 65.3 所示的
GIN 操作符类。
以下 contrib 模块也包含
GIN 操作符类:
btree_gin多种数据类型的 B-tree 等价功能
hstore存储 (键, 值) 对的模块
intarray对 int[] 的增强支持
pg_trgm基于三元组匹配的文本相似度