2025年9月25日: PostgreSQL 18 发布!
支持的版本:当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本:devel
不支持的版本:12 / 11 / 10

65.6. 哈希索引 #

65.6.1. 概述 #

PostgreSQL 包含一种持久化磁盘哈希索引的实现,该实现完全可崩溃恢复。任何数据类型都可以通过哈希索引进行索引,包括没有明确线性排序的数据类型。哈希索引仅存储被索引数据的哈希值,因此对被索引的数据列的大小没有限制。

哈希索引仅支持单列索引,并且不允许进行唯一性检查。

哈希索引仅支持 `=` 运算符,因此指定范围操作的 WHERE 子句将无法利用哈希索引。

每个哈希索引元组仅存储 4 字节的哈希值,而不是实际的列值。因此,在索引 UUID、URL 等较长数据项时,哈希索引可能比 B-tree 小得多。缺少列值也使得所有哈希索引扫描都是有损的。哈希索引可以参与位图索引扫描和反向扫描。

哈希索引最适合用于对大型表进行等值扫描的 SELECT 和 UPDATE 密集型工作负载。在 B-tree 索引中,搜索必须通过树的层级一直下降到找到叶子节点。在拥有数百万行的表中,这种下降会增加数据访问时间。在哈希索引中,相当于叶子节点的部分被称为桶页(bucket page)。相比之下,哈希索引允许直接访问桶页,从而可能减少大型表的索引访问时间。这种“逻辑 I/O”的减少在索引/数据大于 shared_buffers/RAM 时会更加明显。

哈希索引的设计旨在应对哈希值分布不均的情况。如果哈希值分布均匀,则直接访问桶页效果很好。当插入导致桶页满时,额外的溢出页会被链接到该特定桶页,局部扩展存储匹配该哈希值的索引元组。在查询期间扫描哈希桶时,我们需要扫描所有的溢出页。因此,对于某些数据,不平衡的哈希索引在所需块访问次数方面可能比 B-tree 更差。

由于存在溢出情况,我们可以说哈希索引最适合唯一、几乎唯一的数据或每个哈希桶具有较少行数的数据。一种避免问题的方法是使用部分索引条件排除高度非唯一的索引值,但这在许多情况下可能不适用。

与 B-Trees 类似,哈希索引执行简单的索引元组删除。这是一项延迟维护操作,它删除已知可以安全删除的索引元组(那些其项标识符的 LP_DEAD 位已被设置的元组)。如果插入操作发现页面上没有可用空间,我们会尝试通过尝试删除死索引元组来避免创建新的溢出页。如果页面当时被固定,则无法进行删除。删除死索引指针也会在 VACUUM 期间发生。

如果可能,VACUUM 还会尝试将索引元组压缩到尽可能少的溢出页上,从而最小化溢出链。如果一个溢出页变空,溢出页就可以被回收以供其他桶重复使用,但我们永远不会将其返回给操作系统。目前除了使用 REINDEX 重建索引外,没有其他方法可以缩小哈希索引。同样也没有减少桶数量的方法。

随着被索引行数的增加,哈希索引可能会增加桶页的数量。哈希键到桶号的映射选择方式使得索引可以逐步扩展。当需要向索引添加一个新桶时,必须“分裂”一个现有的桶,并将该桶的一部分元组根据更新后的键到桶号的映射转移到新桶中。

扩展操作在前端执行,这可能会增加用户插入操作的执行时间。因此,对于行数快速增长的表,哈希索引可能不适用。

65.6.2. 实现 #

哈希索引中有四种页面:元数据页(页面零),其中包含静态分配的控制信息;主桶页;溢出页;以及位图页,用于跟踪已释放且可供重用的溢出页。出于寻址目的,位图页被视为溢出页的一个子集。

扫描索引和插入元组都需要定位给定元组应该在哪一个桶中。为此,我们需要元数据页中的桶数、highmask 和 lowmask;然而,出于性能考虑,不希望每次此类操作都锁定和固定元数据页。相反,我们在每个后端(backend)的 relcache 条目中保留元数据页的缓存副本。只要目标桶自上次缓存刷新以来未被分裂,这将产生正确的桶映射。

主桶页和溢出页是独立分配的,因为任何给定的索引相对于其桶的数量可能需要更多的或更少的溢出页。哈希码使用一套特殊的寻址规则来支持可变数量的溢出页,而无需在创建主桶页后移动它们。

表中被索引的每一行在哈希索引中都由一个单独的索引元组表示。哈希索引元组存储在桶页中,如果存在的话,也在溢出页中。我们通过将任何一个索引页中的索引项按哈希码排序来加快搜索速度,从而允许在索引页内使用二分搜索。但请注意,对于一个桶中不同索引页之间的哈希码相对顺序,*没有任何*假设。

用于扩展哈希索引的桶分裂算法过于复杂,不值得在此提及,但更详细的描述可在 src/backend/access/hash/README 中找到。分裂算法是崩溃安全的,如果未成功完成,可以重新启动。

提交更正

如果您在文档中发现任何不正确、与您对特定功能的体验不符或需要进一步澄清的内容,请使用 此表格 报告文档问题。