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