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