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

64.6. 哈希索引 #

64.6.1. 概述 #

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

哈希索引只支持单列索引,不允许唯一性检查。

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

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

哈希索引最适合于使用较大表上的等值扫描的 SELECT 和 UPDATE 密集型工作负载。在 B 树索引中,搜索必须向下遍历树直到找到叶页面。在拥有数百万行的表中,这种下降可能会增加对数据的访问时间。哈希索引中叶页面的等效项称为桶页面。相反,哈希索引允许直接访问桶页面,从而可能减少较大表中的索引访问时间。这种“逻辑 I/O”的减少在索引/数据大于 shared_buffers/RAM 时变得更加明显。

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

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

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

如果可以,VACUUM 还会尝试将索引元组压缩到尽可能少的溢出页面上,从而最大限度地减少溢出链。如果溢出页面变为空,可以回收溢出页面以供其他桶重新使用,尽管我们永远不会将其返回给操作系统。目前没有缩减哈希索引的规定,除了使用 REINDEX 重新构建之外。也没有减少桶数的规定。

随着被索引行的数量增加,哈希索引可能会扩展桶页面的数量。哈希键到桶号的映射选择为,索引可以增量扩展。当要向索引添加一个新的桶时,恰好一个现有桶将需要“分割”,根据更新的键到桶号映射将其中一些元组转移到新桶中。

扩展在前景中发生,这可能会增加用户插入的执行时间。因此,哈希索引可能不适合于行数快速增长的表。

64.6.2. 实现 #

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

扫描索引和插入元组都需要找到给定元组应该位于哪个桶中。为此,我们需要从元页面中获取桶计数、highmask 和 lowmask;但是,出于性能原因,对于每个此类操作都必须锁定和固定元页面是不可取的。相反,我们在每个后端 relcache 条目中保留元页面的缓存副本。只要目标桶自上次缓存刷新以来没有被分割,这就会产生正确的桶映射。

主桶页面和溢出页面是独立分配的,因为任何给定的索引可能需要更多或更少的溢出页面,而不是其桶的数量。哈希代码使用一组有趣的寻址规则来支持可变数量的溢出页面,而无需在创建后移动主桶页面。

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

用于扩展哈希索引的桶分割算法过于复杂,这里不再赘述,但在 src/backend/access/hash/README 中有更详细的描述。分割算法是崩溃安全的,如果未成功完成,可以重新启动。

提交更正

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