支持的版本:当前 (17) / 16 / 15 / 14 / 13
开发版本:devel
不支持的版本: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. 实现 #

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

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

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

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

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

提交更正

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