PostgreSQL 提供了持久化磁盘哈希索引的实现,它完全支持崩溃恢复。任何数据类型都可以用哈希索引来建立索引,包括没有明确定义线性排序的数据类型。哈希索引只存储被索引数据的哈希值,因此对被索引数据列的大小没有任何限制。
哈希索引只支持单列索引,并且不允许唯一性检查。
哈希索引只支持 = 操作符,因此指定范围操作的 WHERE 子句无法利用哈希索引。
每个哈希索引元组只存储 4 字节的哈希值,存储实际的列值。因此,在为 UUID、URL 等较长的数据项建立索引时,哈希索引可能比 B 树小得多。列值的缺失也使所有哈希索引扫描都是有损的。哈希索引可以参与位图索引扫描与反向扫描。
哈希索引最适合针对在较大的表上执行等值扫描、且 SELECT 与 UPDATE 密集的工作负载进行优化。在 B 树索引中,搜索必须沿树下降,直到找到叶子页面。在拥有数百万行的表中,这种下降会增加数据访问时间。哈希索引中与叶子页面对应的概念被称为桶页面。与此相对,哈希索引允许直接访问桶页面,从而可能减少在较大表上的索引访问时间。当索引/数据规模超过 shared_buffers/RAM 时,这种“逻辑 I/O” 的减少会更加明显。
哈希索引在设计上能够应对哈希值分布不均的情况。如果哈希值分布均匀,对桶页面的直接访问会工作得很好。当插入导致桶页面变满时,额外的溢出页面会被链接到该特定桶页面,在局部扩展与该哈希值匹配的索引元组的存储空间。在查询期间扫描哈希桶时,我们需要扫描所有的溢出页面。因此对某些数据而言,不均衡的哈希索引所需的块访问次数实际上可能比 B 树更糟。
基于溢出的情形,可以说哈希索引最适合唯一或接近唯一的数据,或每个哈希桶行数较少的数据。一种可能的规避手段是使用部分索引条件把高度非唯一的值排除出索引,但这在许多情况下未必适用。
与 B 树一样,哈希索引执行简单的索引元组删除。这是一种延迟维护操作,删除已知可以安全删除的索引元组(其条目标识符的 LP_DEAD 位已被置位的那些)。如果插入时发现页面上没有可用空间,我们会尝试移除死亡索引元组,以避免创建新的溢出页面。如果页面当时被固定,则无法执行移除。VACUUM 期间也会删除死亡索引指针。
只要可以,VACUUM 还会尝试把索引元组挤压到尽可能少的溢出页面中,使溢出链最小化。如果一个溢出页面变空,溢出页面可以被回收以供其他桶重用,但我们从把它们归还给操作系统。目前除了用 REINDEX 重建之外,没有任何收缩哈希索引的机制。 也没有减少桶数量的机制。
随着被索引行数的增长,哈希索引可能扩展桶页面的数量。哈希键到桶编号的映射经过选择,使索引可以增量扩展。当要向索引添加新桶时,恰好有一个现有桶需要被“分裂”,按照更新后的键到桶编号映射,把它的部分元组转移到新桶中。
扩展在前台进行,这可能增加用户插入的执行时间。因此,对于行数快速增长表,哈希索引可能不适合。
哈希索引中有四种页面:元页面(第零页),包含静态分配的控制信息;主桶页面;溢出页面;以及位图页面,用于跟踪已被释放、可供重用的溢出页面。出于寻址目的,位图页面被视为溢出页面的一个子集。
扫描索引与插入元组都需要定位给定元组应当所在的桶。为此,我们需要元页面中的桶数量、highmask 与 lowmask;但出于性能考虑,为每次此类操作都锁定并固定元页面并不可取。相反,我们在每个后端的 relcache 条目中保留元页面的缓存副本。只要目标桶自上次缓存刷新以来未被分裂,这就能产生正确的桶映射。
主桶页面与溢出页面独立分配,因为任何给定索引相对于其桶数量可能需要更多或更少的溢出页面。哈希代码使用一组有趣的寻址规则来支持数量可变的溢出页面,同时不必在主桶页面创建之后移动它们。
被索引表中的每一行都由哈希索引中的单个索引元组表示。哈希索引元组存储在桶页面以及(如果存在)溢出页面中。我们把任一索引页面中的索引条目按哈希码排序以加快搜索,从而允许在索引页面内使用二分搜索。不过请注意,关于同一桶的不同索引页面之间哈希码的相对顺序,*没有任何*假设。
用于扩展哈希索引的桶分裂算法过于复杂,不值得在此展开,更多细节在
src/backend/access/hash/README 中有描述。分裂算法是崩溃安全的,如果未能成功完成可以重新开始。