GiST framework

Gist(Generalized Search Tree,广义搜索树)是一种访问方法(是一个索引框架),本质上是对支持值之间相对位置关系的数据类型的平衡搜索树的一种泛化。B-tree 只能用于可排序的数据类型,即那些支持比较操作(比如大于、小于等)。对于这类类型,B-tree 的支持是非常高效的。而 Gist 则更为通用,它的操作符类(operator class)允许用户定义任意的数据分布规则,从而控制树的构造方式。因此,Gist 索引可以用于实现不同的数据结构,例如:

  • R-tree(二维/多维空间数据)
  • RD-tree(集合之间的相似性度量)
  • 签名树(类似 Bloom filter 的结构,用于快速过滤模糊匹配,比如文本)

由于 PostgreSQL 的可扩展性,你可以通过实现索引引擎的接口,从零开始创建一个新的访问方法(access method)。但是,除了设计索引逻辑之外,你还需要定义页面布局、高效的锁策略,以及其它底层支持功能。这一切都需要强大的编程能力和大量的实现工作。Gist 简化了这项任务,它处理了所有低层次的技术细节,并提供了搜索算法的基础框架。如果你想让某个新的数据类型支持 Gist 方法,你只需要添加一个新的操作符类(operator class),其中包含大约十几个支持函数(support functions)。与 B-tree 的“简单”操作符类不同,Gist 的操作符类承载了主要的索引逻辑。因此,从这个角度看,Gist 可以被认为是构建新访问方法的一个框架。

每个属于叶子节点的条目(称为“叶子条目”)都包含一个谓词(逻辑条件)和一个指向堆中元组(heap tuple)的引用。索引键(index key)必须满足这个谓词;至于这个键是否直接出现在条目中并不重要。(叶子节点中每一项不是“具体的值”,而是“某种条件”——这个条件是能覆盖实际数据的逻辑表达式。)

每个内部节点中的条目(称为“内部条目”)也包含一个谓词,以及一个指向子节点的引用;子树中所有的数据都必须满足这个谓词。换句话说,内部节点条目的谓词是其所有子节点谓词的“并集”。GiST 的这个重要特性(即“内部节点的谓词是子节点谓词的并集”)实现了类似于 B-tree 中的简单排序(simple ranking)功能(GiST 通过组织谓词的包含关系,实现了类似于 B-tree 按顺序剪枝的搜索效率)。

GiST 树的搜索依赖于 一致性函数(consistency function),它是由操作符类(operator class)定义的一种支持函数。

一致性函数会在某个索引项上被调用,用来判断这个条目的谓词是否与搜索条件一致(即与“索引列的操作符表达式”是否可能匹配)。对于内部节点的条目,一致性函数用于判断是否需要进入对应的子树;对于叶子节点的条目,它用于判断这个索引键是否满足查询条件

搜索从根节点开始,这是树形结构中常见的做法。一致性函数(consistency function)决定哪些子节点需要继续遍历,哪些可以被跳过。然后对选中的每个子节点重复这一过程;与 B-tree 不同,GiST 可能会同时有多个符合条件的子节点。被一致性函数选中的叶子节点条目将作为查询结果返回

GiST 的搜索始终是深度优先的:算法会尽可能尽快到达叶子页面。这种策略对返回前几条结果(Top-N 查询)尤其有利。

在向 GiST 树中插入一个新值时,无法使用一致性函数(consistent()),因为我们需要精确选择一个子节点进行插入。插入逻辑依赖于 penalty() 函数,去评估每个候选子节点“因插入新值而造成的扩展代价”;最终选择 penalty 最小的子节点 来插入。

和B树索引一样,叶子结点没有空间会造成页面分裂(split),split需要两个函数,一个负责在新老节点之间分布数据,另一个函数则对两个谓词进行并集操作,以更新父节点的谓词

随着新值不断插入,已有的谓词会不断扩大,而这些谓词通常只有在页面分裂或整个索引重建时才会被缩小。因此,频繁更新 GiST 索引可能会导致其性能下降

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals