本节介绍一些可能对高级用户有用的 B-树索引实现细节。若要查看更详细、更加侧重内部机制的 B-树实现说明,请参见源码发布包中的 src/backend/access/nbtree/README。
PostgreSQL 的 B-树 索引是多层树结构,其中树的每一层都可以用作页的双向链表。索引的第一个段文件起始处的固定位置保存着一个元页。其余页面要么是叶页,要么是内部页。叶页位于树的最低层,其余各层都由内部页组成。每个叶页都包含指向表中行的元组。每个内部页都包含指向树中下一层的元组。通常,超过 99% 的页面都是叶页。内部页和叶页都使用Section 69.6中描述的标准页格式。
当现有叶页无法容纳一个新传入的元组时,B-树索引就会增加新的叶页。一次页拆分操作会把原本属于溢出页的一部分项移动到新页中,从而为这些项腾出空间。页拆分还必须在父页中插入一个指向新页的下行链接,这又可能导致父页继续拆分。页拆分会以递归的方式“向上级联”。当根页最终也容纳不下新的下行链接时,就会发生一次根页拆分操作。它通过创建一个位于原始根页之上的新根页,为树结构增加一个新的层级。
重复项是这样一种叶页元组(即指向表中行的元组):其中所有被索引的键列值,都与同一索引中至少另一个叶页元组对应列的值相匹配。重复元组在实践中相当常见。当启用一种可选技术时,B-树索引可以为重复项采用一种特殊且节省空间的表示形式:去重。
去重通过周期性地把一组组重复元组合并起来,为每一组形成一个 posting list 元组。在这种表示中,列键值只出现一次,后面跟着一个排好序的 TID 数组,指向表中的各行。这能显著减小那些每个值(或每一种不同列值组合)平均会出现多次的索引的存储大小。查询延迟可能显著降低,整体查询吞吐量也可能显著提升,例行索引清理的开销同样可能显著减少。
即使“重复项”中包含 NULL 值,B-树 去重同样有效,尽管根据任何 B-树操作符类的 = 成员,NULL 值彼此永远不相等。对于实现中任何理解磁盘上 B-树 结构的部分来说,NULL 只是索引值域中的另一个值而已。
去重过程是惰性发生的:当插入一个放不进现有叶页的新项时,就会进行去重。这会防止(或者至少推迟)叶页拆分。与 GIN 的 posting list 元组不同,B-树的 posting list 元组不需要在每次插入新的重复项时都扩展;它们只是叶页原始逻辑内容的一种替代物理表示。这种设计优先考虑混合读写工作负载下的一致性能。大多数客户端应用至少都能从去重中获得适度的性能收益。去重默认启用。
CREATE INDEX 和 REINDEX 都会应用去重来创建 posting list 元组,只是两者采用的策略略有不同。对于从表中取出的已排序输入中遇到的每一组普通重复元组,都会在被加入当前待写入叶页之前先合并成一个 posting list 元组。每个 posting list 元组都会尽量容纳更多的 TID。叶页按通常方式写出,不需要额外独立的去重过程。由于 CREATE INDEX 和 REINDEX 都是一次性的批处理操作,这种策略非常适合它们。
如果某个写密集型工作负载由于索引中的重复值很少甚至没有,而无法从去重中获益,那么它会承担很小且固定的性能损耗(除非显式禁用去重)。 deduplicate_items 存储参数可用于在单个索引内禁用去重。而只读工作负载绝不会因此遭受性能损失,因为读取 posting list 元组至少与读取标准元组表示一样高效。禁用去重通常并没有帮助。
B-树索引并不直接知道,在 MVCC 下同一个逻辑表行可能存在多个现存版本;对索引来说,每个元组都是一个独立对象,都需要自己的索引项。“版本重复项”有时会积累起来,并对查询延迟和吞吐量造成不利影响。这通常发生在以UPDATE为主的工作负载中,其中大多数单次更新都无法应用HOT 优化(通常是因为至少一个被索引列发生了修改,因此必须生成一组新的索引元组版本 — 每一个索引都需要一个新元组)。实际上,B-树去重能够缓解版本频繁更替导致的索引膨胀。注意,由于版本频繁更替,即使唯一索引中的元组存储在磁盘上时,也不一定在物理上唯一。去重优化会有选择地应用于唯一索引,针对的是那些看起来存在版本重复项的页面。其总体目标是在版本频繁更替引发一次“不必要的”页拆分之前,给VACUUM更多运行时间。
系统会应用一种特殊的启发式规则,来判定唯一索引中是否应当执行一次去重轮次。它往往可以直接跳到拆分叶页,从而避免把周期浪费在无益的去重过程中而造成性能损耗。如果你担心去重的开销,可以考虑有选择地设置 deduplicate_items = off。在唯一索引中保持去重启用,坏处很小。
由于实现层面的限制,并非所有情况下都能使用去重。去重是否安全,是在运行 CREATE INDEX 或 REINDEX 时确定的。
请注意,在下列情况下,相等的 datum 之间存在语义上重要的差异,因此去重被视为不安全且不可使用:
text、varchar 和 char 在使用非确定性排序规则时,不能使用去重。必须保留相等的 datum 之间的大小写和重音差异。
numeric 不能使用去重。必须保留相等的 datum 之间的小数位数。
jsonb 不能使用去重,因为 jsonb 的 B-树操作符类在内部使用了 numeric。
float4 和 float8 不能使用去重。这些类型对 -0 和 0 有不同的表示形式,但它们仍然被视为相等,这种差异必须保留。
还有一个实现层面的限制,未来版本的 PostgreSQL 也许会取消它:
容器类型(例如复合类型、数组或范围类型)不能使用去重。
还有一个实现层面的限制,无论使用何种操作符类或排序规则都适用:
INCLUDE 索引永远不能使用去重。